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.
489 lines
11 KiB
Markdown
489 lines
11 KiB
Markdown
# 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逻辑 |