12 KiB
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 实体 (新增)
// 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 登录响应模型 (修改)
// 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 接口 (扩展)
// 新增方法
Task<RefreshTokenResult> RefreshTokenAsync(string refreshToken, string? ipAddress);
Task RevokeTokenAsync(string refreshToken, string? ipAddress);
Task RevokeAllUserTokensAsync(int userId, string? ipAddress);
1.4 AuthController (新增刷新接口)
// POST /api/refresh
[HttpPost("refresh")]
[AllowAnonymous]
public async Task<ApiResponse<LoginResponse>> RefreshToken([FromBody] RefreshTokenRequest request)
2. 前端组件
2.1 RequestManager (修改)
// honey_box/common/request.js
class RequestManager {
// 新增:刷新状态标记
static isRefreshing = false;
// 新增:等待刷新的请求队列
static refreshQueue = [];
// 新增:刷新 Token 方法
static async refreshToken() { ... }
// 修改:request 方法,处理 401 自动刷新
static async request(param) { ... }
}
Data Models
数据库表结构
-- 新增 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)
);
前端存储结构
// 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
单元测试
-
AuthService 测试
- 登录返回双 Token
- Token 刷新成功
- Token 刷新失败(各种错误场景)
- Token 撤销
-
JwtService 测试
- Access Token 生成和验证
- Token 过期时间正确
属性测试
使用 FsCheck 或类似的属性测试库:
- 登录响应属性测试 - 验证响应结构完整性
- Token 有效期属性测试 - 验证 Token 过期时间
- Token 轮换属性测试 - 验证轮换机制正确性
- Token 哈希属性测试 - 验证存储安全性
集成测试
- 完整登录流程测试
- Token 刷新流程测试
- 并发刷新测试
测试框架
- 后端:xUnit + FsCheck
- 前端:手动测试(UniApp 环境限制)