登录态失控时,前端如何止损:从令牌轮换到会话退出的完整设计
很多登录态设计讨论,最终都会落到"JWT 放 Cookie 还是 localStorage"上。但这只是凭证如何携带或存放的一小部分。
真正决定安全上限的,是系统能否回答这些问题:凭证失效后会发生什么?多个请求同时过期时会不会重复刷新?刷新令牌被重放时能否让整组会话止损?用户退出后,旧页面、旧标签页和浏览器缓存里是否还留着可利用的状态?
会话标识、访问令牌与刷新令牌一旦被攻击者获得,在效果上接近用户已经完成的强认证。因此,登录态不应只被当作"前端请求头配置",而应被视为一条可签发、可续期、可撤销、可审计的安全链路。OWASP Session Management Cheat Sheet
先拆开职责:不要把 Cookie、Token 和 Session 当成同一个概念
一个可控的登录态通常有三类角色:
- 访问令牌(Access Token):用于调用业务 API,生命周期应较短;即使泄露,攻击窗口也相对有限。
- 刷新令牌(Refresh Token):用于换取新的访问令牌,权限更敏感、生命周期通常更长,不能把它当作"长期 Access Token"。
- 服务端会话记录或 Session ID:用于保存设备、会话族、过期时间、风险状态和撤销信息。即使访问令牌采用 JWT,服务端通常仍需要保留部分状态,才能支持强制下线、设备管理和异常处置。
Cookie 不是 Token 的对立面,而是浏览器传递凭证的一种载体。服务端 Session ID 可以放在 Cookie 中,刷新令牌也可以放在 Cookie 中;JWT 则既可能由脚本读取后放进 Authorization 请求头,也可能作为 HttpOnly Cookie 自动随请求携带。

关键不是名称,而是要明确每一类凭证的四件事:谁能读取、何时发送、有效多久、如何撤销。
登录和请求阶段:先缩小"凭证可被带走"的机会
对于浏览器应用,一个常见且相对稳妥的分层是:让短期访问令牌仅存在于内存中,或由 BFF(Backend for Frontend)代理业务请求;让刷新令牌或服务端会话标识放进 HttpOnly、Secure Cookie。这样做的目标不是宣称"不会被 XSS 攻击",而是降低攻击脚本直接读取长期凭证并带离浏览器的机会。
Cookie 属性需要分别理解:
HttpOnly:阻止 JavaScript 通过document.cookie直接读取 Cookie,主要保护凭证的保密性。Secure:仅允许 Cookie 通过 HTTPS 发送。SameSite=Strict或Lax:限制 Cookie 在部分跨站请求中的携带范围,是 CSRF 的重要纵深防御;但它不能覆盖所有业务场景和攻击路径,不能替代 CSRF 校验。Path、Domain:应尽量收窄;若不需要跨子域共享,不应设置宽泛的Domain。对主会话 Cookie,可评估使用__Host-前缀。OWASP Session Management Cheat Sheet
不要把认证令牌、刷新令牌或 Session ID 放入 URL 查询参数。URL 会进入浏览器历史、代理日志、服务端访问日志,以及可能的 Referer 传播链路;这会让一次偶然的跳转或日志采集变成凭证泄露。OWASP Session Management Cheat Sheet
HttpOnly 不等于 XSS 无害
HttpOnly 能让攻击脚本读不到 Cookie,但攻击脚本仍运行在受害者页面的同源上下文中。它可能直接调用转账、改邮箱、绑定设备等敏感接口;浏览器会自动附带会话 Cookie。
因此,Cookie 方案不能只做 Cookie 属性,还必须保护状态变更接口:服务端应校验 CSRF token,或使用经过严格设计的等价防线。CSRF token 应通过响应体或受控页面内容下发,并在自定义请求头或请求体中回传;不要把它拼到 URL。OWASP Cross-Site Request Forgery Prevention Cheat Sheet
静默刷新:解决 401 不是"再请求一次"
访问令牌过期后,如果每一个收到 401 的请求都自行调用刷新接口,就会出现刷新风暴:十个并发 API 请求可能触发十次刷新,旧刷新令牌被并发使用后还可能导致合法会话被误判为重放。
前端应把刷新设计成单飞(single-flight)任务:
- 业务请求收到服务端明确标识为"访问令牌过期且可刷新"的
401后,不立即各自刷新,而是检查是否已有刷新任务进行中。 - 若已有任务,当前请求等待该任务结果;若没有,则由一个请求创建刷新任务。
- 刷新成功后,仅重试一次原请求;刷新失败或重试仍失败,则进入统一退出流程。
- 对登录失效、权限不足、账户冻结等不同错误码做区分,不能把所有
401或403都当成"刷新一下"。
多标签页还需要协同。一个标签页完成刷新或退出后,应通过 BroadcastChannel 等机制通知其他标签页清理或更新本地内存态、取消待重试请求并跳转到登录页。若访问令牌仅保存在内存中,不应为了同步而将其写入持久化存储;其他标签页可在后续受控请求中依据服务端状态重新获取短期凭证。广播只是体验与一致性优化,最终裁决仍必须来自服务端的会话有效性判断。
刷新令牌的价值,在于"轮换后可发现重放"
刷新令牌最危险的误用,是把它设置成超长有效期、可无限重复使用的通行证。更可靠的思路是刷新令牌轮换(Refresh Token Rotation):每次刷新成功后,服务端签发一个新的刷新令牌,并让旧令牌失效。
刷新端点需要在服务端以原子方式处理旧令牌的消费,避免多个并发刷新请求都被当作首次合法使用。若已失效的旧令牌之后再次出现,服务端应将其视为可能的重放信号,并撤销仍处于活跃状态的令牌或整个会话族,要求用户重新认证。OAuth 2.0 Security Best Current Practice 要求公共客户端的刷新令牌采用发送者约束或刷新令牌轮换之一,并建议支持闲置过期及在密码修改、授权端退出等安全事件后撤销。RFC 9700
这意味着前端必须接受一个现实:刷新失败不是异常兜底,而是安全状态切换点。刷新接口明确失败时,前端不应无限重试,也不应继续保留"用户看起来已登录"的界面;应清理可读状态、广播退出,并要求重新登录。
XSS 防护的重点:阻止攻击脚本获得执行权
"不把 Token 放 localStorage"是合理的风险收敛,但它不是 XSS 防护方案。因为一旦攻击者脚本可以执行,即使 Cookie 不可读,也可能以当前用户身份发起请求。
前端的优先级应是阻断危险数据进入执行位置:
- 优先使用框架默认的文本渲染与属性绑定,不绕过自动转义。
- 审查
innerHTML、outerHTML、insertAdjacentHTML、document.write等危险注入点;富文本必须经可信净化器处理,且净化规则要随业务允许的标签和属性收敛。 - 管理第三方脚本、埋点、客服组件与供应链依赖;它们进入页面后通常拥有与业务代码相近的执行能力。
- 部署严格 CSP,使用 nonce 或 hash 限制允许执行的脚本,避免宽泛白名单、内联事件处理器和不必要的
eval能力。 - 对大型应用渐进启用 Trusted Types;
require-trusted-types-for 'script'可阻止普通字符串直接流入部分危险 DOM 注入点,从机制上迫使团队集中处理 HTML 创建入口。MDN:Content Security Policy
跨源架构:credentials: 'include' 不是万能开关
当前端与 API 不同源时,写上 fetch(url, { credentials: 'include' }) 只表达"请求愿意携带凭证"。浏览器是否实际发送 Cookie、是否处理响应中的 Set-Cookie,还取决于 Cookie 的 SameSite 配置、请求上下文及服务端的 CORS 响应。
携带凭证的 CORS 响应不能使用 Access-Control-Allow-Origin: *,而应返回明确允许的 Origin,并配合 Access-Control-Allow-Credentials: true。同时,SameSite=Strict 或 Lax 的 Cookie 不会因为 credentials: 'include' 自动变成跨站可发送。跨站 Cookie 若必须使用 SameSite=None,则必须同时设置 Secure,并重新评估 CSRF 风险与浏览器第三方 Cookie 限制。MDN:Set-Cookie header
因此,跨源 SPA、跨子域 SSO、iframe 嵌入和 OAuth 回调不应直接照搬同源站点配置;它们需要单独建模 Cookie 范围、CORS Origin 白名单、CSRF 校验和浏览器兼容策略。
退出与强制失效:让"已退出"真正生效
一次完整退出至少包含两侧动作:
- 前端:清理内存中的访问令牌和用户资料,删除可读存储中的敏感状态,取消等待中的刷新或重试任务,并广播其他标签页退出。
- 服务端:撤销当前会话、刷新令牌或会话族,清除认证 Cookie,并在高风险事件后支持按设备、按用户或按会话族强制失效。
认证相关响应应避免被缓存,例如使用 Cache-Control: no-store。在合适的退出或安全处置场景,服务端还可使用 Clear-Site-Data 协助清除当前源相关的缓存、Cookie 和存储;是否启用应结合产品对离线数据和多端体验的要求评估。OWASP Session Management Cheat Sheet
一份以"失效控制"为核心的检查清单
上线前,不妨逐项确认:
- 访问令牌是否短期有效,刷新令牌是否轮换、是否能识别重放?
- Cookie 是否设置了
Secure、HttpOnly、合适的SameSite,且Domain与Path没有无谓放宽? - 使用 Cookie 的状态变更接口,是否有 CSRF 校验?
- 多个
401是否只触发一次刷新?刷新失败是否只会统一退出,而非循环重试? - 多标签页是否能同步退出和服务端强制失效?
- 是否禁止在 URL、控制台、前端埋点与服务端日志中记录认证凭证?
- 危险 HTML 注入点、富文本、第三方脚本、CSP 与依赖升级是否进入安全审查?
- 服务端是否能查看和撤销设备会话,并对异常刷新、旧令牌重放或异常地域切换告警?
前端无法单独保证登录态安全:刷新令牌存储、会话撤销、CSRF 校验、CORS 精确配置、异常检测都需要服务端配合。但前端可以把会话生命周期设计得更清晰,让攻击发生或登录失效时,系统能够更快地收敛到"停止使用、清理状态、重新认证",而不是让一个过期或被盗的凭证持续失控。