mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-12-07 13:03:09 +00:00
- Added refresh token support in the account model with new fields: refreshToken, refreshTokenExpiry, and tokenVersion. - Created a new token management utility (utils/tokenManager.js) for generating and verifying access and refresh tokens. - Updated JWT utility (utils/jwt.js) to maintain backward compatibility while introducing new token generation methods. - Enhanced middleware for JWT authentication to support new token types and automatic token refreshing. - Expanded API endpoints in routes/accounts.js to include refresh token functionality, logout options, and token info retrieval. - Introduced automatic token refresh mechanism in the front-end integration examples. - Comprehensive migration checklist and documentation for the new refresh token system. - Added database migration script to accommodate new fields in the Account table.
11 KiB
11 KiB
Refresh Token系统API文档
概述
ClassworksKV现在支持标准的Refresh Token认证系统,提供更安全的用户认证机制。新系统包含:
- Access Token: 短期令牌(默认15分钟),用于API访问
- Refresh Token: 长期令牌(默认7天),用于刷新Access Token
- Token版本控制: 支持令牌失效和安全登出
- 向后兼容: 支持旧版JWT令牌
配置选项
可以通过环境变量配置token系统:
# Access Token配置
ACCESS_TOKEN_EXPIRES_IN=15m # Access Token过期时间
REFRESH_TOKEN_EXPIRES_IN=7d # Refresh Token过期时间
# 密钥配置(HS256算法)
JWT_SECRET=your-access-token-secret
REFRESH_TOKEN_SECRET=your-refresh-token-secret
# RSA密钥配置(RS256算法,可选)
JWT_ALG=RS256
ACCESS_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
ACCESS_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..."
REFRESH_TOKEN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
REFRESH_TOKEN_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n..."
API端点
1. OAuth登录回调
OAuth登录成功后,系统会返回令牌对。
回调URL参数(新版):
https://your-frontend.com/?access_token=eyJ...&refresh_token=eyJ...&expires_in=15m&success=true&provider=github
旧版兼容参数:
https://your-frontend.com/?token=eyJ...&success=true&provider=github
2. 刷新访问令牌
当Access Token即将过期或已过期时,使用Refresh Token获取新的Access Token。
端点: POST /api/accounts/refresh
请求体:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
响应(成功):
{
"success": true,
"message": "令牌刷新成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": "15m",
"account": {
"id": "clxxxx",
"provider": "github",
"email": "user@example.com",
"name": "User Name",
"avatarUrl": "https://..."
}
}
}
错误响应:
{
"success": false,
"message": "刷新令牌已过期"
}
错误状态码:
400: 缺少刷新令牌401: 无效的刷新令牌、令牌已过期、账户不存在、令牌版本不匹配
3. 登出(当前设备)
撤销当前设备的Refresh Token,但不影响其他设备。
端点: POST /api/accounts/logout
请求头:
Authorization: Bearer <access_token>
响应:
{
"success": true,
"message": "登出成功"
}
4. 登出所有设备
撤销账户的所有令牌,强制所有设备重新登录。
端点: POST /api/accounts/logout-all
请求头:
Authorization: Bearer <access_token>
响应:
{
"success": true,
"message": "已从所有设备登出"
}
5. 获取令牌信息
查看当前令牌的详细信息和状态。
端点: GET /api/accounts/token-info
请求头:
Authorization: Bearer <access_token>
响应:
{
"success": true,
"data": {
"accountId": "clxxxx",
"tokenType": "access",
"tokenVersion": 1,
"issuedAt": "2024-11-01T08:00:00.000Z",
"expiresAt": "2024-11-01T08:15:00.000Z",
"expiresIn": 900,
"isExpired": false,
"isLegacyToken": false,
"hasRefreshToken": true,
"refreshTokenExpiry": "2024-11-08T08:00:00.000Z"
}
}
自动刷新机制
响应头刷新
当Access Token剩余有效期少于5分钟时,系统会在响应头中提供新的Access Token:
响应头:
X-New-Access-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Token-Refreshed: true
前端应检查这些响应头并更新本地存储的token。
前端集成示例
JavaScript/TypeScript
class TokenManager {
constructor() {
this.accessToken = localStorage.getItem('access_token');
this.refreshToken = localStorage.getItem('refresh_token');
}
// 设置令牌对
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}
// 清除令牌
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
// 刷新访问令牌
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await fetch('/api/accounts/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
refresh_token: this.refreshToken
}),
});
const data = await response.json();
if (data.success) {
this.accessToken = data.data.access_token;
localStorage.setItem('access_token', this.accessToken);
return this.accessToken;
} else {
throw new Error(data.message);
}
} catch (error) {
this.clearTokens();
throw error;
}
}
// API请求拦截器
async request(url, options = {}) {
const headers = {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
...options.headers,
};
const response = await fetch(url, {
...options,
headers,
});
// 检查是否有新的访问令牌
const newAccessToken = response.headers.get('X-New-Access-Token');
if (newAccessToken) {
this.accessToken = newAccessToken;
localStorage.setItem('access_token', newAccessToken);
}
// 如果token过期,尝试刷新
if (response.status === 401) {
try {
await this.refreshAccessToken();
// 重试请求
return this.request(url, options);
} catch (refreshError) {
// 刷新失败,重定向到登录页
window.location.href = '/login';
throw refreshError;
}
}
return response;
}
// 登出
async logout() {
try {
await this.request('/api/accounts/logout', {
method: 'POST',
});
} catch (error) {
console.error('Logout error:', error);
} finally {
this.clearTokens();
window.location.href = '/login';
}
}
// 登出所有设备
async logoutAll() {
try {
await this.request('/api/accounts/logout-all', {
method: 'POST',
});
} catch (error) {
console.error('Logout all error:', error);
} finally {
this.clearTokens();
window.location.href = '/login';
}
}
}
// 使用示例
const tokenManager = new TokenManager();
// OAuth回调处理
function handleOAuthCallback() {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
if (accessToken && refreshToken) {
tokenManager.setTokens(accessToken, refreshToken);
// 重定向到应用主页
window.location.href = '/dashboard';
} else {
// 处理旧版回调
const legacyToken = params.get('token');
if (legacyToken) {
tokenManager.setTokens(legacyToken, null);
window.location.href = '/dashboard';
}
}
}
// API调用示例
async function getUserProfile() {
try {
const response = await tokenManager.request('/api/accounts/profile');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to get user profile:', error);
throw error;
}
}
React Hook
import { useState, useCallback, useEffect } from 'react';
export function useAuth() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const tokenManager = new TokenManager();
const checkAuth = useCallback(async () => {
if (!tokenManager.accessToken) {
setIsAuthenticated(false);
setLoading(false);
return;
}
try {
const response = await tokenManager.request('/api/accounts/profile');
const data = await response.json();
if (data.success) {
setUser(data.data);
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
} catch (error) {
setIsAuthenticated(false);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
checkAuth();
}, [checkAuth]);
const login = useCallback((accessToken, refreshToken) => {
tokenManager.setTokens(accessToken, refreshToken);
setIsAuthenticated(true);
checkAuth();
}, [checkAuth]);
const logout = useCallback(async () => {
await tokenManager.logout();
setIsAuthenticated(false);
setUser(null);
}, []);
return {
isAuthenticated,
user,
loading,
login,
logout,
tokenManager,
};
}
安全考虑
1. Token存储
- Access Token: 可以存储在内存或localStorage中
- Refresh Token: 建议存储在httpOnly cookie中(需要后端支持),或者安全的本地存储
2. HTTPS
- 生产环境必须使用HTTPS传输令牌
3. Token轮换
- 系统支持令牌版本控制,可以快速失效所有令牌
4. 过期时间
- Access Token短期有效(15分钟)
- Refresh Token长期有效(7天)
- 可根据安全需求调整
迁移指南
从旧版JWT系统迁移
-
前端更新:
- 更新OAuth回调处理逻辑
- 实现Token刷新机制
- 处理新的响应头
-
向后兼容:
- 旧版JWT token仍然有效
- 系统会在响应中标记
isLegacyToken: true - 建议用户重新登录获取新令牌
-
数据库迁移:
# 运行Prisma迁移 npm run prisma migrate dev --name add_refresh_token_support
错误处理
常见错误
| 错误代码 | 错误信息 | 处理方式 |
|---|---|---|
| 401 | JWT token已过期 | 使用refresh token刷新 |
| 401 | 无效的刷新令牌 | 重新登录 |
| 401 | 令牌版本不匹配 | 重新登录 |
| 401 | 账户不存在 | 重新登录 |
错误处理流程
graph TD
A[API请求] --> B{Token有效?}
B -->|是| C[返回数据]
B -->|否,401错误| D{有Refresh Token?}
D -->|否| E[重定向登录]
D -->|是| F[尝试刷新Token]
F --> G{刷新成功?}
G -->|是| H[重试原请求]
G -->|否| E
H --> C
监控和日志
建议监控指标
- Token刷新频率
- Token刷新失败率
- 用户登出频率
- 异常登录尝试
日志记录
系统会记录以下事件:
- Token生成
- Token刷新
- Token撤销
- 认证失败
性能优化
1. Token缓存
- 在内存中缓存已验证的token(适用于高并发场景)
2. 数据库优化
- 为
refreshToken字段添加索引 - 定期清理过期的refresh token
3. 前端优化
- 实现Token预刷新机制
- 使用Web Workers处理Token逻辑