316 lines
12 KiB
Markdown
316 lines
12 KiB
Markdown
# Design Document: Refresh Token 机制
|
||
|
||
## Overview
|
||
|
||
为 HoneyBox 前端 API 实现双 Token 认证机制,包括:
|
||
- 后端:修改登录响应、新增刷新接口、新增数据库表
|
||
- 前端:实现自动刷新逻辑、请求队列管理
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Token 流程图 │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────┐ 登录请求 ┌──────────────┐ │
|
||
│ │ 前端 │ ──────────────> │ AuthController │ │
|
||
│ │ UniApp │ │ /api/login │ │
|
||
│ └──────────┘ └────────┬────────┘ │
|
||
│ │ │ │
|
||
│ │ 返回 accessToken │ 生成双 Token │
|
||
│ │ + refreshToken ▼ │
|
||
│ │ <────────────────── ┌──────────────────┐ │
|
||
│ │ │ AuthService │ │
|
||
│ │ │ - GenerateTokens│ │
|
||
│ │ │ - SaveRefreshToken │
|
||
│ │ └────────┬─────────┘ │
|
||
│ │ │ │
|
||
│ │ ▼ │
|
||
│ │ ┌──────────────────┐ │
|
||
│ │ │ user_refresh_tokens │ │
|
||
│ │ │ (Database) │ │
|
||
│ │ └──────────────────┘ │
|
||
│ │ │
|
||
│ │ API 请求 (携带 accessToken) │
|
||
│ │ ──────────────────────────────────────────> │
|
||
│ │ │
|
||
│ │ 401 Unauthorized (Token 过期) │
|
||
│ │ <────────────────────────────────────────── │
|
||
│ │ │
|
||
│ │ 刷新请求 (携带 refreshToken) │
|
||
│ │ ──────────────────> ┌──────────────────┐ │
|
||
│ │ │ /api/refresh │ │
|
||
│ │ └────────┬─────────┘ │
|
||
│ │ │ │
|
||
│ │ 返回新的双 Token │ Token 轮换 │
|
||
│ │ <────────────────────────────┘ │
|
||
│ │ │
|
||
│ │ 重试原始请求 (新 accessToken) │
|
||
│ │ ──────────────────────────────────────────> │
|
||
│ │ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Components and Interfaces
|
||
|
||
### 1. 后端组件
|
||
|
||
#### 1.1 UserRefreshToken 实体 (新增)
|
||
|
||
```csharp
|
||
// server/HoneyBox/src/HoneyBox.Model/Entities/UserRefreshToken.cs
|
||
[Table("user_refresh_tokens")]
|
||
public class UserRefreshToken
|
||
{
|
||
[Key]
|
||
public long Id { get; set; }
|
||
|
||
public int UserId { get; set; }
|
||
|
||
[Required]
|
||
[MaxLength(256)]
|
||
public string TokenHash { get; set; } = null!;
|
||
|
||
public DateTime ExpiresAt { get; set; }
|
||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||
|
||
[MaxLength(50)]
|
||
public string? CreatedByIp { get; set; }
|
||
|
||
public DateTime? RevokedAt { get; set; }
|
||
|
||
[MaxLength(50)]
|
||
public string? RevokedByIp { get; set; }
|
||
|
||
[MaxLength(256)]
|
||
public string? ReplacedByToken { get; set; }
|
||
|
||
// 计算属性
|
||
[NotMapped]
|
||
public bool IsExpired => DateTime.Now >= ExpiresAt;
|
||
|
||
[NotMapped]
|
||
public bool IsRevoked => RevokedAt != null;
|
||
|
||
[NotMapped]
|
||
public bool IsActive => !IsRevoked && !IsExpired;
|
||
|
||
// 导航属性
|
||
[ForeignKey("UserId")]
|
||
public virtual User User { get; set; } = null!;
|
||
}
|
||
```
|
||
|
||
#### 1.2 登录响应模型 (修改)
|
||
|
||
```csharp
|
||
// server/HoneyBox/src/HoneyBox.Model/Models/Auth/LoginResponse.cs
|
||
public class LoginResponse
|
||
{
|
||
/// <summary>
|
||
/// Access Token (JWT),有效期 30 分钟
|
||
/// </summary>
|
||
public string AccessToken { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// Refresh Token,有效期 7 天
|
||
/// </summary>
|
||
public string RefreshToken { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// Access Token 过期时间(秒)
|
||
/// </summary>
|
||
public long ExpiresIn { get; set; }
|
||
}
|
||
```
|
||
|
||
#### 1.3 IAuthService 接口 (扩展)
|
||
|
||
```csharp
|
||
// 新增方法
|
||
Task<RefreshTokenResult> RefreshTokenAsync(string refreshToken, string? ipAddress);
|
||
Task RevokeTokenAsync(string refreshToken, string? ipAddress);
|
||
Task RevokeAllUserTokensAsync(int userId, string? ipAddress);
|
||
```
|
||
|
||
#### 1.4 AuthController (新增刷新接口)
|
||
|
||
```csharp
|
||
// POST /api/refresh
|
||
[HttpPost("refresh")]
|
||
[AllowAnonymous]
|
||
public async Task<ApiResponse<LoginResponse>> RefreshToken([FromBody] RefreshTokenRequest request)
|
||
```
|
||
|
||
### 2. 前端组件
|
||
|
||
#### 2.1 RequestManager (修改)
|
||
|
||
```javascript
|
||
// honey_box/common/request.js
|
||
class RequestManager {
|
||
// 新增:刷新状态标记
|
||
static isRefreshing = false;
|
||
|
||
// 新增:等待刷新的请求队列
|
||
static refreshQueue = [];
|
||
|
||
// 新增:刷新 Token 方法
|
||
static async refreshToken() { ... }
|
||
|
||
// 修改:request 方法,处理 401 自动刷新
|
||
static async request(param) { ... }
|
||
}
|
||
```
|
||
|
||
## Data Models
|
||
|
||
### 数据库表结构
|
||
|
||
```sql
|
||
-- 新增 user_refresh_tokens 表
|
||
CREATE TABLE user_refresh_tokens (
|
||
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
|
||
UserId INT NOT NULL,
|
||
TokenHash NVARCHAR(256) NOT NULL,
|
||
ExpiresAt DATETIME2 NOT NULL,
|
||
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||
CreatedByIp NVARCHAR(50) NULL,
|
||
RevokedAt DATETIME2 NULL,
|
||
RevokedByIp NVARCHAR(50) NULL,
|
||
ReplacedByToken NVARCHAR(256) NULL,
|
||
|
||
CONSTRAINT FK_UserRefreshTokens_Users
|
||
FOREIGN KEY (UserId) REFERENCES users(id) ON DELETE CASCADE,
|
||
|
||
INDEX IX_UserRefreshTokens_UserId (UserId),
|
||
INDEX IX_UserRefreshTokens_TokenHash (TokenHash)
|
||
);
|
||
```
|
||
|
||
### 前端存储结构
|
||
|
||
```javascript
|
||
// uni.storage 存储
|
||
{
|
||
"accessToken": "eyJhbGciOiJIUzI1NiIs...", // JWT Access Token
|
||
"refreshToken": "a1b2c3d4e5f6...", // Refresh Token
|
||
"tokenExpireTime": 1706000000000 // Access Token 过期时间戳
|
||
}
|
||
```
|
||
|
||
## Correctness Properties
|
||
|
||
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
||
### Property 1: 登录响应结构完整性
|
||
|
||
*For any* 成功的登录请求(微信或手机号),返回的响应对象 SHALL 包含非空的 accessToken、非空的 refreshToken 和正整数的 expiresIn。
|
||
|
||
**Validates: Requirements 1.1, 1.2**
|
||
|
||
### Property 2: Access Token 有效期正确性
|
||
|
||
*For any* 生成的 Access Token,解析其 JWT payload 中的 exp claim,该值与当前时间的差值 SHALL 在 29-31 分钟范围内(允许 1 分钟误差)。
|
||
|
||
**Validates: Requirements 1.3**
|
||
|
||
### Property 3: Refresh Token 存储正确性
|
||
|
||
*For any* 成功的登录请求,数据库中 SHALL 存在对应的 UserRefreshToken 记录,且 ExpiresAt 与当前时间的差值在 6.9-7.1 天范围内。
|
||
|
||
**Validates: Requirements 1.4, 1.5**
|
||
|
||
### Property 4: Token 刷新有效性
|
||
|
||
*For any* 有效的 Refresh Token,调用刷新接口 SHALL 返回新的有效 accessToken 和 refreshToken,且新旧 Token 不相同。
|
||
|
||
**Validates: Requirements 2.1**
|
||
|
||
### Property 5: 无效 Token 拒绝
|
||
|
||
*For any* 随机生成的无效字符串作为 Refresh Token,调用刷新接口 SHALL 返回错误响应。
|
||
|
||
**Validates: Requirements 2.4**
|
||
|
||
### Property 6: Token 轮换完整性
|
||
|
||
*For any* 成功的 Token 刷新操作,旧的 Refresh Token SHALL 被标记为已撤销(RevokedAt 非空),且其 ReplacedByToken 字段 SHALL 指向新 Token 的哈希值。
|
||
|
||
**Validates: Requirements 2.5, 2.6**
|
||
|
||
### Property 7: Token 哈希存储安全性
|
||
|
||
*For any* 存储在数据库中的 TokenHash,其值 SHALL 不等于原始 Refresh Token 明文,且长度为 64 字符(SHA256 十六进制)。
|
||
|
||
**Validates: Requirements 4.1**
|
||
|
||
### Property 8: Token 有效性检查完整性
|
||
|
||
*For any* 已过期或已撤销的 Refresh Token,调用刷新接口 SHALL 返回错误响应,不返回新 Token。
|
||
|
||
**Validates: Requirements 5.3**
|
||
|
||
### Property 9: UserAccount 兼容性维护
|
||
|
||
*For any* 成功的登录请求,UserAccount 表中对应用户的 account_token 字段 SHALL 被更新。
|
||
|
||
**Validates: Requirements 6.3**
|
||
|
||
## Error Handling
|
||
|
||
### 后端错误处理
|
||
|
||
| 错误场景 | HTTP 状态码 | 错误消息 |
|
||
|---------|------------|---------|
|
||
| Refresh Token 为空 | 400 | 刷新令牌不能为空 |
|
||
| Refresh Token 无效 | 401 | 无效的刷新令牌 |
|
||
| Refresh Token 已过期 | 401 | 刷新令牌已过期 |
|
||
| Refresh Token 已撤销 | 401 | 刷新令牌已失效 |
|
||
| 用户不存在 | 401 | 用户不存在 |
|
||
| 用户已禁用 | 401 | 账号已被禁用 |
|
||
|
||
### 前端错误处理
|
||
|
||
| 错误场景 | 处理方式 |
|
||
|---------|---------|
|
||
| 刷新成功 | 更新本地 Token,重试原请求 |
|
||
| 刷新失败 (401) | 清除本地存储,跳转登录页 |
|
||
| 刷新失败 (网络错误) | 显示网络错误提示 |
|
||
| 并发刷新 | 加入队列等待 |
|
||
|
||
## Testing Strategy
|
||
|
||
### 单元测试
|
||
|
||
1. **AuthService 测试**
|
||
- 登录返回双 Token
|
||
- Token 刷新成功
|
||
- Token 刷新失败(各种错误场景)
|
||
- Token 撤销
|
||
|
||
2. **JwtService 测试**
|
||
- Access Token 生成和验证
|
||
- Token 过期时间正确
|
||
|
||
### 属性测试
|
||
|
||
使用 FsCheck 或类似的属性测试库:
|
||
|
||
1. **登录响应属性测试** - 验证响应结构完整性
|
||
2. **Token 有效期属性测试** - 验证 Token 过期时间
|
||
3. **Token 轮换属性测试** - 验证轮换机制正确性
|
||
4. **Token 哈希属性测试** - 验证存储安全性
|
||
|
||
### 集成测试
|
||
|
||
1. **完整登录流程测试**
|
||
2. **Token 刷新流程测试**
|
||
3. **并发刷新测试**
|
||
|
||
### 测试框架
|
||
|
||
- 后端:xUnit + FsCheck
|
||
- 前端:手动测试(UniApp 环境限制)
|