前端基于Axios实现Token刷新时拦截延迟API请求及多标签页场景的方案咨询
你这个需求太典型了!很多做前端鉴权的同学都会遇到——Token过期刷新时,不想让用户看到一堆错误提示,还要兼顾多标签页的场景,确实比一开始想的要复杂,但其实有成熟的方案,不管是手写实现还是用现成库都能搞定。
一、先解决单标签页的请求拦截与延迟问题
你想到的请求队列思路是完全正确的,但不用手动取消请求再重发,结合Axios的拦截器和Promise就能优雅实现,核心是用一个刷新状态标记和请求队列来控制:
定义全局状态:
- 用变量
isRefreshing标记是否正在刷新Token,初始为false - 用数组
pendingRequests存储等待刷新完成后重新发起的请求(每个元素是Promise的resolve函数)
- 用变量
响应拦截器里处理Token过期:
当收到401响应时,先判断是否为Token过期(可结合后端返回的错误标识,比如error.response.data.code === 'TOKEN_EXPIRED'):- 如果
isRefreshing为false:- 将
isRefreshing设为true,发起刷新Token的请求 - 刷新成功后,更新本地的accessToken,遍历
pendingRequests逐个调用resolve,重新发起队列中的请求 - 刷新失败则跳转到登录页,清空队列
- 将
- 如果
isRefreshing为true:- 将当前请求包装成Promise,加入
pendingRequests队列,等待刷新完成后再执行
- 将当前请求包装成Promise,加入
- 如果
给个简化的代码示例:
import axios from 'axios'; let isRefreshing = false; let pendingRequests = []; const api = axios.create({ baseURL: '/api' }); // 响应拦截器 api.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 排除刷新Token的请求本身,避免循环触发 if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; if (!isRefreshing) { isRefreshing = true; try { // 调用后端刷新Token接口 const refreshResponse = await api.post('/auth/refresh', { refreshToken: localStorage.getItem('refreshToken') }); const newAccessToken = refreshResponse.data.accessToken; // 更新本地Token与Axios默认请求头 localStorage.setItem('accessToken', newAccessToken); api.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; // 重新发起队列中所有等待的请求 pendingRequests.forEach(resolve => resolve(newAccessToken)); pendingRequests = []; } catch (refreshError) { // 刷新失败,跳转登录页 window.location.href = '/login'; pendingRequests = []; } finally { isRefreshing = false; } } // 返回Promise,等待刷新完成后再发起原请求 return new Promise(resolve => { pendingRequests.push(token => { originalRequest.headers['Authorization'] = `Bearer ${token}`; resolve(api(originalRequest)); }); }); } return Promise.reject(error); } );
二、多标签页场景的处理
你提到的BroadcastChannel确实是这个场景的最佳方案之一,它能实现同源不同标签页之间的通信:
- 创建BroadcastChannel实例:
const authChannel = new BroadcastChannel('auth_channel');
在刷新Token的逻辑中添加广播:
当一个标签页开始刷新Token时,发送广播通知其他标签页“正在刷新Token”;刷新成功后,再发送“Token已更新”的广播,其他标签页收到后更新自身的Token即可。其他标签页监听广播:
authChannel.addEventListener('message', (event) => { if (event.data.type === 'REFRESHING_TOKEN') { // 标记当前标签页处于等待状态,避免重复发起刷新请求 isRefreshing = true; } else if (event.data.type === 'TOKEN_UPDATED') { // 更新本地Token与Axios请求头 localStorage.setItem('accessToken', event.data.accessToken); api.defaults.headers.common['Authorization'] = `Bearer ${event.data.accessToken}`; isRefreshing = false; // 重新发起当前标签页中等待的请求 pendingRequests.forEach(resolve => resolve(event.data.accessToken)); pendingRequests = []; } });
这样就能避免多个标签页同时发起刷新Token的请求,保证只有一个标签页执行刷新操作,其他标签页等待更新即可。
三、有没有现成的库可以直接用?
当然有!专门针对Axios的axios-auth-refresh库已经封装了请求队列、Token刷新的核心逻辑,你只需要配置刷新Token的函数就能快速落地,省去手写大量代码的麻烦。
用法示例:
import axios from 'axios'; import createAuthRefreshInterceptor from 'axios-auth-refresh'; const api = axios.create({ baseURL: '/api' }); // 定义刷新Token的逻辑函数 const refreshAuthLogic = async (failedRequest) => { const refreshToken = localStorage.getItem('refreshToken'); const response = await axios.post('/auth/refresh', { refreshToken }); const newAccessToken = response.data.accessToken; localStorage.setItem('accessToken', newAccessToken); failedRequest.response.config.headers['Authorization'] = `Bearer ${newAccessToken}`; return Promise.resolve(); }; // 给Axios实例添加刷新拦截器 createAuthRefreshInterceptor(api, refreshAuthLogic);
这个库已经处理了请求队列、避免重复刷新的问题,如果你需要多标签页支持,再结合上面的BroadcastChannel逻辑即可。
最后总结
其实你没把问题想复杂,这个场景本来就需要这些细节处理——核心就是「控制刷新状态」+「缓存等待请求」+「跨标签通信」。手写实现能灵活定制,用现成库能快速落地,完全可以根据你的项目需求选择合适的方案~
备注:内容来源于stack exchange,提问作者George

