这是一种用于保证系统安全性和提升用户体验的现代认证方案。
🔑 核心概念:Access Token 与 Refresh Token
传统的会话(Session)机制依赖服务器状态,而 Token 机制是无状态的。为了平衡安全性和用户体验,我们引入了两种 Token:
| 特性 | Access Token (存取令牌) | Refresh Token (续签令牌) |
|---|---|---|
| 用途 | 访问受保护的 API 资源(如获取用户资料、创建文章)。 | 换取新的 Access Token,避免用户重复登录。 |
| 有效期限 | 短效 (通常 15 分钟到 1 小时)。 | 长效 (通常 7 天到 30 天)。 |
| 存储位置 | 客户端应安全存储(如内存、HTTP-only Cookie)。 | 客户端应安全存储,并存储在 Redis 或数据库 中。 |
| 安全性 | 泄露损失小,因为很快失效。 | 泄露损失大,必须严格保护,可用于撤销。 |
🛠️ 两种 Token 的工作流程
整个认证流程分为三个主要步骤:登录、资源访问和续签。
1. 登录 (POST /api/auth/login)
这是用户获取两种 Token 的起点:
| 步骤 | 角色 | 描述 |
|---|---|---|
| 1. | 用户/客户端 | 发送用户名和密码到 /api/auth/login。 |
| 2. | 服务器 (Auth) | 验证凭证,生成 短效 Access Token (AT) 和 长效 Refresh Token (RT)。 |
| 3. | 服务器 (Auth) | 将 RT 存储到 Redis 中,并以用户 ID 为 Key。 |
| 4. | 服务器 (Auth) | 将 AT 和 RT 一起返回给客户端。 |
| 5. | 客户端 | 安全地存储 AT 和 RT。 |
2. 资源访问 (访问受保护的 API)
这是 AT 的主要用途:
| 步骤 | 角色 | 描述 |
|---|---|---|
| 1. | 客户端 | 在 HTTP Header (Authorization: Bearer |
| 2. | 服务器 (Middleware) | authenticateToken 中间件验证 AT 的签名和是否过期。 |
| 3.a | 成功 | AT 有效,允许访问资源。 |
| 3.b | 失败 | AT 过期或无效(HTTP 403 Forbidden),客户端需要进入续签流程。 |
3. Token 续签 (POST /api/auth/refresh-token)
当 Access Token 过期时,客户端需要使用 Refresh Token 来悄悄地获取新的 Access Token,避免用户被强制登出。
| 步骤 | 角色 | 描述 |
|---|---|---|
| 1. | 客户端 | 发送 Refresh Token (RT) 到 /api/auth/refresh-token。 |
| 2. | 服务器 (Auth) | 验证 RT 的签名和过期时间。 |
| 3. | 服务器 (Auth) | 🔑 关键检查:查询 Redis,确认该 RT 是否与存储的 Token 匹配,且没有被撤销。 |
| 4. | 服务器 (Auth) | 如果有效,生成 新的 Access Token (NEW AT) 和 新的 Refresh Token (NEW RT)。 |
| 5. | 服务器 (Auth) | 替换 Redis 中的 RT(使用 NEW RT 覆盖旧的 RT)。 |
| 6. | 服务器 (Auth) | 返回 NEW AT 和 NEW RT 给客户端。 |
| 7. | 客户端 | 用新的 AT 和 RT 替换本地存储的旧 Token。 |
💻 如何在项目中使用
A. 客户端 (前端/App) 如何使用
登录后:拿到 AT 和 RT 后,立即将它们存储起来。
- AT:用于 API 调用。
- RT:通常存储在 Local Storage 或更安全的 HTTP-only Cookie 中。
API 请求:所有受保护的请求(如
/api/user/profile)都必须在请求头中加入Authorization: Bearer <AT>。Token 过期处理:
- 当 API 返回 403/401 错误,且错误信息显示 AT 过期时。
- 客户端自动向
/api/auth/refresh-token发送存储的 RT。 - 如果续签成功,用 NEW AT 和 NEW RT 替换旧的,并重新发送原始请求。
B. 服务器端 如何使用
在 routes/auth.js 中:
Redis 存储 (持久化):使用
saveRefreshToken(user.id, refreshToken)将 RT 存储在 Redis 中,这是实现撤销的基础。/login路由:
const accessToken = generateAccessToken(user); // 短效
const refreshToken = generateRefreshToken(user); // 长效
await saveRefreshToken(user.id, refreshToken);
// 返回 { accessToken, refreshToken, ... }
/refresh-token路由 (验证和轮换):
const storedToken = await getRefreshToken(userId);
if (!storedToken || storedToken !== refreshToken) {
// ❌ 如果不匹配,说明是无效或已撤销的 Token
return res.status(403);
}
// ... 生成 NEW AT/RT
await saveRefreshToken(userId, newRefreshToken); // 覆盖旧的 RT
/logout路由 (撤销):
await deleteRefreshToken(req.user.userId); // 从 Redis 中删除 RT
// 客户端下次尝试续签就会失败,实现真正的登出
