1
1
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-12-07 13:03:09 +00:00
ClassworksKV/REFRESH_TOKEN_API.md
SunWuyuan 2ab90ffebc
feat: Implement Refresh Token system with enhanced security and user experience
- 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.
2025-11-02 09:48:03 +08:00

489 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Refresh Token系统API文档
## 概述
ClassworksKV现在支持标准的Refresh Token认证系统提供更安全的用户认证机制。新系统包含
- **Access Token**: 短期令牌默认15分钟用于API访问
- **Refresh Token**: 长期令牌默认7天用于刷新Access Token
- **Token版本控制**: 支持令牌失效和安全登出
- **向后兼容**: 支持旧版JWT令牌
## 配置选项
可以通过环境变量配置token系统
```bash
# 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`
**请求体:**
```json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**响应(成功):**
```json
{
"success": true,
"message": "令牌刷新成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": "15m",
"account": {
"id": "clxxxx",
"provider": "github",
"email": "user@example.com",
"name": "User Name",
"avatarUrl": "https://..."
}
}
}
```
**错误响应:**
```json
{
"success": false,
"message": "刷新令牌已过期"
}
```
**错误状态码:**
- `400`: 缺少刷新令牌
- `401`: 无效的刷新令牌、令牌已过期、账户不存在、令牌版本不匹配
### 3. 登出(当前设备)
撤销当前设备的Refresh Token但不影响其他设备。
**端点:** `POST /api/accounts/logout`
**请求头:**
```
Authorization: Bearer <access_token>
```
**响应:**
```json
{
"success": true,
"message": "登出成功"
}
```
### 4. 登出所有设备
撤销账户的所有令牌,强制所有设备重新登录。
**端点:** `POST /api/accounts/logout-all`
**请求头:**
```
Authorization: Bearer <access_token>
```
**响应:**
```json
{
"success": true,
"message": "已从所有设备登出"
}
```
### 5. 获取令牌信息
查看当前令牌的详细信息和状态。
**端点:** `GET /api/accounts/token-info`
**请求头:**
```
Authorization: Bearer <access_token>
```
**响应:**
```json
{
"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
```javascript
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
```jsx
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系统迁移
1. **前端更新**
- 更新OAuth回调处理逻辑
- 实现Token刷新机制
- 处理新的响应头
2. **向后兼容**
- 旧版JWT token仍然有效
- 系统会在响应中标记`isLegacyToken: true`
- 建议用户重新登录获取新令牌
3. **数据库迁移**
```bash
# 运行Prisma迁移
npm run prisma migrate dev --name add_refresh_token_support
```
## 错误处理
### 常见错误
| 错误代码 | 错误信息 | 处理方式 |
|---------|---------|---------|
| 401 | JWT token已过期 | 使用refresh token刷新 |
| 401 | 无效的刷新令牌 | 重新登录 |
| 401 | 令牌版本不匹配 | 重新登录 |
| 401 | 账户不存在 | 重新登录 |
### 错误处理流程
```mermaid
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逻辑