Skip to content

Commit e7902b7

Browse files
HuanCheng65claude
andcommitted
feat(auth): add OAuth authentication system with plugin support
- Add OAuth module with dynamic provider loading mechanism - Implement OAuth service with plugin and npm package loading support - Add OAuth types and base provider class for extensibility - Create user OAuth connection model for account linking - Add OAuth routes: providers list, login redirect, callback handling - Implement user synchronization logic (existing user binding, email matching, new user creation) - Add comprehensive unit tests for OAuth service and user login methods - Add end-to-end tests covering full OAuth flow and error scenarios - Include example OAuth provider implementation and documentation - Update environment configuration with OAuth settings 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f204c30 commit e7902b7

17 files changed

+2663
-2
lines changed

docs/oauth-implementation-guide.md

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# OAuth 功能实现指南
2+
3+
## 概述
4+
5+
本文档描述了 Cheese Backend 中 OAuth 登录功能的实现。该功能允许用户使用第三方 OAuth 提供商(如学校的 OAuth 系统)进行登录。
6+
7+
## 核心组件
8+
9+
### 1. OAuth Service (`src/auth/oauth/oauth.service.ts`)
10+
- 动态加载和管理多个 OAuth 提供商
11+
- 支持插件机制和 npm 包加载
12+
- 提供统一的 OAuth 流程接口
13+
14+
### 2. OAuth 类型定义 (`src/auth/oauth/oauth.types.ts`)
15+
- `OAuthProvider` 接口:定义提供商必须实现的方法
16+
- `BaseOAuthProvider` 抽象类:提供通用实现
17+
- `OAuthUserInfo` 接口:标准化用户信息格式
18+
19+
### 3. 数据库模型 (`src/auth/oauth/oauth.prisma`)
20+
- `UserOAuthConnection` 模型:存储用户与第三方账号的关联关系
21+
22+
### 4. 控制器路由 (`src/users/users.controller.ts`)
23+
- `GET /users/auth/oauth/providers` - 获取可用的 OAuth 提供商列表
24+
- `GET /users/auth/oauth/login/:providerId` - 重定向到 OAuth 提供商授权页面
25+
- `GET /users/auth/oauth/callback/:providerId` - 处理 OAuth 回调
26+
27+
## 环境配置
28+
29+
`.env` 文件中添加以下配置:
30+
31+
```bash
32+
# 启用的 OAuth 提供商(逗号分隔)
33+
OAUTH_ENABLED_PROVIDERS=ruc,google
34+
35+
# OAuth 插件搜索路径
36+
OAUTH_PLUGIN_PATHS=plugins/oauth
37+
38+
# 是否允许从 npm 包加载提供商
39+
OAUTH_ALLOW_NPM_LOADING=false
40+
41+
# 提供商凭据(以 'ruc' 为例)
42+
OAUTH_RUC_CLIENT_ID=your-client-id
43+
OAUTH_RUC_CLIENT_SECRET=your-client-secret
44+
OAUTH_RUC_REDIRECT_URL=http://localhost:3000/users/auth/oauth/callback/ruc
45+
46+
# 前端重定向路径
47+
FRONTEND_OAUTH_SUCCESS_PATH=/oauth-success
48+
FRONTEND_OAUTH_ERROR_PATH=/oauth-error
49+
```
50+
51+
## 实现新的 OAuth 提供商
52+
53+
### 方法一:插件文件
54+
55+
`plugins/oauth/` 目录下创建提供商实现文件:
56+
57+
```javascript
58+
// plugins/oauth/your-provider.js
59+
const axios = require('axios');
60+
61+
class YourOAuthProvider {
62+
constructor(config) {
63+
this.config = {
64+
...config,
65+
authorizationUrl: 'https://your-provider.com/oauth/authorize',
66+
tokenUrl: 'https://your-provider.com/oauth/token',
67+
scope: ['read:user', 'user:email'],
68+
};
69+
}
70+
71+
getConfig() {
72+
return this.config;
73+
}
74+
75+
getAuthorizationUrl(state, accessType) {
76+
const params = new URLSearchParams({
77+
client_id: this.config.clientId,
78+
redirect_uri: this.config.redirectUrl,
79+
scope: this.config.scope.join(' '),
80+
response_type: 'code',
81+
});
82+
83+
if (state) params.append('state', state);
84+
if (accessType) params.append('access_type', accessType);
85+
86+
return `${this.config.authorizationUrl}?${params.toString()}`;
87+
}
88+
89+
async handleCallback(code, state) {
90+
const response = await axios.post(this.config.tokenUrl, {
91+
client_id: this.config.clientId,
92+
client_secret: this.config.clientSecret,
93+
code,
94+
grant_type: 'authorization_code',
95+
redirect_uri: this.config.redirectUrl,
96+
});
97+
98+
return response.data.access_token;
99+
}
100+
101+
async getUserInfo(accessToken) {
102+
const response = await axios.get('https://your-provider.com/api/user', {
103+
headers: { 'Authorization': `Bearer ${accessToken}` },
104+
});
105+
106+
const userData = response.data;
107+
return {
108+
id: userData.id.toString(),
109+
email: userData.email,
110+
name: userData.name,
111+
username: userData.username,
112+
preferredUsername: userData.preferred_username,
113+
};
114+
}
115+
}
116+
117+
function createProvider(config) {
118+
return new YourOAuthProvider(config);
119+
}
120+
121+
module.exports = { createProvider, default: createProvider };
122+
```
123+
124+
### 方法二:TypeScript 实现
125+
126+
```typescript
127+
// plugins/oauth/your-provider.ts
128+
import axios from 'axios';
129+
import { BaseOAuthProvider, OAuthProviderConfig, OAuthUserInfo } from '../../src/auth/oauth/oauth.types';
130+
131+
export class YourOAuthProvider extends BaseOAuthProvider {
132+
constructor(config: OAuthProviderConfig) {
133+
super({
134+
...config,
135+
authorizationUrl: 'https://your-provider.com/oauth/authorize',
136+
tokenUrl: 'https://your-provider.com/oauth/token',
137+
scope: ['read:user', 'user:email'],
138+
});
139+
}
140+
141+
async handleCallback(code: string, state?: string): Promise<string> {
142+
const response = await axios.post(this.config.tokenUrl, {
143+
client_id: this.config.clientId,
144+
client_secret: this.config.clientSecret,
145+
code,
146+
grant_type: 'authorization_code',
147+
redirect_uri: this.config.redirectUrl,
148+
});
149+
150+
return response.data.access_token;
151+
}
152+
153+
async getUserInfo(accessToken: string): Promise<OAuthUserInfo> {
154+
const response = await axios.get('https://your-provider.com/api/user', {
155+
headers: { 'Authorization': `Bearer ${accessToken}` },
156+
});
157+
158+
const userData = response.data;
159+
return {
160+
id: userData.id.toString(),
161+
email: userData.email,
162+
name: userData.name,
163+
username: userData.username,
164+
preferredUsername: userData.preferred_username,
165+
};
166+
}
167+
}
168+
169+
export function createProvider(config: OAuthProviderConfig) {
170+
return new YourOAuthProvider(config);
171+
}
172+
```
173+
174+
## OAuth 流程
175+
176+
1. **用户点击 OAuth 登录**
177+
- 前端调用 `GET /users/auth/oauth/providers` 获取可用提供商
178+
- 前端引导用户访问 `GET /users/auth/oauth/login/:providerId`
179+
180+
2. **重定向到提供商**
181+
- 后端生成授权 URL 并重定向用户到 OAuth 提供商
182+
183+
3. **用户授权并回调**
184+
- 用户在提供商页面完成授权
185+
- 提供商重定向用户到 `GET /users/auth/oauth/callback/:providerId`
186+
187+
4. **处理回调**
188+
- 后端交换 authorization code 获取 access token
189+
- 使用 access token 获取用户信息
190+
- 根据用户信息进行登录或注册
191+
- 生成 JWT token 并重定向到前端
192+
193+
5. **用户登录成功**
194+
- 前端从 URL 参数获取 JWT token
195+
- 从 Cookie 获取 refresh token
196+
- 完成登录状态设置
197+
198+
## 用户同步逻辑
199+
200+
### 1. 检查已有绑定
201+
- 查询 `UserOAuthConnection` 表,查找是否已有该提供商和用户 ID 的绑定
202+
- 如果找到且关联用户未被删除,直接登录
203+
204+
### 2. 按邮箱匹配现有用户
205+
- 如果未找到绑定但 OAuth 提供了邮箱
206+
- 查找本地是否有相同邮箱的活跃用户
207+
- 如果找到,创建新的 OAuth 绑定并登录
208+
209+
### 3. 创建新用户
210+
- 如果既无绑定又无邮箱匹配,创建新用户
211+
- 生成唯一用户名(基于 OAuth 用户信息)
212+
- 创建用户、用户档案和 OAuth 绑定
213+
- 生成随机密码(用户不会使用)
214+
215+
## 安全考虑
216+
217+
1. **State 参数验证**
218+
- OAuth 流程中的 state 参数用于防止 CSRF 攻击
219+
- 建议在生产环境中实现 state 验证
220+
221+
2. **路径遍历防护**
222+
- 插件加载时验证路径安全性
223+
- 只允许加载预期目录下的文件
224+
225+
3. **配置验证**
226+
- 验证提供商 ID 只包含安全字符
227+
- 检查必要配置项的存在
228+
229+
4. **错误处理**
230+
- 统一的错误处理和日志记录
231+
- 不泄露敏感信息给前端
232+
233+
## 扩展功能
234+
235+
### 添加新提供商支持
236+
1. 实现提供商类(参考上面的示例)
237+
2. 将实现文件放在 `plugins/oauth/` 目录
238+
3. 在环境变量中配置提供商凭据
239+
4. 将提供商 ID 添加到 `OAUTH_ENABLED_PROVIDERS`
240+
241+
### 自定义用户信息映射
242+
在提供商实现的 `getUserInfo` 方法中,可以自定义如何将提供商的用户数据映射到标准的 `OAuthUserInfo` 格式。
243+
244+
### 长期令牌管理
245+
可以在 `UserOAuthConnection` 表中存储 OAuth refresh token,用于长期访问第三方 API(如果需要)。
246+
247+
## 前端集成
248+
249+
前端需要实现以下页面:
250+
251+
1. **登录页面**
252+
- 调用 `/users/auth/oauth/providers` 获取提供商列表
253+
- 为每个提供商提供登录按钮
254+
255+
2. **OAuth 成功页面** (`/oauth-success`)
256+
- 从 URL 参数获取 `token``email`
257+
- 设置应用登录状态
258+
259+
3. **OAuth 错误页面** (`/oauth-error`)
260+
- 显示错误信息
261+
- 提供重新登录选项
262+
263+
## 故障排除
264+
265+
### 常见问题
266+
267+
1. **提供商未加载**
268+
- 检查环境变量配置
269+
- 确认插件文件路径和格式正确
270+
- 查看应用启动日志
271+
272+
2. **回调失败**
273+
- 确认回调 URL 配置正确
274+
- 检查提供商的 client credentials
275+
- 查看错误日志了解具体失败原因
276+
277+
3. **用户信息获取失败**
278+
- 检查用户信息 API 的 URL 和权限要求
279+
- 确认 access token 有效且有足够权限
280+
281+
### 调试技巧
282+
283+
1. 启用详细日志记录
284+
2. 使用 OAuth 提供商的开发者工具
285+
3. 检查网络请求和响应
286+
4. 验证 JWT token 的内容和有效性

0 commit comments

Comments
 (0)