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

原文链接

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

很多登录态设计讨论,最终都会落到"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 精确配置、异常检测都需要服务端配合。但前端可以把会话生命周期设计得更清晰,让攻击发生或登录失效时,系统能够更快地收敛到"停止使用、清理状态、重新认证",而不是让一个过期或被盗的凭证持续失控。

参考资料

相关推荐
小马过河R3 天前
微信小程序自定义登录态维护:从入门到生产级落地
后端·微信小程序·小程序·架构·登录态
小江的记录本6 天前
【CSS】CSS 核心:盒模型、BFC/IFC、Flex/Grid 布局、响应式布局、移动端适配(附《思维导图》)
前端·css·面试·前端框架·tensorflow·html5·xss
DsirNg6 天前
凭证不是登录态:从攻击面重做浏览器认证设计
oauth·xss·csrf·token·cookie·认证授权·web 安全
蒲公英eric8 天前
从客户端到服务端:DVWA DOM 型 XSS 模块完整漏洞分析教程
前端·web安全·ai·xss·dvwa·ai安全
Blockchina9 天前
Codex安全盲区:我用4组本地测试复盘SQLi、XSS、越权与路径穿越
sql·安全·xss
黄俊懿10 天前
【架构师从入门到进阶】第五章:DNS&CDN&网关优化思路——第七节:网关-XSS攻击与预防
网关·网络安全·架构·系统架构·架构师·xss·架构设计
HackTwoHub10 天前
Butter_Cookie 黄油曲奇|浏览器渗透插件,云存储检测、XSS、SQL 注入一站式 Web 安全测试工具
前端·sql·安全·web安全·网络安全·自动化·xss
网安蟹佬霸14 天前
Android安全攻防实战:从APK逆向到Frida动态Hook全流程详解(附脚本)
android·前端·安全·web安全·逆向·csrf·网安
Rain的Java大神之路16 天前
短信接口被狂刷怎么处理
java·运维·后端·web安全·面试·架构·xss
竹枝溪16 天前
Tomcat与Servlet全套小白教程
java·servlet·tomcat·cookie·filter·session·listener