Files
raisfast/docs/oauth2-flow.md
T

38 KiB

OAuth2 用户注册/登录流程图

整体时序图

┌────────┐     ┌────────────┐     ┌──────────────┐     ┌──────────────────┐
│  前端   │     │ raisfast  │     │ GitHub OAuth │     │   SQLite DB      │
│ 浏览器  │     │  后端 API  │     │   Provider   │     │                  │
└───┬────┘     └─────┬──────┘     └──────┬───────┘     └────────┬─────────┘
    │                │                    │                      │
    │  1. GET /api/v1/auth/oauth/github  │                      │
    │───────────────>│                    │                      │
    │                │                    │                      │
    │                │  2. 检查 OAuth 是否启用                     │
    │                │  3. 检查 GitHub Provider 是否已配置          │
    │                │                    │                      │
    │                │  4. 生成 state (32字节随机hex)              │
    │                │  5. 生成 code_verifier (43字符随机串)       │
    │                │  6. 计算 code_challenge = BASE64URL(SHA256(code_verifier))
    │                │                    │                      │
    │                │  7. INSERT INTO oauth_states               │
    │                │    (state, provider, code_verifier,        │
    │                │     expires_at = now + 10min)              │
    │                │───────────────────────────────────────────>│
    │                │                    │                      │
    │                │  8. 构建 GitHub 授权 URL                    │
    │                │    https://github.com/login/oauth/authorize
    │                │    ?client_id=xxx                             │
    │                │    &state=xxx                                 │
    │                │    &code_challenge=xxx                        │
    │                │    &code_challenge_method=S256                │
    │                │    &scope=user:email                          │
    │                │                    │                      │
    │  9. 302 重定向到 GitHub 授权页    │                      │
    │<───────────────│                    │                      │
    │                │                    │                      │
    │  10. 用户在 GitHub 页面登录并授权  │                      │
    │────────────────────────────────────>│                      │
    │                │                    │                      │
    │  11. GitHub 回调带 code + state    │                      │
    │<────────────────────────────────────│                      │
    │                │                    │                      │
    │  12. GET /api/v1/auth/oauth/github/callback?code=xxx&state=xxx
    │───────────────>│                    │                      │
    │                │                    │                      │
    │                │  ┌─────────────────────────────────────┐  │
    │                │  │       回调处理核心流程                │  │
    │                │  │  (见下方详细流程图)                   │  │
    │                │  └─────────────────────────────────────┘  │
    │                │                    │                      │
    │  13. 302 重定向到前端回调页        │                      │
    │      ?access_token=xxx            │                      │
    │      &refresh_token=xxx           │                      │
    │      &expires_in=900              │                      │
    │<───────────────│                    │                      │
    │                │                    │                      │
    │  14. 前端提取 URL 参数,存入 localStorage,跳转首页       │
    │                │                    │                      │

回调处理核心流程

                        ┌─────────────────────┐
                        │  收到 callback 请求  │
                        │  provider + code    │
                        │  + state            │
                        └──────────┬──────────┘
                                   │
                                   ▼
                 ┌─────────────────────────────────┐
                 │  1. 检查 OAuth 是否启用           │
                 │     config.oauth.enabled == true │
                 └──────────────┬──────────────────┘
                                │
                          No ───┴─── Yes
                          │          │
                          ▼          ▼
                   ┌──────────┐  ┌──────────────────────────┐
                   │ 返回 400 │  │ 2. 从 DB 查找并消费 state  │
                   │ OAuth    │  │    SELECT * FROM           │
                   │ 未启用   │  │    oauth_states            │
                   └──────────┘  │    WHERE id = ?            │
                                 │    AND expires_at > now()  │
                                 │    然后 DELETE (一次性使用)  │
                                 └────────────┬───────────────┘
                                              │
                                    ┌─────────┴─────────┐
                                    │                   │
                              未找到/已过期          找到 state
                                    │                   │
                                    ▼                   ▼
                             ┌──────────┐  ┌──────────────────────┐
                             │ 返回 400 │  │ 3. 校验 provider 匹配  │
                             │ 无效或   │  │    state.provider ==   │
                             │ 过期的   │  │    请求的 provider     │
                             │ state    │  └──────────┬───────────┘
                             └──────────┘             │
                                                不匹配 │ 匹配
                                                      │ │
                                                      ▼ ▼
                                               ┌────────┐  ┌──────────────────────────┐
                                               │ 400    │  │ 4. 用 code 换 access_token │
                                               │        │  │    POST github.com/       │
                                               └────────┘  │    login/oauth/           │
                                                           │    access_token          │
                                                           │    ┌──────────────────┐  │
                                                           │    │ client_id       │  │
                                                           │    │ client_secret   │  │
                                                           │    │ code            │  │
                                                           │    │ code_verifier   │  │
                                                           │    └──────────────────┘  │
                                                           └──────────┬───────────────┘
                                                                      │
                                                                      ▼
                                                           ┌──────────────────────┐
                                                           │ 5. 获取 GitHub 用户信息│
                                                           │    GET api.github.com/ │
                                                           │    user               │
                                                           │    Header: Bearer token│
                                                           └──────────┬───────────┘
                                                                      │
                                                                      ▼
                                                           ┌──────────────────────┐
                                                           │ 6. 检查邮箱是否返回   │
                                                           │    profile.email ?    │
                                                           └──────────┬───────────┘
                                                                      │
                                                          有邮箱      │     无邮箱
                                                          直接用      │     或为空
                                                              │        │        │
                                                              │        │        ▼
                                                              │        │  ┌─────────────────────┐
                                                              │        │  │ 额外调用              │
                                                              │        │  │ GET /user/emails      │
                                                              │        │  │ 找 primary+verified   │
                                                              │        │  │ 的邮箱                │
                                                              │        │  └──────────┬──────────┘
                                                              │        │             │
                                                              ▼        ▼             ▼
                                                           ┌──────────────────────────────┐
                                                           │  得到 OAuthUserInfo:          │
                                                           │  - provider_user_id (GitHub ID)│
                                                           │  - email                      │
                                                           │  - display_name (login)       │
                                                           │  - avatar_url                 │
                                                           │  - raw_profile (完整 JSON)     │
                                                           └──────────────┬───────────────┘
                                                                          │
                                                                          ▼

用户查找/创建决策树

                     ┌──────────────────────────┐
                     │ 拿到 OAuthUserInfo        │
                     │ (provider + user_id +     │
                     │  email + name + avatar)   │
                     └────────────┬─────────────┘
                                  │
                                  ▼
                 ┌────────────────────────────────────┐
                 │ A. 查找已有 OAuth 绑定               │
                 │    SELECT * FROM oauth_accounts     │
                 │    WHERE provider = ?               │
                 │    AND provider_user_id = ?         │
                 └──────────────┬─────────────────────┘
                                │
                        ┌───────┴───────┐
                        │               │
                    找到绑定         未找到绑定
                        │               │
                        ▼               ▼
            ┌─────────────────┐  ┌──────────────────────────────┐
            │ 已绑定用户!     │  │ B. 检查 state.user_id         │
            │                 │  │    (发起时已登录用户传入)       │
            │ 1. 查找本地用户  │  │    → 表示"绑定"操作            │
            │ 2. 更新 OAuth   │  └──────────────┬───────────────┘
            │    绑定信息      │                 │
            │    (token/profile│          ┌──────┴──────┐
            │     等刷新)      │          │             │
            │ 3. 签发 JWT +   │      有 user_id    无 user_id
            │    refresh_token│          │             │
            │ 4. 302 重定向   │          ▼             ▼
            │    到前端        │  ┌─────────────┐ ┌──────────────────┐
            └─────────────────┘  │ C. 绑定模式 │ │ D. 检查邮箱匹配   │
                                 │             │ │                  │
                                 │ 1. 绑定到   │ │ 有 email 且      │
                                 │    当前用户 │ │ 能在 users 表    │
                                 │ 2. 签发 JWT │ │ 找到匹配邮箱?   │
                                 │ 3. 重定向   │ └────────┬─────────┘
                                 └──────┬──────┘          │
                                        │          ┌──────┴──────┐
                                        │          │             │
                                        │     找到匹配用户   无匹配用户
                                        │          │             │
                                        │          ▼             ▼
                                        │  ┌──────────────┐ ┌──────────────────────┐
                                        │  │ E. 自动绑定   │ │ F. 自动注册新用户     │
                                        │  │              │ │                      │
                                        │  │ 1. 将 OAuth  │ │ 1. 生成唯一 username  │
                                        │  │    绑定到    │ │    base = login 名    │
                                        │  │    匹配用户  │ │    sanitize 清理      │
                                        │  │ 2. 签发 JWT  │ │    冲突→加前缀/后缀   │
                                        │  │ 3. 重定向    │ │                      │
                                        │  └──────┬───────┘ │ 2. 生成占位密码       │
                                        │         │         │    "!oauth:github:xxx"│
                                        │         │         │                      │
                                        │         │         │ 3. INSERT INTO users  │
                                        │         │         │    role = 'reader'    │
                                        │         │         │                      │
                                        │         │         │ 4. UPDATE users SET   │
                                        │         │         │    avatar = 头像URL   │
                                        │         │         │    email_verified = 1 │
                                        │         │         │                      │
                                        │         │         │ 5. 发射 UserRegistered│
                                        │         │         │    事件               │
                                        │         │         │                      │
                                        │         │         │ 6. 创建 OAuth 绑定    │
                                        │         │         └──────────┬───────────┘
                                        │         │                    │
                                        ▼         ▼                    ▼
                                        ┌──────────────────────────────────┐
                                        │     签发 JWT + refresh_token      │
                                        │                                  │
                                        │  1. generate_access_token_internal│
                                        │     (user_id, role, tenant_id,    │
                                        │      jwt_secret, expires_in)      │
                                        │                                  │
                                        │  2. generate_refresh_token_string │
                                        │     (32字节随机 hex)              │
                                        │                                  │
                                        │  3. INSERT INTO refresh_tokens    │
                                        │                                  │
                                        │  4. 302 重定向到前端:              │
                                        │     {redirect_url}                │
                                        │     ?access_token=xxx             │
                                        │     &refresh_token=xxx            │
                                        │     &expires_in=900               │
                                        └──────────────────────────────────┘

用户名自动生成策略

输入: display_name = "octocat"
      │
      ▼
┌─────────────────────┐
│ sanitize_username()  │
│ 只保留 a-z A-Z 0-9 _ │
│ 去除首尾下划线        │
└──────────┬──────────┘
           │
           ▼
    username = "octocat"
           │
           ▼
┌─────────────────────────────┐
│ SELECT * FROM users          │
│ WHERE username = 'octocat'   │
└──────────────┬──────────────┘
               │
        ┌──────┴──────┐
        │             │
    不存在(OK)     已存在
        │             │
        ▼             ▼
  返回 "octocat"  ┌────────────────────────────────┐
                  │ 尝试加 GitHub 前缀              │
                  │ username = "github_octocat"     │
                  │ SELECT ... WHERE username = ?   │
                  └──────────────┬─────────────────┘
                                 │
                          ┌──────┴──────┐
                          │             │
                      不存在(OK)     已存在
                          │             │
                          ▼             ▼
                   返回               ┌──────────────────────────┐
                   "github_octocat"   │ 追加 4 位随机 hex 后缀    │
                                      │ suffix = random_hex(2)   │
                                      │ "github_octocat_a3f8"    │
                                      └──────────────────────────┘

安全机制

┌──────────────────────────────────────────────────────────────────┐
│                        安全防护层                                 │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────── PKCE (Proof Key for Code Exchange) ────────────┐ │
│  │                                                             │ │
│  │  客户端(后端)              GitHub                           │ │
│  │  ┌─────────────┐          ┌──────────┐                     │ │
│  │  │ 生成随机     │          │          │                     │ │
│  │  │ code_verifier│────────>│ 存储     │                     │ │
│  │  │ (43 字符)    │          │ code_    │                     │ │
│  │  └──────┬──────┘          │ challenge│                     │ │
│  │         │                 │          │                     │ │
│  │         ▼                 │          │                     │ │
│  │  SHA256(code_verifier)    │          │                     │ │
│  │         │                 │          │                     │ │
│  │         ▼                 │          │                     │ │
│  │  BASE64URL(hash)          │          │                     │ │
│  │  = code_challenge         │          │                     │ │
│  │                           │          │                     │ │
│  │  授权请求 ───────────────>│ 校验     │                     │ │
│  │  带 code_challenge        │ challenge│                     │ │
│  │                           │          │                     │ │
│  │  回调时 ─────────────────>│ 用       │                     │ │
│  │  带 code_verifier         │ verifier │                     │ │
│  │                           │ 重新计算 │                     │ │
│  │                           │ 验证匹配 │                     │ │
│  │                           └──────────┘                     │ │
│  │                                                             │ │
│  │  防止:authorization code 被截获后无法使用                    │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  ┌──────────── State 参数 (CSRF 防护) ────────────────────────┐ │
│  │                                                             │ │
│  │  发起时:                                                    │ │
│  │    state = random_hex(32)   →  64 字符随机串               │ │
│  │    INSERT INTO oauth_states (id=state, ..., expires_at)    │ │
│  │                                                             │ │
│  │  回调时:                                                    │ │
│  │    SELECT * FROM oauth_states WHERE id = ?                 │ │
│  │    AND expires_at > datetime('now')                        │ │
│  │    DELETE FROM oauth_states WHERE id = ?  ← 一次性使用     │ │
│  │                                                             │ │
│  │  防止:CSRF 攻击(伪造回调请求)                             │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  ┌──────────── Rate Limiting ─────────────────────────────────┐ │
│  │                                                             │ │
│  │  OAuth 端点受全局 rate_limit 保护                           │ │
│  │  默认 60 次/分钟/IP                                         │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  ┌──────────── 解绑安全 ──────────────────────────────────────┐ │
│  │                                                             │ │
│  │  解绑前检查:                                                │ │
│  │    if user.password_hash 是占位值 (以 "!oauth:" 开头)       │ │
│  │       AND oauth 绑定数量 <= 1                               │ │
│  │    then 拒绝解绑                                           │ │
│  │    → 防止用户无法登录                                       │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

数据库表关系

┌──────────────────────┐       ┌──────────────────────────┐
│       users          │       │     oauth_accounts        │
├──────────────────────┤       ├──────────────────────────┤
│ id TEXT PK           │◄──┐   │ id TEXT PK               │
│ tenant_id TEXT       │   │   │ user_id TEXT FK ─────────┤──┘
│ email TEXT UNIQUE    │   │   │ provider TEXT            │    一个用户可绑定
│ username TEXT UNIQUE │   │   │ provider_user_id TEXT    │    多个 Provider
│ password_hash TEXT   │   │   │ email TEXT               │
│ role TEXT            │   │   │ display_name TEXT        │
│ avatar TEXT          │   │   │ avatar_url TEXT          │
│ bio TEXT             │   │   │ access_token TEXT        │
│ website TEXT         │   │   │ refresh_token TEXT       │
│ email_verified INT   │   │   │ token_expires_at TEXT   │
│ created_at TEXT      │   │   │ profile TEXT (JSON)      │
│ updated_at TEXT      │   │   │ created_at TEXT          │
└──────────────────────┘   │   │ updated_at TEXT          │
                           │   └──────────────────────────┘
                           │
                           │   UNIQUE(provider, provider_user_id)
                           │
                           │   ┌──────────────────────────┐
                           │   │     oauth_states          │
                           │   ├──────────────────────────┤
                           │   │ id TEXT PK (= state 值)   │
                           │   │ provider TEXT             │
                           └───│ user_id TEXT FK (可空)    │  ← 绑定模式传入
                               │ code_verifier TEXT        │
                               │ created_at TEXT           │
                               │ expires_at TEXT           │
                               │                          │
                               │ 10 分钟后过期             │
                               │ Worker cron 定期清理      │
                               └──────────────────────────┘

前端对接示例

┌────────────────────────────────────────────────────────────────┐
│  登录页 (/login)                                               │
│                                                                │
│  ┌─────────────────────────────────┐                          │
│  │  邮箱: [________________]        │                          │
│  │  密码: [________________]        │                          │
│  │  [ 登 录 ]                       │                          │
│  │                                  │                          │
│  │  ────── 或 ──────                │                          │
│  │                                  │                          │
│  │  [GitHub 登录]  [Google 登录]     │  ← 调用 GET /api/v1/   │
│  │                                  │     auth/oauth/github   │
│  └─────────────────────────────────┘     (浏览器直接跳转)      │
│                                                                │
├────────────────────────────────────────────────────────────────┤
│  回调页 (/auth/callback)                                       │
│                                                                │
│  useEffect(() => {                                             │
│    const params = new URLSearchParams(location.search);        │
│    const accessToken = params.get('access_token');             │
│    const refreshToken = params.get('refresh_token');           │
│    const expiresIn = params.get('expires_in');                 │
│                                                                │
│    if (accessToken) {                                          │
│      localStorage.setItem('access_token', accessToken);       │
│      localStorage.setItem('refresh_token', refreshToken);     │
│      navigate('/');                                            │
│    } else {                                                    │
│      // 显示错误                                                │
│      const error = params.get('error');                        │
│      showToast(error || 'OAuth login failed');                 │
│      navigate('/login');                                       │
│    }                                                           │
│  }, []);                                                       │
│                                                                │
├────────────────────────────────────────────────────────────────┤
│  账号设置页 (/settings/account)                                 │
│                                                                │
│  已绑定的社交账号:                                               │
│  ┌──────────────────────────────────────┐                     │
│  │ 🐙 GitHub  octocat   [解绑]          │  ← DELETE /api/v1/  │
│  │ 🔵 Google  (未绑定)   [绑定]          │     auth/oauth/     │
│  │ 💬 微信    (未绑定)   [绑定]          │     github/unbind   │
│  └──────────────────────────────────────┘                     │
│                                                                │
│  [设置密码]  ← OAuth 用户首次设置密码后成为"混合登录"用户       │
│                                                                │
└────────────────────────────────────────────────────────────────┘

JWT Token 签发流程(OAuth 和普通登录共用)

                        ┌─────────────────────┐
                        │   确认用户身份       │
                        │   user_id + role     │
                        │   + tenant_id        │
                        └──────────┬──────────┘
                                   │
                                   ▼
                ┌──────────────────────────────────────┐
                │  生成 Access Token (JWT HS256)        │
                │                                      │
                │  Claims:                             │
                │  {                                   │
                │    "sub": "0192a3b4-user-id",        │
                │    "role": "reader",                 │
                │    "tenant_id": "default",           │
                │    "exp": 1713980000,                │
                │    "iat": 1713979100                 │
                │  }                                   │
                │                                      │
                │  签名: HMAC-SHA256(jwt_secret)        │
                │  有效期: 15 分钟 (可配置)              │
                └──────────────┬───────────────────────┘
                               │
                               ▼
                ┌──────────────────────────────────────┐
                │  生成 Refresh Token                   │
                │                                      │
                │  32 字节随机 → 64 字符 hex 字符串     │
                │  "a3f8b2c1d4e5f6...64chars"          │
                │                                      │
                │  INSERT INTO refresh_tokens           │
                │  有效期: 7 天 (可配置)                 │
                └──────────────┬───────────────────────┘
                               │
                               ▼
                ┌──────────────────────────────────────┐
                │  返回给前端 (通过 URL 参数)            │
                │                                      │
                │  {redirect_url}                      │
                │    ?access_token=eyJhbG...            │
                │    &refresh_token=a3f8b2c1...         │
                │    &expires_in=900                    │
                └──────────────────────────────────────┘