标签 JWT 下的文章

为什么要提供一个生命周期短的 Access Token

主要是出于 安全性可控性 的考虑,虽然看起来多了一步“刷新”,但整体上能大幅降低风险并提升灵活度:

  1. 降低令牌泄露后的风险

    • 如果你只发一个超长生命周期的 Access Token,一旦它被截获,不论是网络中间人攻击、XSS 漏洞还是客户端泄密,攻击者都能在很长一段时间内肆意调用你的 API。
    • 而短生命周期(比如 5–15 分钟)的 Access Token 即使被拿到,也只能在极短的窗口期内使用,过期后就作废,大部分攻击都来不及实施。
  2. 更灵活的撤销与控制

    • 假设用户强制登出、改了密码、或者你的风控系统发现异常行为,你需要“立即”让已有令牌失效。
    • 如果只有一个超长寿命的 Token,你几乎没法撤销——你只能把它加入黑名单(需要网关实时查黑名单,性能和一致性都成问题)。
    • 而有了 Refresh Token 机制,Access Token 过期后客户端必须去校验 Refresh Token 才能拿新令牌。你只要在服务端将该 Refresh Token 作废,此后所有刷新尝试都会失败,Access Token 再也无法续期。
  3. 遵循 OAuth/OpenID Connect 最佳实践

    • 业界公认的做法就是“短命令牌 + 长续期凭证(Refresh Token)”。几乎所有主流授权服务(Auth0、Okta、Keycloak、Google、Facebook)都在这么做。
    • 这样可以在保证用户体验(自动刷新令牌)与安全风控(滚动作废、快速撤销)之间取得平衡。
  4. 减少频繁认证的用户体验成本

    • 如果只发一个超短命令牌,没续期机制,客户端就要在令牌过期前强制用户再登录,体验糟糕;
    • 如果发超长命令牌又担心泄露风险,那就落到同时要做黑名单和实时校验,复杂度和运维成本均高。
  5. Refresh Token 的额外安全措施

    • 你可以对 Refresh Token 做 一次性、可旋转(Rotating Refresh Token),以及 绑定客户端指纹IP/设备限制 等,使得它本身即便泄露也更难被滥用。
    • 并且 Refresh Token 通常只会在后端安全环境(或浏览器的 HttpOnly Cookie)中使用,不容易被窃取。

小结

  • 短生命周期的 Access Token:快速到期,暴露窗口小。
  • 长生命周期的 Refresh Token:隐蔽存储,可控续期,方便撤销。

二者结合,才能在 安全性用户体验 之间取得最佳平衡——既不会让你频繁弹框让用户重登录,也不会让攻击者长期持有有效凭证。

为什么不担心 Refresh Token 泄露呢

虽然 Refresh Token 生命周期更长,但我们并不是“完全不担心”它的泄露,而是通过一系列设计和防护手段,把它的风险降到可控、可检测的程度。关键在于:

  1. 安全存储

    • HttpOnly + Secure Cookie:在浏览器端把 Refresh Token 放在只读且只能通过 HTTPS 访问的 Cookie 里,JS 无法读取,也不随非安全连接暴露。
    • 安全保管:移动端或服务端应用通常把它存在受信任的存储(Keychain、Keystore、Vault),避免曝光给前端。
  2. 绑定使用环境

    • Client Binding:每个 Refresh Token 都和特定的客户端 ID/密钥(或证书)绑定,只能由它自己来使用。即使被窃取,离开了原来的客户端就无法成功请求新令牌。
    • 设备指纹:可以额外记录设备标识(IP、User-Agent、TLS 信息等),对比不符就拒绝刷新。
  3. 旋转与一次性

    • Rotating Refresh Tokens:每次刷新后,服务端发回一个全新的 Refresh Token,并让旧的立即失效。这意味着即便被截获,也只可用一次且生命周期极短。
    • 防重放:你可以在服务端记录上一次的令牌 ID(jti),对重复使用进行拒绝。
  4. 可撤销与监控

    • 黑名单/白名单:一旦检测到异常,比如同一 Refresh Token 在两地几乎同时使用,就能主动撤销它,并通知用户重新登录。
    • 速率限制:对刷新接口加严格调用频率限制,暴力猜测或批量攻击会被挡下。
  5. 最小权限原则

    • Scope 限制:Refresh Token 可以只用于刷新令牌,不携带直接访问资源的权限。真正调用 API 还是要用短期 Access Token。
    • Audience 限定:只允许在特定的授权服务器和资源服务间使用,不要让它流到其他系统。
  6. 监控与告警

    • 实时监控刷新失败率、地理位置和设备变化。一旦发现可疑行为,自动阻断并要求重新登录。

小结

  • Access Token 泄露 -> 窃取者仅能在很短时间内调用 API,风险窗口小。
  • Refresh Token 泄露 -> 因为它存放更安全、且有绑定、旋转、撤销、监控等多重保护,就算被拿到,也很难拿去继续换取新令牌,或者只能换一次很快就失效。

通过上述多层防御,即使长期凭证(Refresh Token)生命周期更长,其风险也在可控范围内,而用户体验依然流畅。

给 Refresh Token 做的安全措施也可以用于 Access Token,为什么不用

确实可以把“Refresh Token 的安全机制”也用到 Access Token(JWT)上,但这样做往往会丧失 JWT 最大的两个优势:高效可缓存的无状态验证。具体来说:

  1. 频率 vs. 负载

    • Access Token 会在每次 API 请求中带上,服务端通常只做一次本地签名验证(HMAC/公钥验签)就可以判断它是否合法,几乎没有额外开销。
    • 如果要对每个 Access Token 都做「环境绑定」「黑名单检查」「一次性旋转」等,就要在内存/Redis/数据库里查一遍,会给高并发 API 带来很大延迟和运维负担。
  2. 生命周期与撤销策略

    • Access Token 本身被设计成短命(比如 5–15 分钟),过期后即自动失效,所以大多数场景下不需要做专门的“撤销”——你让它到期就行。
    • 如果你对 Access Token 也要做“撤销”或“旋转”,那就得维护一份「当前有效的 jti 列表」或「黑名单」,每一次请求前都要查询,这又回到状态化令牌的模式,没有必要。
  3. 缓存与性能

    • 典型做法是把公钥/JWKs 缓存在内存,签名验证不再访问外部系统,API 响应几乎无感知。
    • 一旦加入「每次验证都要打缓存层」或「读写会话表」的步骤,不仅吞吐下降,还可能引入可用性风险(缓存挂了、DB 慢了都影响 API)。
  4. 折衷做法:黑名单 + 短命令牌

    • 如果你确实需要对 Access Token 做紧急撤销,可以维护一个「最近被撤销令牌的黑名单」,但只缓存剩余有效期很短的一小部分 jti,查黑名单命中率低,性能影响也有限。
    • 再配合 Access Token 本身极短的生命周期,整体依然比把所有保护都并到每次请求上轻量得多。

总结

  • 技术上可行,把 Refresh Token 上的安全手段都搬到 Access Token 上;
  • 但实践中一般不用,因为会让每天成千上万次的 API 调用都要跑一次 DB/缓存查状态,击垮你的高性能场景。

因此,我们才把 Access Token 设计成短命+本地验签、Refresh Token 设计成长命+状态化管理,二者配合,既满足了安全、撤销、环境绑定等需求,也保证了高并发 API 的性能体验。