Access Token 与 Refresh Token

只签发一个长期有效的 Access Token 看似简单,但被盗后攻击者也能长期使用。常见设计是把登录凭证拆成短期 Access Token 和长期 Refresh Token。

1. 职责对比

项目 Access Token Refresh Token
用途 调用业务接口 换取新 Token
发送频率 几乎每个受保护请求 Access Token 过期或临近过期时
有效期 较短,如 15 分钟 较长,如 7 天
示例形态 JWT 高熵随机字符串或 JWT
服务端状态 可以无状态验证 推荐关联服务端会话
泄露影响 短期冒用 可持续获得新 Access Token,风险更高

这些时间只是教学值,实际项目要根据风险、用户体验和撤销能力决定。

2. 为什么需要两种 Token

Access Token 短期有效
  -> 泄露窗口较短

Refresh Token 长期有效且可撤销
  -> 用户不必每 15 分钟重新输入密码
  -> 服务端可通过会话记录终止登录

jsonwebtoken 只负责 JWT 的底层签发与验证,不会替你设计刷新、轮换、撤销和多设备会话。官方 README 也提醒自动刷新可能引入漏洞,因此刷新流程必须由业务系统谨慎设计。

3. 本教程的选择

本教程采用:

生成 Refresh Token:

import { randomBytes } from "node:crypto";

function createRefreshToken(): string {
  return randomBytes(32).toString("base64url");
}

生成摘要:

import { createHash } from "node:crypto";

function hashRefreshToken(token: string): string {
  return createHash("sha256").update(token).digest("hex");
}

为什么 Refresh Token 不使用 Argon2?

如果 Refresh Token 不是高熵随机值,这个结论就不成立。

4. 刷新流程

客户端提交 Refresh Token
  -> 服务端计算 SHA-256
  -> 查找会话
  -> 检查会话未撤销、未过期
  -> 生成新的 Access Token
  -> 生成新的 Refresh Token
  -> 更新会话中的摘要
  -> 返回新凭证

5. Refresh Token Rotation

轮换意味着每次刷新后旧 Refresh Token 立即失效:

R1 --刷新--> A2 + R2
R1 再次使用 -> 拒绝
R2 --刷新--> A3 + R3

它能缩短旧 Token 被盗后的可用时间。如果系统要检测重放,可进一步保存 Token 家族关系:旧 Token 再次出现时,撤销整个家族并要求重新登录。

本教程实现基础轮换,但不展开完整 Token Family 数据结构。

6. 多设备登录

每次登录创建独立会话:

用户 123
  |- 手机会话 session-a
  |- 公司电脑 session-b
  `- 家庭电脑 session-c

因此不应只在用户表保存一个 Refresh Token,否则新设备登录可能覆盖旧设备,也难以分别退出。

7. 浏览器如何保存 Token

没有适用于所有项目的唯一答案。常见折中是:

需要同时理解:

教程为了突出后端流程,示例通过 JSON 请求体传 Refresh Token。生产浏览器应用通常应改成安全 Cookie,并配套 CSRF 和 CORS 策略。

8. Access Token 过期后的状态码

业务接口收到过期 Access Token:

HTTP/1.1 401 Unauthorized

客户端可以尝试调用刷新接口。如果 Refresh Token 也无效或会话已撤销,则要求用户重新登录。

9. 退出登录的真实含义

只删除客户端 Token 不足以完成可靠退出:被复制的 Refresh Token 仍可能存在。

完整退出应包括:

  1. 服务端撤销对应会话。
  2. 客户端删除 Access Token。
  3. 客户端删除 Refresh Token 或清除 Cookie。

已签发的 Access Token 通常会继续有效到 exp,这也是其有效期应较短的原因。