You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
22 KiB
22 KiB
OAuth2 模块化功能设计方案
创建日期:2026-05-26 状态:已批准
一、架构概览
┌─────────────────────────────────────────────────────────┐
│ 前端 (Vue) │
│ /pages/auth/login.vue (集成 GitHub 登录按钮) │
└─────────────────────┬───────────────────────────────────┘
│ 1. 点击登录
▼
┌─────────────────────────────────────────────────────────┐
│ 后端 API (Nitro) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ OAuthModule (核心调度器) │ │
│ │ - registerProvider() 注册 Provider │ │
│ │ - getAuthorizationUrl() 生成授权 URL │ │
│ │ - handleCallback() 处理回调 │ │
│ │ - bindAccount() 绑定账号 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ GitHubProvider │ │ GoogleProvider │ ... (可扩展) │
│ │ (implements) │ │ (implements) │ │
│ └────────────────┘ └────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ OAuthStateStore (state 临时存储,5min TTL) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AuthService (现有认证服务) │ │
│ │ - getCurrentUser() │ │
│ │ - loginUser() / registerUser() │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 数据库 (SQLite) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ users │ │ sessions │ │oauth_accounts│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
核心设计原则:
- 配置驱动 — 内置 Provider 通过
oauth.providers.ts配置注册,零代码添加新 Provider - 策略模式 — 每个 Provider 是独立策略类,实现统一接口
- 最小改动 — 集成到现有登录页,不影响现有认证逻辑
二、核心模块设计
2.1 OAuth Provider 接口
// packages/oauth/src/providers/base.ts
export interface OAuthProvider {
name: string; // 'github' | 'google'
icon: string; // emoji 或 Icon 组件名
clientId: string;
clientSecret: string;
authorizeUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[]; // 请求的权限范围
// 特定字段映射(不同 Provider 的返回字段不同)
mapUserInfo(raw: Record<string, unknown>): OAuthUserInfo;
}
export type OAuthUserInfo = {
provider: string;
providerUserId: string; // 第三方平台的用户 ID
username?: string;
email?: string;
avatar?: string;
};
2.2 OAuth 模块核心
// packages/oauth/src/core/oauth-manager.ts
export class OAuthManager {
// 注册 Provider
registerProvider(provider: OAuthProvider): void;
// 生成授权 URL(含 state)
async getAuthorizationUrl(providerName: string): Promise<string>;
// 处理回调(交换 token,获取用户信息)
async handleCallback(providerName: string, code: string, state: string): Promise<OAuthCallbackResult>;
// 绑定账号(已登录用户绑定第三方账号)
async bindAccount(userId: number, providerName: string, providerUserId: string): Promise<void>;
// 解绑账号
async unbindAccount(userId: number, providerName: string): Promise<void>;
// 获取用户已绑定的 OAuth 账号列表
async getUserBindings(userId: number): Promise<OAuthBinding[]>;
}
export type OAuthCallbackResult = {
success: boolean;
isNewUser: boolean;
user?: MinimalUser;
requiresBinding: boolean; // 账号已存在但未绑定,需用户确认
bindingToken?: string; // 临时 binding token
};
export type OAuthBinding = {
id: number;
provider: string;
username: string | null;
email: string | null;
avatar: string | null;
boundAt: Date;
};
2.3 State 管理(防止 CSRF)
// packages/oauth/src/core/oauth-state-store.ts
// 内存 Map + 5min TTL 过期清理
// State 包含: providerName, redirectUri, createdAt, userId(可选)
State 存储结构:
type OAuthState = {
providerName: string;
redirectUri: string;
createdAt: number; // timestamp
userId?: number; // 绑定模式下才有值
isBinding: boolean; // 是否为绑定模式
};
三、数据库设计
3.1 oauth_accounts 表
CREATE TABLE `oauth_accounts` (
`id` integer PRIMARY KEY AUTOINCREMENT,
`user_id` integer NOT NULL,
`provider` text NOT NULL,
`provider_user_id` text NOT NULL,
`username` text,
`email` text,
`avatar` text,
`created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
`updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
UNIQUE(`provider`, `provider_user_id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade
);
CREATE INDEX `oauth_accounts_user_id_idx` ON `oauth_accounts` (`user_id`);
索引设计: provider + provider_user_id 唯一索引确保不能重复绑定。
四、API 设计
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/auth/oauth/:provider/authorize |
生成授权 URL,重定向到 GitHub |
| GET | /api/auth/oauth/:provider/callback |
GitHub 回调,处理 token 交换 |
| POST | /api/auth/oauth/bind |
已登录用户绑定 OAuth 账号 |
| DELETE | /api/auth/oauth/:provider/unbind |
解绑 OAuth 账号 |
| GET | /api/auth/oauth/bindings |
获取当前用户的绑定列表 |
| GET | /api/auth/oauth/:provider/status |
查询某 Provider 的绑定状态 |
4.1 授权接口
GET /api/auth/oauth/:provider/authorize
Query Parameters:
- bind: number (可选) 绑定模式下传递 1
- redirect_uri: string (可选) 自定义回调跳转地址
Response: 302 重定向到 Provider 授权页面
4.2 回调接口
GET /api/auth/oauth/:provider/callback
Query Parameters:
- code: string Provider 授权码
- state: string 防止 CSRF 的状态令牌
Response: 302 重定向到前端页面
- 登录成功:/auth/login?oauth_success=1
- 绑定成功:/profile?bind_success=1
- 失败:/auth/login?oauth_error=xxx
4.3 绑定接口
POST /api/auth/oauth/bind
Body:
{
bindingToken: string; // 回调时返回的临时 token
}
Response:
{
success: true,
binding: {
provider: string;
username: string | null;
}
}
4.4 解绑接口
DELETE /api/auth/oauth/:provider/unbind
Response:
{
success: true
}
4.5 绑定列表接口
GET /api/auth/oauth/bindings
Response:
{
bindings: [
{
id: number;
provider: string;
username: string | null;
email: string | null;
avatar: string | null;
boundAt: Date;
}
]
}
4.6 单个 Provider 绑定状态
GET /api/auth/oauth/:provider/status
Response:
{
bound: boolean;
binding?: {
id: number;
username: string | null;
}
}
五、流程图
5.1 新用户 OAuth 登录流程
┌──────────────────────────────────────────────────────────────────┐
│ GitHub OAuth 登录流程 │
└──────────────────────────────────────────────────────────────────┘
用户 前端 后端 GitHub
│ │ │ │
│ 点击 GitHub 登录按钮 │ │ │
│──────────────────────>│ │ │
│ │ │ │
│ │ GET /api/auth/oauth/github/authorize │
│ │────────────────────────────────────────────>
│ │ │ │
│ │ │ 1. 生成 state │
│ │ │ 2. 存储 state │
│ │ │ 3. 拼接授权 URL │
│ │ │<────────────────────────────────────────────
│ │ │ │
│<──────────────────────│ 302 重定向到 GitHub │ │
│ │ │ │
│ 在 GitHub 授权页面 │ │ │
│ 确认授权 │ │ │
│───────────────────────│───────────────────────│──────────────────────>
│ │ │ │
│ │ │ GitHub 回调 │
│ │ │ ?code=xxx&state=xxx │
│ │ │<──────────────────────│
│ │ │ │
│ │ │ 验证 state │
│ │ │ 用 code 换 token │
│ │ │ 获取用户信息 │
│ │ │──────────────────────>
│ │ │ │
│ │ │ 创建/查找用户 │
│ │ │ 创建 session │
│ │ │ │
│<──────────────────────│ 302 重定向到 /auth/login?oauth_success=1 │
│ │ │ │
│ 检测 url参数 │ │ │
│ 刷新 session │ │ │
│──────────────────────>│ │ │
│ │ │ │
│ │ GET /api/auth/session │
│ │────────────────────────────────────────────>
│ │ │ │
│ │<─────────────────────────────────────────────│ 登录成功
│ │ │ │
│ 显示登录成功 │ │ │
│ │ │ │
5.2 已注册用户绑定 GitHub 流程
┌──────────────────────────────────────────────────────────────────┐
│ 账号绑定流程 │
└──────────────────────────────────────────────────────────────────┘
已登录用户 前端 后端 GitHub
│ │ │ │
│ 点击绑定 GitHub │ │ │
│──────────────────────>│ │ │
│ │ │ │
│ │ GET /api/auth/oauth/github/authorize?bind=1 │
│ │────────────────────────────────────────────>
│ │ │ │
│ │ │ 生成 binding state │
│ │ │ 包含 userId │
│ │ │ │
│<──────────────────────│ 302 重定向到 GitHub │ │
│ │ │ │
│ 授权 │ │ │
│───────────────────────│───────────────────────│──────────────────────>
│ │ │ │
│ │ │ GitHub 回调 │
│ │ │ 验证 binding state │
│ │ │ 用 code 换 token │
│ │ │ 获取用户信息 │
│ │ │ │
│ │ │ 插入 oauth_accounts │
│ │ │ │
│<──────────────────────│ 302 重定向到 /profile?bind_success=1 │
│ │ │ │
│ 显示绑定成功 │ │ │
│ │ │ │
5.3 GitHub 用户映射关系
GitHub API 返回字段 -> OAuthUserInfo 映射 (GitHubProvider)
GitHub 返回:
{
"id": 123456,
"login": "username",
"email": "user@example.com", // 需要 user:email 权限
"avatar_url": "https://..."
}
映射后:
{
provider: "github",
providerUserId: "123456",
username: "username",
email: "user@example.com",
avatar: "https://..."
}
六、现有登录页集成
在 /pages/auth/login.vue 增加:
<!-- 现有登录表单 -->
<template>
<div class="login-form">
<UForm ...> ... </UForm>
<!-- 新增:第三方登录分割线 -->
<div class="divider">
<span>或</span>
</div>
<!-- 新增:OAuth 登录按钮 -->
<button @click="loginWithGithub" class="btn-github">
<Icon name="github" /> GitHub 登录
</button>
</div>
</template>
<script setup>
async function loginWithGithub() {
// 跳转到后端授权接口
window.location.href = '/api/auth/oauth/github/authorize';
}
</script>
七、模块化扩展(添加 Google Provider)
只需在配置文件中添加:
// packages/oauth/src/providers/index.ts
export const oauthProviders = {
github: new GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
google: new GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
scopes: ['openid', 'email', 'profile'],
}),
};
零代码新增 Provider — 只需配置。
八、错误处理
| 错误码 | 说明 | 处理方式 |
|---|---|---|
OAUTH_PROVIDER_NOT_FOUND |
不支持的 Provider | 提示用户 |
OAUTH_STATE_INVALID |
state 验证失败 | CSRF 攻击风险,清除跳转 |
OAUTH_STATE_EXPIRED |
state 过期(>5min) | 提示重新授权 |
OAUTH_TOKEN_EXCHANGE_FAILED |
token 交换失败 | 提示重试 |
OAUTH_USER_INFO_FAILED |
获取用户信息失败 | 提示重试 |
OAUTH_ALREADY_BIND |
账号已绑定其他用户 | 提示解绑后重试 |
OAUTH_BINDING_USER_MISMATCH |
绑定用户不匹配 | 重新登录授权 |
九、配置项
// .env.example
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
// OAuth 配置
OAUTH_STATE_TTL_MS=300000 # 5 minutes
OAUTH_CALLBACK_URL=/api/auth/oauth # 回调基础路径
十、文件结构
packages/oauth/
├── src/
│ ├── index.ts # 模块入口
│ ├── core/
│ │ ├── oauth-manager.ts # 核心管理器
│ │ ├── oauth-state-store.ts # State 存储
│ │ └── oauth-error.ts # 错误类
│ ├── providers/
│ │ ├── base.ts # Provider 接口
│ │ ├── github.ts # GitHub Provider
│ │ ├── google.ts # Google Provider (预留)
│ │ └── index.ts # Provider 导出
│ ├── db/
│ │ ├── schema.ts # oauth_accounts 表定义
│ │ └── queries.ts # 数据库操作
│ └── api/
│ ├── authorize.get.ts # 授权接口
│ ├── callback.get.ts # 回调接口
│ ├── bind.post.ts # 绑定接口
│ ├── unbind.delete.ts # 解绑接口
│ ├── bindings.get.ts # 绑定列表接口
│ └── status.get.ts # 绑定状态接口
├── package.json
└── tsconfig.json
docs/superpowers/
├── specs/
│ └── 2026-05-26-oauth2-design.md # 本设计文档
└── oauth2/
├── README.md # 使用指南
├── PROVIDERS.md # Provider 配置文档
├── FLOW.md # 流程图详解
└── API.md # API 接口文档
十一、后续扩展
- 更多 Provider — Google(国内需翻墙)、微信(需商户资质)
- OAuth 端点管理 — 后台管理 OAuth 配置,支持动态添加
- 登录日志 — 记录 OAuth 登录历史,用于安全审计
- 强制绑定 — 首次登录后强制绑定邮箱或手机号
- OAuth 链接有效期 — 支持在个人中心查看和解绑 OAuth 账号