登录态失控时,前端如何止损:从令牌轮换到会话退出的完整设计

原文链接

登录态失控时,前端如何止损:从令牌轮换到会话退出的完整设计

很多登录态设计讨论,最终都会落到"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)代理业务请求;让刷新令牌或服务端会话标识放进 HttpOnlySecure Cookie。这样做的目标不是宣称"不会被 XSS 攻击",而是降低攻击脚本直接读取长期凭证并带离浏览器的机会。

Cookie 属性需要分别理解:

  • HttpOnly:阻止 JavaScript 通过 document.cookie 直接读取 Cookie,主要保护凭证的保密性
  • Secure:仅允许 Cookie 通过 HTTPS 发送。
  • SameSite=StrictLax:限制 Cookie 在部分跨站请求中的携带范围,是 CSRF 的重要纵深防御;但它不能覆盖所有业务场景和攻击路径,不能替代 CSRF 校验。
  • PathDomain:应尽量收窄;若不需要跨子域共享,不应设置宽泛的 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)任务

  1. 业务请求收到服务端明确标识为"访问令牌过期且可刷新"的 401 后,不立即各自刷新,而是检查是否已有刷新任务进行中。
  2. 若已有任务,当前请求等待该任务结果;若没有,则由一个请求创建刷新任务。
  3. 刷新成功后,仅重试一次原请求;刷新失败或重试仍失败,则进入统一退出流程。
  4. 对登录失效、权限不足、账户冻结等不同错误码做区分,不能把所有 401403 都当成"刷新一下"。

多标签页还需要协同。一个标签页完成刷新或退出后,应通过 BroadcastChannel 等机制通知其他标签页清理或更新本地内存态、取消待重试请求并跳转到登录页。若访问令牌仅保存在内存中,不应为了同步而将其写入持久化存储;其他标签页可在后续受控请求中依据服务端状态重新获取短期凭证。广播只是体验与一致性优化,最终裁决仍必须来自服务端的会话有效性判断。

刷新令牌的价值,在于"轮换后可发现重放"

刷新令牌最危险的误用,是把它设置成超长有效期、可无限重复使用的通行证。更可靠的思路是刷新令牌轮换(Refresh Token Rotation):每次刷新成功后,服务端签发一个新的刷新令牌,并让旧令牌失效。

刷新端点需要在服务端以原子方式处理旧令牌的消费,避免多个并发刷新请求都被当作首次合法使用。若已失效的旧令牌之后再次出现,服务端应将其视为可能的重放信号,并撤销仍处于活跃状态的令牌或整个会话族,要求用户重新认证。OAuth 2.0 Security Best Current Practice 要求公共客户端的刷新令牌采用发送者约束或刷新令牌轮换之一,并建议支持闲置过期及在密码修改、授权端退出等安全事件后撤销。RFC 9700

这意味着前端必须接受一个现实:刷新失败不是异常兜底,而是安全状态切换点。刷新接口明确失败时,前端不应无限重试,也不应继续保留"用户看起来已登录"的界面;应清理可读状态、广播退出,并要求重新登录。

XSS 防护的重点:阻止攻击脚本获得执行权

"不把 Token 放 localStorage"是合理的风险收敛,但它不是 XSS 防护方案。因为一旦攻击者脚本可以执行,即使 Cookie 不可读,也可能以当前用户身份发起请求。

前端的优先级应是阻断危险数据进入执行位置:

  • 优先使用框架默认的文本渲染与属性绑定,不绕过自动转义。
  • 审查 innerHTMLouterHTMLinsertAdjacentHTMLdocument.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=StrictLax 的 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 是否设置了 SecureHttpOnly、合适的 SameSite,且 DomainPath 没有无谓放宽?
  • 使用 Cookie 的状态变更接口,是否有 CSRF 校验?
  • 多个 401 是否只触发一次刷新?刷新失败是否只会统一退出,而非循环重试?
  • 多标签页是否能同步退出和服务端强制失效?
  • 是否禁止在 URL、控制台、前端埋点与服务端日志中记录认证凭证?
  • 危险 HTML 注入点、富文本、第三方脚本、CSP 与依赖升级是否进入安全审查?
  • 服务端是否能查看和撤销设备会话,并对异常刷新、旧令牌重放或异常地域切换告警?

前端无法单独保证登录态安全:刷新令牌存储、会话撤销、CSRF 校验、CORS 精确配置、异常检测都需要服务端配合。但前端可以把会话生命周期设计得更清晰,让攻击发生或登录失效时,系统能够更快地收敛到"停止使用、清理状态、重新认证",而不是让一个过期或被盗的凭证持续失控。

参考资料

相关推荐
云水一下7 小时前
零基础玩转bWAPP靶场(六十四):XSS - Reflected (Custom Header)
web安全·xss·bwapp·header·custom·reflected
障碍的枫子2 天前
xss和CSRF的攻击和防御
前端·xss·csrf
qyyyyy5703 天前
英文技术标准和协议规范怎么读?RFC、接口规范与安全标准 PDF 翻译流程
网络协议·网络安全·机器翻译·csrf·技术美术
云水一下3 天前
零基础玩转bWAPP靶场(五十八):XSS - Stored (User-Agent)
web安全·xss·bwapp·user-agent·stored
Full Stack Developme3 天前
跨站请求伪造 (CSRF) 是什么 设计及工作原理
前端·okhttp·csrf
三8443 天前
CSRF跨站请求伪造基础
前端·csrf
Sagittarius_A*4 天前
Stored Cross Site Scripting(XSS)存储型跨站脚本漏洞:恶意数据持久化缺陷与 DVWA 分级绕过实战
前端·网络·web安全·网络安全·xss·dvwa
云水一下5 天前
零基础玩转bWAPP靶场(四十六):XSS - Reflected (AJAX/JSON)
web安全·ajax·json·xss·bwapp·reflected
云水一下5 天前
零基础玩转bWAPP靶场(五十五):XSS - Stored (Blog)
web安全·xss·blog·bwapp·stored