不再用localStorage:2025年用Cookie管理JWT令牌的最佳方法
很多开发者习惯把JWT令牌存在localStorage里,但这样做其实有安全风险。如果网站存在XSS漏洞,攻击者可以轻易偷走令牌。换个方法,把令牌放在HttpOnly Cookie里,能解决这个问题。
HttpOnly Cookie有三个好处:
JavaScript无法读取,XSS攻击偷不走令牌
浏览器自动在每次请求中携带,不需要手动设置请求头
可以使用安全标志,降低CSRF和窃听风险
只需要一些配置,就能大幅提升安全性。
服务器端配置:Node.js中签发和刷新令牌
先搭建一个简单的Express服务器:
// server/index.mjs
import express from 'express';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
const app = express();
app.use(express.json());
app.use(cookieParser());
// 设置CSRF保护
app.use(csurf({ cookie: true }));
// 获取CSRF令牌的接口
app.get('/api/csrf-token', (req, res) => {
res.setHeader('x-csrf-token', req.csrfToken());
res.sendStatus(204);
});这段代码设置了JSON解析、Cookie解析和CSRF保护。之后所有POST、PUT、DELETE请求都需要提供CSRF令牌。
登录接口:生成访问令牌和刷新令牌
import jwt from 'jsonwebtoken';
const isProd = process.env.NODE_ENV === 'production';
app.post('/api/token', (req, res) => {
const { username, password } = req.body;
// 这里应该验证用户名和密码
if (!isValidUser(username, password)) {
return res.status(401).json({ error: '用户名或密码错误' });
}
const accessToken = jwt.sign({ sub: username }, process.env.ACCESS_SECRET, {
expiresIn: '15m'
});
const refreshToken = jwt.sign({ sub: username }, process.env.REFRESH_SECRET, {
expiresIn: '7d'
});
res
.cookie('access_token', accessToken, {
httpOnly: true,
sameSite: 'strict',
secure: isProd,
maxAge: 15 * 60 * 1000
})
.cookie('refresh_token', refreshToken, {
httpOnly: true,
sameSite: 'strict',
secure: isProd,
maxAge: 7 * 24 * 60 * 60 * 1000
})
.json({ user: username, status: 'active' });
});验证通过后生成两个令牌,都设置为HttpOnly Cookie,JavaScript无法读取,但浏览器会自动发送。
刷新令牌接口
app.post('/api/token/refresh', (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) return res.sendStatus(401);
jwt.verify(refreshToken, process.env.REFRESH_SECRET, (err, decoded) => {
if (err) return res.sendStatus(403);
const newAccess = jwt.sign({ sub: decoded.sub }, process.env.ACCESS_SECRET, {
expiresIn: '15m'
});
const newRefresh = jwt.sign({ sub: decoded.sub }, process.env.REFRESH_SECRET, {
expiresIn: '7d'
});
res
.cookie('access_token', newAccess, {
httpOnly: true,
sameSite: 'strict',
secure: isProd,
maxAge: 15 * 60 * 1000
})
.cookie('refresh_token', newRefresh, {
httpOnly: true,
sameSite: 'strict',
secure: isProd,
maxAge: 7 * 24 * 60 * 60 * 1000
})
.json({ ok: true });
});
});这个接口用刷新令牌来获取新的访问令牌,保持用户登录状态。
前端react应用的使用方法
获取CSRF令牌:
async function getCSRF() {
const res = await fetch('/api/csrf-token', {
credentials: 'include'
});
return res.headers.get('x-csrf-token');
}登录函数:
const loginUser = async (formData) => {
const csrf = await getCSRF();
const res = await fetch('/api/token', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf
},
body: JSON.stringify(formData)
});
if (res.ok) {
const data = await res.json();
// 只存储非敏感信息
localStorage.setItem('username', data.user);
} else {
throw new Error('登录失败');
}
};自动刷新令牌:
useEffect(() => {
const refreshInterval = setInterval(async () => {
try {
const csrf = await getCSRF();
await fetch('/api/token/refresh', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf
}
});
} catch (error) {
console.log('令牌刷新失败');
}
}, 14 * 60 * 1000); // 每14分钟刷新一次
return () => clearInterval(refreshInterval);
}, []);实际应用场景
文件上传
const uploadFile = async (file) => {
const formData = new FormData();
formData.append('file', file);
await fetch('/api/upload', {
method: 'POST',
body: formData,
credentials: 'include'
});
};Next.js服务端渲染
export async function getServerSideProps({ req }) {
const token = req.cookies.access_token;
// 用令牌获取用户数据
return { props: { userData } };
}WebSocket连接
const socket = new WebSocket(
`wss://example.com/chat?token=${getTokenFromCookie()}`
);移动端React Native
// 使用Keychain或SecureStore存储令牌
import Keychain from 'react-native-keychain';
const storeToken = async (token) => {
await Keychain.setGenericPassword('token', token);
};安全建议
设置合适的令牌过期时间
生产环境一定要启用Secure标志
使用SameSite防止CSRF攻击
定期轮换密钥
监控异常登录行为
总结
从localStorage切换到HttpOnly Cookie管理JWT令牌,虽然需要一些额外配置,但能显著提高应用安全性。这种方法适合大多数Web应用,特别是需要较高安全要求的项目。
现代浏览器都支持这些特性,现在就开始升级你的认证系统吧。记住,安全是一个持续的过程,需要定期审查和更新安全措施。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!