Back

Cookies 与 localStorage 在 JWT 身份认证中的对比

Cookies 与 localStorage 在 JWT 身份认证中的对比

你已经构建好了身份认证流程,JWT 也运行正常,现在你面临着每个前端开发者迟早都会遇到的问题:这个 token 到底该存在哪里?这个问题的重要性远超大多数教程所揭示的程度,而那句常见的建议——“直接用 HttpOnly cookies”——跳过了你真正需要理解的权衡取舍。

下面将清晰地梳理两种方案,分析各自实际能防御哪些攻击,以及现代应用在实践中如何处理这一问题。

核心要点

  • localStorage 可被页面上运行的任何 JavaScript 完全访问,因此容易遭受基于 XSS 的 token 窃取攻击。
  • HttpOnly cookies 完全阻止 JavaScript 访问,但会引入 CSRF 风险,可通过 SameSiteSecure 属性加以缓解。
  • 现代主流方案是将短期访问令牌存储在内存中,将刷新令牌存储在设置了 HttpOnlySecureSameSite 属性的 cookie 中。
  • OWASP 及 OAuth 浏览器端应用规范均不建议将长期令牌存放在 localStorage 中。
  • 正确的选择取决于你的威胁模型、对后端的控制程度,以及你的 API 是否要求使用 Authorization 请求头。

JWT 存储究竟涉及哪些风险

JWT 的存储位置决定了哪些攻击向量会对你的应用构成威胁。两种主要威胁分别是:

  • XSS(跨站脚本攻击): 恶意 JavaScript 在你的应用上下文中执行。
  • CSRF(跨站请求伪造): 诱使用户的浏览器发出非预期的已认证请求。

没有任何一种存储方案能同时消除这两种风险。关键在于理解你所接受的是哪种风险,以及如何对其进行缓解。

localStorage:便捷但可被 JavaScript 访问

将 JWT 存储在 localStorage 中非常直观——你可以写入、读取,并手动将其附加到 Authorization: Bearer 请求头中。对于期望使用该请求头格式的 API,这种方式配合得相当顺畅。

问题在于,localStorage 可被页面上运行的任何 JavaScript 完全访问。一旦攻击者成功注入脚本——无论是通过依赖项漏洞、被入侵的 CDN,还是你自己代码中的 XSS 缺陷——他们就能直接读取并窃取 token。正因如此,OWASP 明确不建议将会话标识符存储在 localStorage 中。

这并非纸上谈兵。现代 Web 应用会引入数十个第三方脚本,每一个都是潜在的攻击面。

HttpOnly Cookies:更强的 XSS 防护,但需考量新问题

HttpOnly cookie 完全无法被 JavaScript 读取。即使攻击者在你的页面上运行了代码,也无法提取 token 的值。这是一项实质性的安全提升。

但 cookie 会带来 CSRF 暴露风险。浏览器会自动将 cookie 附加到匹配的请求中,包括由恶意第三方网站触发的请求。

以下三个 cookie 属性协同作用,可有效弥补这一缺口:

  • HttpOnly — 完全阻止 JavaScript 访问。
  • Secure — 仅通过 HTTPS 传输 cookie。
  • SameSite — 控制 cookie 在跨站场景下的发送行为。

关于 SameSite,当该属性未设置时,现代浏览器默认采用 Lax 模式,该模式会阻止跨站子请求(例如来自其他源的 POST 请求)携带 cookie,但允许顶级导航时发送。Strict 模式更为严格,会阻止任何跨站请求(包括顶级导航)携带 cookie。建议始终显式设置该属性,而非依赖浏览器默认行为。现代浏览器对 SameSite 的支持情况良好,可通过 Can I Use 查阅详情。

在正确配置 SameSite=StrictLax 的前提下,大多数同站身份认证场景的 CSRF 风险可得到显著降低。对于敏感的状态变更接口,建议结合反 CSRF token 实现纵深防御。

现代应用普遍采用的方案

许多生产环境应用采用拆分存储的方式来解决这一问题:

  1. 短期访问令牌存储在 JavaScript 内存中(模块级变量或 React state)。
  2. 刷新令牌存储在设置了 HttpOnlySecureSameSite 属性的 cookie 中。

访问令牌会在标签页关闭或页面刷新时消失,但通过静默调用 /refresh 接口,可利用 cookie 获取新的访问令牌。访问令牌永远不会写入持久化存储,刷新令牌也永远无法被 JavaScript 读取。

这一方案与当前使用 OAuth 2.0 PKCE(带 PKCE 的授权码流程)的浏览器端应用规范保持一致,也是 OAuth 2.0 浏览器端应用规范所推荐的做法。如果你使用的是 OpenID Connect(OIDC),同样适用这一方案——请将 ID token 和刷新令牌都从 localStorage 中移除。

安全审查清单

上线前,请逐项核查:

  • ☑ 所有存储 token 的 cookie 均已设置 HttpOnly 标志。
  • ☑ 已启用 Secure 标志(强制使用 HTTPS)。
  • ☑ 已显式将 SameSite 设置为 StrictLax
  • ☑ 访问令牌有效期较短,通常以分钟而非小时计。
  • ☑ 已配置 Content Security Policy 请求头。
  • localStorage 中不存在长期有效的 JWT。

为你的应用选择合适的方案

没有放之四海而皆准的答案。如果你能控制后端,且应用与 API 部署在同一域名下,配置了正确 SameSite 属性的 HttpOnly cookies 是更稳健的默认选择。如果你需要集成要求使用 Authorization 请求头的第三方 API,且无法在服务端设置 cookie,那么使用短期有效的内存存储是合理的备选方案——但切记不要将长期令牌持久化到 localStorage 中。

总结

当前的安全规范一致不建议将长期有效的 JWT 存放在 localStorage 中。对于大多数同域场景,配置了 SecureSameSite 属性的 HttpOnly cookies 是最稳健的默认方案;而对于更复杂的场景,内存存储结合刷新令牌 cookie 则是更合适的选择。一旦你理解了威胁模型——一边是 XSS,另一边是 CSRF——就能清晰地权衡利弊,为你的应用做出合理的选择,而不是凭感觉猜测。

常见问题

sessionStorage 与 localStorage 存在相同的弱点:页面上运行的任何 JavaScript 都可以读取其中的内容。两者唯一的区别在于,sessionStorage 会在标签页关闭时清除数据。这缩短了暴露窗口,但并不能防御 XSS 攻击。在 token 存储方面,应以与 localStorage 相同的谨慎态度对待 sessionStorage,避免将长期令牌存放其中。

SameSite=Strict 会阻止跨站请求携带 cookie,从而防御大多数 CSRF 攻击模式。但对于高价值的状态变更接口,额外添加反 CSRF token 可实现纵深防御。由于 SameSite 由浏览器强制执行,旧版客户端或某些边缘情况可能不遵守该属性。因此,双重提交 token 模式仍是一种合理的安全保障措施。

常见的范围是 5 到 15 分钟。这足够短,使得被盗的 token 利用价值有限;同时也足够长,不至于频繁请求刷新接口。与此配合的是存储在 HttpOnly cookie 中的刷新令牌,其有效期可设置为数小时到数天。如果你的应用涉及支付等敏感操作,建议缩短有效期,并对关键操作要求重新认证。

将访问令牌存储在 JavaScript 内存中——模块级变量、React state 或闭包——而非 localStorage。保持其短期有效,并尽可能通过后端接口进行刷新。如果确实需要在页面重载后保持状态,应将刷新流程路由到你自己的后端,由后端在服务端持有长期凭证,切勿将长期令牌暴露给客户端存储。

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay