1. 引言
在基于 JWT(JSON Web Token)的鉴权体系中,AccessToken 通常设置为短有效期(15分钟~1小时),以降低令牌泄露造成的安全风险。但这也带来一个实际问题:客户端在 AccessToken 过期后,每次都需要用户重新登录获取新令牌,体验较差。本文说明如何通过双 Token 模型——即 AccessToken 与 RefreshToken 的分工协作,来实现无感续期。
阅读本文后,你将理解 RefreshToken 的核心作用与使用边界,掌握在前后端分离场景中配置双 Token 刷新机制的方法,知晓 RefreshToken 的安全存储方案,以及处理过期、并发刷新、轮换策略等常见问题的工程实践。
2. 为什么需要 RefreshToken——双 Token 模型的分工与设计初衷
2.1 单 Token 模式的局限
如果只使用一个 AccessToken 进行鉴权,通常有两种选择:
- 短有效期:Token 15 分钟过期,用户每 15 分钟就需要重新登录,体验极差。
- 长有效期:Token 设置 7 天或更长,一旦泄露,攻击者可在长时间内获得认证权限,安全风险大。
单 Token 模式在安全与体验之间难以平衡。频繁重新登录影响用户留存,而过长的有效期又难以在运维中控制风险。
2.2 双 Token 模型的分工
双 Token 模型引入两个角色:
- AccessToken(访问令牌):短期有效,通常 15 分钟~1 小时。每次 API 请求都携带,用于服务端鉴权。泄露后影响范围小,因为有效期短。
- RefreshToken(刷新令牌):长期有效,通常 7~30 天。仅在特定刷新接口使用,用于获取新的 AccessToken。泄露后攻击者可长期刷新令牌,因此对存储和传输安全性要求更高。
这种分工的核心思路是:将“每次请求都验证”的短期凭证与“仅在刷新时调用”的长期凭证分离,在保证安全的前提下实现无感续期。
2.3 RefreshToken 的使用边界
需要明确的是,RefreshToken 不能作为业务鉴权凭证。它只应在如下场景使用:
- 客户端通过
/auth/refresh接口向服务端发送 RefreshToken。 - 服务端校验 RefreshToken 有效性,签发新的 AccessToken(和可选的新的 RefreshToken)。
- 客户端用新的 AccessToken 发起后续请求。
任何业务接口都不应接受 RefreshToken 替代 AccessToken。
3. RefreshToken 的生成与数据结构设计
3.1 生成方式:随机字符串 vs JWT 格式
RefreshToken 的生成有两种主流方式:
方式一:加密随机字符串(推荐)
使用 crypto.randomBytes(32) 等强随机源生成 32 字节以上的随机字符串。这种方式不可预测,无法被逆向,安全性高。
1 | |
方式二:JWT 格式
将 RefreshToken 也签发为 JWT,内部携带 userId、版本号、过期时间等信息。服务端可以无状态校验,但一旦 JWT 泄露,攻击者可以解析 payload 获取信息。实践中更推荐用随机字符串,由服务端维护状态,便于控制吊销和轮换。
3.2 服务端存储结构
无论哪种方式,服务端通常需要存储 RefreshToken 的关联信息。一个典型的数据库表或 Redis 哈希结构如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | varchar | 用户标识 |
| token_hash | varchar | RefreshToken 的 SHA-256 哈希(或明文) |
| expires_at | datetime | 过期时间 |
| is_used | boolean | 是否已被使用(轮换后标记) |
| created_at | datetime | 创建时间 |
如果使用 Redis,可设置 TTL 自动过期。关键点在于:每次刷新时需要查存储、验证哈希、标记使用、插入新记录。
3.3 设计要点
- 长度 ≥ 32 字节:建议 64 字节以上,降低暴力碰撞概率。
- 使用加密随机源:不要用
Math.random()或time()等可预测源。 - 避免可预测性:禁止使用时间戳+自增序列的简单组合。
4. AccessToken 过期自动刷新机制——完整交互流程
4.1 前端拦截与刷新触发
在前后端分离架构中,前端通常通过 HTTP 拦截器(如 Axios 的 response.interceptors)统一处理 401 状态码:
客户端发起 API 请求,携带 AccessToken。
服务端校验发现 AccessToken 过期,返回 401 Unauthorized。
前端拦截器检测到 401,判断是否由 AccessToken 过期引起(而非权限不足)。
如果是过期,前端暂停所有待发请求,调用
/auth/refresh接口,携带 RefreshToken。服务端校验 RefreshToken 有效后,返回新的 AccessToken(以及新的 RefreshToken)。
前端用新 Token 重试原请求,并更新本地存储。
如果 RefreshToken 也过期或无效,服务端返回 401,前端清除本地 Token,跳转登录页。
4.2 服务端校验与颁发
服务端刷新逻辑的关键步骤:
接收 RefreshToken(放在请求体或 httpOnly Cookie 中)。
从存储中查找该 RefreshToken 的哈希,获取关联的 userId、过期时间、是否已使用。
检查是否过期(当前时间 > expires_at)。
检查是否已被使用(is_used = true),如果是,说明可能发生了重放,建议将关联的所有 RefreshToken 全部失效(攻击者可能已窃取)。
校验通过后,签发新的 AccessToken(短有效期)。
轮换:废弃旧的 RefreshToken(标记 is_used = true 或删除),生成并返回新的 RefreshToken。
如果需要,服务端可绑定指纹信息(如 User-Agent、IP 段)到 RefreshToken 记录,用于下次校验时比对。
4.3 流程图描述
1 | |
5. 实战代码:Spring Security JWT 刷新 Token 配置
以下以 Spring Boot + Spring Security 为例,说明核心实现。假设项目中已集成 JWT 签发与校验。
5.1 存储层(Redis)
1 | |
Redis 的 TTL 天然解决过期问题,且读写速度快。
5.2 刷新服务
1 | |
注意:此处对 RefreshToken 存储的是 SHA-256 哈希,避免明文泄露。delete 后 save 实现轮换。
5.3 安全配置
在 SecurityConfig 中,将 /auth/refresh 路径加入白名单,无需 AccessToken 校验:
1 | |
同时应配置 CSRF 防护,如果 RefreshToken 通过 POST 请求体发送,需要确保跨站请求伪造被限制。
6. 实战代码:Node.js(Express + jsonwebtoken)刷新接口实现
6.1 签发与存储
1 | |
6.2 中间件
1 | |
前端拦截器根据 code: 'TOKEN_EXPIRED' 触发刷新流程。
7. RefreshToken 存储安全方案(前端 + 后端)
7.1 前端存储方案对比
| 存储方式 | 风险 | 适用场景 |
|---|---|---|
| localStorage | 易受 XSS 攻击,攻击者可读取 | 不推荐存储 RefreshToken |
| sessionStorage | 同样受 XSS 影响,但浏览器关闭即清除 | 可配合定时刷新使用,但仍有风险 |
| httpOnly + Secure + SameSite=Strict Cookie | 无法通过 JavaScript 读取,防 XSS 效果好 | 推荐,尤其适合 API 与前端同源 |
| 内存变量(闭包)+ 定时刷新 | 无持久存储,会话结束后丢失 | 高安全需求场景,但用户需频繁登录 |
建议:如果前后端同域,使用 httpOnly Cookie 存储 RefreshToken,前端无需关心其存取。如果跨域,可使用 SameSite=None; Secure 的 Cookie(需 HTTPS),或使用框架提供的封装方案。
7.2 后端存储安全
- Redis 存储哈希:不存明文,即使 Redis 被入侵也无法直接使用 RefreshToken。
- 定期清理过期记录:Redis TTL 自动处理;数据库可设置定时任务删除过期行。
- 哈希加盐:对 RefreshToken 计算 hash 时拼接服务端固定盐值(如应用密钥),避免彩虹表攻击。
7.3 明文 vs 哈希
明文存储:校验时直接比对字符串,性能好。但要求 Redis/数据库访问必须严格 ACL 控制,且日志中不应记录明文 Token。
哈希存储:增加一层安全垫,即使存储泄露也无法直接使用 Token。但需要每次计算哈希后再比对,性能差异可忽略。推荐使用哈希存储。
7.4 轮换策略
每次刷新后,旧 RefreshToken 必须立即失效(删除或标记为已使用)。这是防止重放攻击的关键措施。如果攻击者同时窃取了旧 RefreshToken 和新的 AccessToken,但新的 RefreshToken 已经轮换,旧 Token 失效则攻击者无法二次刷新。
重要:如果服务端发现一个 RefreshToken 被使用两次(即第二次刷新时,该 Token 已标记为使用),应判定为被盗用,将该用户的所有 RefreshToken 失效,并要求重新登录。
8. 踩坑记录:RefreshToken 过期处理、并发刷新、轮换策略
8.1 RefreshToken 过期处理
RefreshToken 本身也有有效期,如果用户在有效期内关闭浏览器再打开,AccessToken 可能已过期,但 RefreshToken 还在。此时前端应在启动时主动静默刷新一次,服务端返回新的 AccessToken 和 RefreshToken。
更优雅的做法是:前端在 RefreshToken 过期前(例如剩余 1 天),主动调用一个 /auth/refresh/status 接口来预判是否需要刷新。但实践中,大多数应用直接等 401 触发刷新即可,因为刷新接口是幂等的。
8.2 并发刷新问题
当同时多个 API 请求都返回 401 时,前端可能发起多个并发的 /auth/refresh 请求。这会导致:
- 服务端收到重复的刷新请求,第一个请求成功轮换了 RefreshToken,后续请求携带的旧 RefreshToken 已被标记为使用,因此返回失败。
- 多个失败导致前端误判为 RefreshToken 也过期,进入登录页。
解决方案:前端使用一个 pending promise 来去重——第一个 401 触发刷新时,后续所有 401 等待同一个刷新的 Promise 完成,然后重试。
1 | |
8.3 轮换策略陷阱
如果轮换时只更新而不删除旧 Token,攻击者可同时持有新旧两个 RefreshToken,获得双倍的有效期窗口。务必在生成新 RefreshToken 后,立即删除或标记旧记录。
另外,如果服务端采用无状态 JWT 作为 RefreshToken(即 JWT 本身携带过期信息,不做存储),则无法实现真正的轮换。这种情况下,攻击者同时持有新旧 Token 都能使用。建议始终使用服务端状态记录来控制轮换。
8.4 长期不活跃
用户关闭浏览器 7 天后,RefreshToken 过期,需要重新登录。如果希望允许用户长期保持会话,可使用滑动会话策略:每次刷新时更新 RefreshToken 的过期时间为 当前时间 + 7天。但需要注意,这会使令牌长期不失效,大屏显示等场景需配合其他安全措施(如 IP 绑定、设备指纹)。
9. OAuth2 RefreshToken 与 JWT 结合——企业级扩展
9.1 OAuth2 中的 RefreshToken
在 OAuth2 授权码流程中,Authorization Server 返回:
access_token:可以是 JWT 或不透明字符串。refresh_token:通常是一个不透明随机字符串,需服务端查库校验。OAuth2 的刷新流程与自研双 Token 类似,但增加了client_id和client_secret的校验。
9.2 自研双 Token vs OAuth2 方案对比
| 维度 | 自研双 Token | OAuth2(授权码模式) |
|---|---|---|
| 适用场景 | 单一应用、内部系统 | 多系统 SSO、第三方授权 |
| 刷新验证 | 仅验证 RefreshToken | 需要验证 client_id + client_secret + refresh_token |
| 轮换要求 | 建议强制轮换 | public client 强制轮换,confidential client 可选 |
| 无状态性 | 可支持(JWT),但轮换需查库 | 不支持无状态 |
9.3 集成建议
- 对于企业内部系统,自研双 Token 足够,实现简单。
- 如果需要对接多个外部应用(如第三方登录、SSO 网关),使用 OAuth2 框架(如 Spring Authorization Server, Keycloak)。
注意事项:OAuth2 RefreshToken 在不轮换的情况下,只要不发送刷新请求,旧 Token 一直有效。公共客户端(public client,如纯前端应用)要求强制轮换,以降低泄露风险。
10. 总结与拓展
10.1 核心要点回顾
AccessToken 和 RefreshToken 的分工:短期 AccessToken 用于业务鉴权,长期 RefreshToken 仅在刷新接口使用。
轮换与失效:每次刷新后必须废弃旧 RefreshToken,服务端标记为已使用或删除,防止重放攻击。
安全存储:前端用 httpOnly Cookie 存储 RefreshToken,后端存储其哈希值,数据库/Redis 访问严格控制 ACL。
并发处理:前端用 pending promise 去重刷新请求,服务端加乐观锁或版本号防止同一 Token 被并发使用。
过期策略:RefreshToken 设置合理有效期(7~30天),结合滑动窗口或主动预刷新提升体验。
10.2 拓展思考
绑定设备指纹:在 RefreshToken 记录中保存用户的 User-Agent、IP 段、设备 ID。刷新时比对,不一致则要求重新登录。可降低令牌被跨设备盗用的风险。
Device Grant 流程:对于无浏览器客户端(如移动 App、IoT 设备),可使用 OAuth2 设备授权码流程替代双 Token 模型。
性能影响:每次刷新仅涉及一次 Redis 查询(或数据库查找),相比重新登录(可能涉及 LDAP、密码哈希等),开销更小。双 Token 模型对后端性能几乎无影响。
推荐工具与框架:
- Spring Security:内置
refresh_token扩展,结合OAuth2AuthorizationService即可快速实现。
- Spring Security:内置
Node.js:
express-jwt+redis实践轻量级实现,搭配helmet增强安全头。- 通用:Keycloak、Auth0 等服务可直接配置 RefreshToken 行为。
通过合理使用双 Token 模型,可以在不牺牲安全性的前提下,实现用户无感的会话续期,提升系统整体体验。本文提供的方案已在多个生产环境中实践,可根据业务需要选择适配。
总结
通过本文的学习,相信你已经对「JWT」有了更深入的理解。建议结合实际项目多加练习。如有疑问,欢迎交流!