# 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 │ └──────────────────────────────────────┘ ```