这是一种用于保证系统安全性和提升用户体验的现代认证方案。


🔑 核心概念: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 ) 中携带 Access Token。
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) 如何使用

  1. 登录后:拿到 AT 和 RT 后,立即将它们存储起来。

    • AT:用于 API 调用。
    • RT:通常存储在 Local Storage 或更安全的 HTTP-only Cookie 中。
  2. API 请求:所有受保护的请求(如 /api/user/profile)都必须在请求头中加入 Authorization: Bearer <AT>

  3. Token 过期处理

    • 当 API 返回 403/401 错误,且错误信息显示 AT 过期时。
    • 客户端自动向 /api/auth/refresh-token 发送存储的 RT。
    • 如果续签成功,用 NEW AT 和 NEW RT 替换旧的,并重新发送原始请求。

B. 服务器端 如何使用

routes/auth.js 中:

  1. Redis 存储 (持久化):使用 saveRefreshToken(user.id, refreshToken) 将 RT 存储在 Redis 中,这是实现撤销的基础。

  2. /login 路由

    const accessToken = generateAccessToken(user); // 短效
    const refreshToken = generateRefreshToken(user); // 长效
    await saveRefreshToken(user.id, refreshToken); 
    // 返回 { accessToken, refreshToken, ... }
  1. /refresh-token 路由 (验证和轮换)
   const storedToken = await getRefreshToken(userId);
   if (!storedToken || storedToken !== refreshToken) { 
       // ❌ 如果不匹配,说明是无效或已撤销的 Token
       return res.status(403); 
   }
   // ... 生成 NEW AT/RT
   await saveRefreshToken(userId, newRefreshToken); // 覆盖旧的 RT
  1. /logout 路由 (撤销)
    await deleteRefreshToken(req.user.userId); // 从 Redis 中删除 RT
    // 客户端下次尝试续签就会失败,实现真正的登出