JWT刷新时旧token未及时回收导致重复登录怎么解决?
我在用JWT做登录鉴权时遇到个问题,用户在A设备刷新token后,旧token居然还能在B设备正常登录,这样用户明明退出了怎么还会被绕过?
我的实现逻辑是这样的:用户登录成功后存储token到localStorage,刷新接口会返回新token覆盖旧值。但测试时发现:
// 刷新token的axios拦截器
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
return refreshAccessToken().then(newToken => {
localStorage.setItem('token', newToken); // 直接覆盖旧token
error.config.headers['Authorization'] = <code>Bearer ${newToken}</code>;
return axios(error.config);
});
}
return Promise.reject(error);
}
);
尝试过在服务端设置shortToken+refreshToken机制,但好像没生效?用户退出登录时清除localStorage,但别人用抓包工具复制之前的旧token,居然还能访问接口。是不是应该让服务端主动作废旧token?或者我的刷新逻辑哪里漏了?
JWT的设计初衷就是无状态,但这也带来了无法主动作废旧token的问题。要解决这个问题,不能只靠客户端清除localStorage,因为那只是前端的操作,攻击者完全可以绕过前端直接调用接口。
解决方案的核心思路是:引入服务端的token状态管理,哪怕只管一小部分关键状态。
我推荐你采用“黑名单机制”配合合理的过期时间控制。具体分下面几步来做:
第一步,在用户刷新token时,把旧token加到服务端的黑名单里,并设置一个过期时间等于原token剩余有效期。比如你的access token有效期是15分钟,那么当用户刷新时,就把旧token加入黑名单并缓存15分钟。
你可以用Redis来实现这个黑名单,key可以用token的jti(JWT ID)或者token本身的哈希值,value可以存个标记或时间戳,过期自动删除。
第二步,修改你的认证中间件,在验证JWT签名和过期时间之后,再查一下这个token是否在黑名单里。如果在,就拒绝请求。
下面是Node.js + Express的一个简单示例:
第三步,刷新token的逻辑要做调整。不要只在前端覆盖,而是在服务端明确处理新老token的关系。
第四步,前端的拦截器也要改,不能只等401才刷新。因为你现在有黑名单机制了,即使旧token没过期也会被拒绝,所以应该更积极地使用refresh token流程。
最后说下原理:JWT之所以难回收,是因为它不依赖服务端状态。但我们可以通过增加一个轻量级的状态层(黑名单)来打破完全无状态的限制,只对最近一次被淘汰的token做短期追踪。这样既保留了JWT大部分性能优势,又解决了安全漏洞。
补充建议:
- access token有效期别设太长,15分钟比较合理
- refresh token可以长一点,比如7天,但也要支持服务端主动废除(比如退出登录时删掉refresh token记录)
- 所有敏感操作最好再加上二次验证,比如修改密码时要输入当前密码
你现在的问题本质是“只做了客户端清理,没做服务端状态控制”,补上黑名单这一步就完整了。这套方案我们线上用了很久,效果不错。
你说得对,**服务端需要主动作废旧 Token**,否则抓包拿到的旧 Token 还能继续用,直到它自然过期。这不是前端覆盖 localStorage 就能解决的事。
### 解决方案:Token 黑名单(黑名单机制)
1. **服务端维护一个 Token 黑名单**,比如用 Redis 存,Key 是 Token 的 JTI(或 Hash),Value 是 Token 剩余有效期。
2. 每次用户刷新 Token,**旧 Token 放入黑名单**。
3. 用户退出登录时,当前 Token 也加入黑名单。
4. 每次请求进入接口前,**先校验 Token 是否在黑名单中**,命中则拒绝访问。
### 刷新逻辑补充建议
你现在的拦截器逻辑只是前端替换 localStorage,但没有通知服务端回收旧 Token。建议在刷新 Token 成功后,**调用一个接口通知服务端作废旧 Token**,或者干脆让刷新接口内部自动把旧 Token 加入黑名单。
### 示例代码(刷新 Token 时通知服务端):
### 总结一下
- 前端 localStorage 替换 Token,只是更新了本地状态;
- 真正要让旧 Token 无效,必须服务端加黑名单机制;
- 这样哪怕抓包拿到了旧 Token,也会被服务端拦截;
- Token 黑名单建议用 Redis 实现,效率高,过期自动清理。
你要是用 JWT 又想强控制登录状态,这一步是绕不过去的。别想着“无状态”能解决所有问题,这玩意儿就得加点“状态”。