Axios请求拦截器刷新token时触发无限循环问题咨询
问题产生的根本原因
两个核心逻辑错误共同触发了无限循环卡死问题:
- 你在拦截器分支中写的自执行async函数是异步非阻塞的,拦截器不会等待该函数执行完成,就会直接返回config发起请求。你在自执行函数内部修改
config.headers的操作完全无效,因为config已经被提前返回用于发起请求了。 - 你调用刷新token接口使用的是绑定了当前拦截器的同一个axios实例
instance,刷新token的请求本身也会被这个请求拦截器拦截。此时旧的过期access_token还没被新token替换,拦截器判断过期后会再次触发刷新token逻辑,无限递归调用刷新接口,最终页面卡死。 - 额外隐患:没有做刷新请求并发控制,当多个接口同时触发过期判断时,会同时发起多个无效的刷新token请求。
修复方案
按以下规则调整代码即可解决问题:
- 将请求拦截器的回调函数直接声明为
async函数,移除内部无意义的自执行异步IIFE,保证拦截器会等待token刷新完成后再返回请求配置 - 对刷新token的接口做特殊放行:要么在拦截器中判断到是
/refresh接口时直接跳过token校验逻辑,要么单独创建一个未绑定当前鉴权拦截器的axios实例专门用来发刷新token的请求,从根源避免递归触发拦截器 - 增加刷新状态锁和等待队列,避免token刷新期间重复发起刷新请求,同时保证刷新期间发起的其他接口能在拿到新token后正常携带鉴权头
- 修正刷新接口的传参格式:axios.post第二个参数为请求体,需和后端要求的参数格式对齐,不要直接传裸token字符串
修复后的核心代码参考:
// 新增刷新锁和等待队列 let isRefreshing = false; let pendingRequests = []; instance.interceptors.request.use( async (config) => { // 刷新接口直接放行,避免递归触发拦截器 if (config.url === '/refresh') { return config; } const accessToken = localStorage.getItem("access_token"); // 无token场景直接放行(比如登录接口) if (!accessToken) { return config; } const auth = jwt_decode(accessToken); const expireTime = auth.exp * 1000; const now = Date.now(); // token未过期,直接携带token放行 if (expireTime > now) { config.headers["Authorization"] = 'Bearer ' + accessToken; return config; } // token过期,处理刷新逻辑 if (!isRefreshing) { isRefreshing = true; try { const refreshToken = localStorage.getItem("refresh_token"); // 按后端要求传参,示例为常见的JSON格式传参 const response = await instance.post("/refresh", { refreshToken }); const { accessToken: newAccessToken, refreshToken: newRefreshToken } = response.data; // 更新本地存储的token localStorage.setItem("access_token", newAccessToken); localStorage.setItem("refresh_token", newRefreshToken); setAuth({ token: newAccessToken, refreshToken: newRefreshToken }); // 执行等待队列中的请求,替换为新token pendingRequests.forEach(cb => cb(newAccessToken)); pendingRequests = []; // 给当前请求携带新token放行 config.headers["Authorization"] = 'Bearer ' + newAccessToken; return config; } catch (err) { // 刷新失败(比如refresh_token也过期),清空本地存储跳转登录 localStorage.removeItem("access_token"); localStorage.removeItem("refresh_token"); // 此处补充你的跳转登录逻辑,例如 router.push('/login') return Promise.reject(err); } finally { isRefreshing = false; } } else { // 正在刷新token,将当前请求加入等待队列 return new Promise(resolve => { pendingRequests.push((newToken) => { config.headers["Authorization"] = 'Bearer ' + newToken; resolve(config); }) }) } }, (error) => { return Promise.reject(error); } ); // 调整刷新方法的传参逻辑,和后端格式对齐即可 const refreshAccessToken = async (token) => { return instance.post("/refresh", { refreshToken: token }); };
内容的提问来源于stack exchange,提问作者Alpharius
相关产品推荐
相关产品推荐

