# 双 Token 做了 Refresh Token 轮换，和只换 Access 差在哪？

**URL:** <https://www.shiqidu.com/t/topic/1895>\
**Category:** 开发调优\
**Tags:** 前端开发, OAuth, 信息安全\
**Created:** [2026 年11 月 2 日 06:47 UTC](https://www.shiqidu.com/t/topic/1895 "2026-11-02T06:47:46Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年11 月 2 日 06:47 UTC](https://www.shiqidu.com/t/topic/1895/1 "2026-11-02T06:47:46Z")

</div>

Access 过期就换一枚新的，refresh 却一直用同一枚——很多项目的「双 Token」停在这一步。另一种做法是每次兑换 access 时顺手作废旧 refresh，再发一枚新的。表面上还是 access + refresh，出事以后能撑多久，两边差很多。下面按 Auth0 的轮换说明，对照 RFC 9700 和 OWASP 的写法说威胁模型，不贴某家控制台逐步截图。

## 长寿命 refresh 漏了会怎样

短寿命 access 就算被人截到，窗口通常只有几分钟到一小时量级。refresh 往往按天、按周甚至更长活着，专门用来在用户无感的情况下续 access。一枚长期 refresh 落进 XSS、备份日志、共享电脑的本地存储，攻击者就可以反复换新的 access，直到你撤销或它过期。

Auth0《[Refresh Token Rotation](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation)》的定义是：每次用 refresh 换 access 时，授权服务器同时下发一枚新的 refresh，并作废请求里那枚旧的。这样不再长期握着「同一枚高价值凭证」。文档写明该做法符合 OAuth 2.0 BCP，并适用于授权码、带 PKCE 的授权码、设备码、资源所有者密码等流。

Auth0 还写了自动重用检测：旧 refresh 一旦在已作废后再次出现，会把同一家族后续签发的 refresh 一并废掉，逼双方重新登录。合法客户端和攻击者谁先用那枚泄露的 refresh，结果都是家族失效——这是用「偶发把好人踢下线」换「偷到一枚不能无限续命」。日志里可以看到类似失败兑换的事件，方便接到监控。

只轮换 access、refresh 固定不变时，泄露窗口约等于那枚 refresh 的剩余寿命。上了 rotation 之后，单枚 refresh 的有用窗口被压到「下一次成功兑换之前」。再叠加重用检测，两边抢同一枚旧票会整族作废。

## RFC 9700 / OWASP 怎么要求公共客户端

《[RFC 9700](https://www.rfc-editor.org/rfc/rfc9700)》（OAuth 2.0 Security BCP）§2.2.2 写：面向公共客户端的 refresh，必须做发送方约束（sender-constrained），或使用 refresh token rotation。§4.14 把 rotation 说成：每次刷新响应都发新 refresh，旧的作废并保留族谱关系。合法与恶意两侧若都拿着已作废的去换，服务器能察觉重放并撤销当前活跃 refresh。同一节还建议在密码变更、在授权服务器登出等安全事件时自动撤销 refresh，并让长期不用的 refresh 自行过期。

《[OWASP OAuth2 Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html)》同样把「sender-constraining（DPoP / mTLS）或 refresh token rotation」列为保护 refresh 的做法，并提醒 rotation 可与 PoP 叠加做纵深防御。发送方约束是「没有对应密钥就用不了这枚令牌」。轮换是「即便能用，窗口也短，重放会暴露」。两条不是互斥。

公共客户端（SPA、很多原生 App）存不了可靠的客户端密钥，单靠「refresh 很长、access 很短」不够。要么令牌绑定到客户端密钥材料，要么轮换把单枚 refresh 的有效窗口压短。

## 别把 localStorage 里的长期 refresh 当成「双 Token 就够了」

Auth0 原文对 SPA 说得很狠：浏览器里没有一种持久存储能保证「只有本应用能读到」高价值凭证，因此长期 refresh 并不适合 SPA。轮换是在浏览器隐私策略搞砸静默 iframe 续期之后的补救手段之一，不是「塞进 localStorage 就安全」。默认实现往往把令牌放在内存里，持久化要另看 Token Best Practices，不能自行脑补成「有 refresh 轮换 = localStorage 可放心」。

XSS 一旦能跑你的脚本，内存里的令牌和 localStorage 里的令牌都可能被读走。轮换降低的是「偷到之后长期免登录」的窗口，不消灭读取面。能上通行密钥、少依赖长期 refresh 时，前端少放一枚高价值字符串，始终更干净。服务端仍要有会话与撤销策略。通行密钥走挑战–响应、私钥不出设备，和 refresh 轮换是上下游关系，不是互相替代；浏览器侧入口一般是 `PublicKeyCredential`。

有没有团队上了轮换之后，被重用检测误伤、整族 refresh 被撤、用户突然要重新登录的？你们 refresh 现在放内存、放 HttpOnly Cookie，还是仍在 localStorage？
