jwt 令牌严禁存 localstorage,必须用内存存储或 httponly cookie;refresh_token 必须通过 httponly+secure+samesite=strict cookie 传输;前端须用 authfetch 拦截 401 并排队刷新;服务端必须返回 x-auth-expiry 响应头供前端判断过期。

JWT 令牌不能存 localStorage,这是前端安全红线。只要页面存在 XSS 漏洞,localStorage 里的 access_token 就会被直接读走——它不设防、不可撤销、无法绑定设备,等于把钥匙挂在门把手上。
为什么不能用 localStorage 存 access_token
这不是“推荐不这么做”,而是明确违反 OWASP ASVS V7.1.1。常见错误现象包括:
- 用户登出后,
localStorage中的 token 仍残留,下次打开页面自动携带,造成“假登出” - Chrome 扩展或恶意脚本执行
localStorage.getItem('token')即可拿到凭证 - 服务端吊销了该 token(如用户改密),但客户端仍用旧 token 请求,直到自然过期
真正可用的方案只有两个:内存存储(最安全)或 HttpOnly Cookie(需配合 CSRF 防护)。前者适合 SPA;后者适合 SSR 或混合渲染场景。
refresh_token 必须走 HttpOnly + Secure + SameSite=Strict Cookie
前端 JS 永远不该拿到 refresh_token 字符串。它的唯一合法载体就是服务端下发的 Cookie,且必须满足三项硬性要求:
-
HttpOnly:阻止document.cookie读取,防御 XSS 直接窃取 -
Secure:强制仅通过 HTTPS 传输,防止中间人明文截获 -
SameSite=Strict:拒绝跨站请求携带,大幅降低 CSRF 风险
如果服务端返回的 Refresh Token 是明文塞进响应体(比如 { refresh_token: "xxx" }),那整个刷新流程就已失效——你等于把长期有效的高危凭证交到 JS 手中。检查响应头:Set-Cookie: refresh_token=abc123; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh 才算合规。
前端必须封装统一的 authFetch() 拦截 401 并排队刷新
没有拦截器的刷新逻辑,必然触发竞态条件。典型表现是:多个并行请求同时收到 401,各自发起刷新,结果只有一条成功,其余全失败并跳登录页。
核心做法是用一个共享 Promise 控制刷新动作:
let refreshTokenPromise = null;
function authFetch(url, options = {}) {
return fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${accessTokenFromMemory}`,
...options.headers
}
}).catch(err => {
if (err.name === 'TypeError' && err.message.includes('fetch')) throw err;
}).then(res => {
if (res.status === 401 && !url.includes('/auth/refresh')) {
if (!refreshTokenPromise) {
refreshTokenPromise = fetch('/auth/refresh', { method: 'POST', credentials: 'include' })
.then(r => r.json())
.then(data => {
accessTokenFromMemory = data.access_token;
return data;
})
.finally(() => { refreshTokenPromise = null; });
}
return refreshTokenPromise.then(() => authFetch(url, options));
}
return res;
});
}
注意三点:
- 所有受保护请求必须走
authFetch(),不能混用原生fetch -
credentials: 'include'是关键,否则浏览器不带HttpOnlyCookie - 刷新成功后要清空旧
access_token内存变量,再重发原请求
服务端必须提供 X-Auth-Expiry 响应头,前端禁用 exp 字段做判断
JWT 的 exp 是客户端解码出来的,完全不可信。时钟漂移、服务器时间误差、动态续期策略都会让“本地判断过期”和“服务端真实状态”脱节。
正确做法是:每次响应都带 X-Auth-Expiry: "2026-05-18T16:23:45Z"(RFC 8941 格式),前端用这个时间戳决定是否提前刷新(比如提前 60 秒)。
容易被忽略的点:
- 服务端必须在每次返回
access_token时同步更新该 header,不能只在登录接口返回 - 前端不能解析 JWT payload 获取
exp,哪怕只是“参考”也不行——这是安全设计的分水岭 - 如果服务端没提供该 header,前端只能降级为轮询 /refresh 接口,但会增加无效请求量
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











