JWT过期后如何自动刷新而不让用户重新登录?
我用JWT做用户认证,token一小时过期。现在的问题是,用户操作到一半突然跳回登录页,体验太差了。我看别人说可以用refresh token自动续期,但具体怎么在前端安全地实现?
我试过在每次请求前检查token是否快过期,如果是就先调用刷新接口,但有时候还是会因为并发请求导致多个刷新同时发生,甚至拿到旧的access token。有没有靠谱的做法?
比如我的刷新逻辑大概是这样:
const refreshToken = async () => {
const res = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include' // refresh token在httpOnly cookie里
});
const data = await res.json();
localStorage.setItem('accessToken', data.accessToken);
return data.accessToken;
};
但多个API请求几乎同时触发这个函数,就会重复刷新,还可能覆盖掉最新的token。该怎么解决?
先说为什么会这样。假设页面加载时同时发了5个请求,每个请求都检测到token快过期了,然后各自调用refreshToken函数。这5个请求几乎同时到达服务器,服务器返回5个新的access token,但只有最后一个存进localStorage的是有效的,前面4个已经失效了。更糟糕的是,这4个请求用的可能是旧token,直接401。
解决思路是用一个全局的Promise来做"锁"。第一个请求触发刷新后,后续的请求不再发起刷新请求,而是等待第一个刷新完成,拿到同一个新token。这样不管多少并发请求,实际只刷新一次。
看代码,我给你写一个完整的封装:
使用的时候就很简单了:
这里有几个细节需要注意。第一,refresh token必须放在httpOnly cookie里,你做对了,这样可以防止XSS攻击窃取refresh token。第二,access token虽然放在localStorage,但可以考虑也用cookie,不过localStorage方便前端读取判断过期时间。
再说一个进阶的优化。如果你想在请求发出前就判断token快过期,主动刷新而不是等401,可以加个预刷新逻辑:
这样双重保障,主动刷新加被动刷新,基本不会出现用户突然掉线的情况。
最后提醒一个坑,后端的refresh token也要有过期时间和单次使用机制。每次刷新后旧的refresh token应该失效,防止被重放攻击。如果检测到已使用的refresh token再次被使用,说明可能被窃取,应该直接让所有该用户的token失效,强制重新登录。这个需要后端配合,前端控制不了。