使用axios-auth-refresh处理refresh token时多请求重复触发刷新如何解决
问题核心原因
你的api_client被定义为每次调用都会创建全新axios实例的函数,retrieve/send方法每次执行都会生成独立的axios实例,实例之间的拦截器、刷新状态锁、请求队列完全隔离,自然无法实现「单次刷新、队列等待」的逻辑,并发请求时每个实例都会单独触发刷新。
同时你还存在两个隐性bug:
- 刷新token请求使用了加了401拦截的client实例发起,若刷新接口返回401会触发死循环
- cookie过期时间配置错误,
new Date(Date.now() + 259200)仅比当前时间多259秒(约4分钟),实际要3天的话需要乘以1000转成毫秒
原生拦截器版本修复方案
将axios实例、刷新状态、队列都改为全局单例,避免重复创建实例:
import axios from 'axios'; import axiosRetry from 'axios-retry'; import config from 'config'; import {isNull, isUndefined} from 'underscore'; import Cookies from 'universal-cookie'; const cookies = new Cookies(); // 全局唯一axios实例 const client = axios.create({baseURL: config.server}); let isRefreshing = false; let failedQueue = []; const retryCodes = [401] axiosRetry(client, { retries: 3, retryDelay: (retryCount) => retryCount * 1000, retryCondition: (error) => retryCodes.includes(error?.response?.status) }); // 请求拦截器 client.interceptors.request.use(request => { const cookieToken = cookies.get('token'); const cookieUser = cookies.get('user'); if(!isNull(cookieToken) && !isNull(cookieUser) && !isUndefined(cookieToken.token)) { request.headers['Authorization'] = `Bearer ${cookieToken.token}`; } request.withCredentials = true; request.headers['Content-Type'] = 'application/json' request.headers['X-Fname'] = cookieUser?.username; return request; }, error => Promise.reject(error)); // 队列处理 const processQueue = (error, token = null) => { failedQueue.forEach(prom => error ? prom.reject(error) : prom.resolve(token)) failedQueue = []; } // 刷新token方法,用无拦截的axios发起请求 const getRefreshAuthToken = () => { const cookieToken = cookies.get('token'); const cookieUser = cookies.get('user'); if((isNull(cookieToken) || isNull(cookieUser)) || isUndefined(cookieToken.refreshToken)) { return Promise.reject('无可用刷新token'); } return axios.post(`${config.server}/user/refresh?refreshToken=${cookieToken.refreshToken}`); } // 响应拦截器 client.interceptors.response.use(response => response, error => { const originalRequest = error.config; if (error?.response?.status !== 401 || originalRequest._retry) { return Promise.reject(error); } // 已在刷新中,加入队列等待 if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }) }).then(token => { originalRequest.headers['Authorization'] = `Bearer ${token}`; return client(originalRequest); }).catch(err => Promise.reject(err)); } // 首次触发刷新 originalRequest._retry = true; isRefreshing = true; return new Promise((resolve, reject) => { getRefreshAuthToken() .then(response => { const {token, refreshToken} = response.data; // 更新cookie cookies.remove("token", { domain: window.location.hostname, path: "/grt" }); cookies.remove("token", { domain: window.location.hostname, path: "/" }); cookies.set("token", { token, refreshToken }, { domain: window.location.hostname, path: "/grt", expires: new Date(Date.now() + 259200 * 1000) // 修正毫秒单位 }); // 处理队列并重试当前请求 processQueue(null, token); originalRequest.headers['Authorization'] = `Bearer ${token}`; resolve(client(originalRequest)); }).catch(err => { processQueue(err, null); reject(err); }).finally(() => { isRefreshing = false }); }); }) // 导出请求方法,复用全局唯一实例 export const retrieve = async (url, data = undefined) => { const response = await client.get(url, data); return response.data; } export const send = async (url, data = undefined) => { const response = await client.post(url, data, { skipAuthRefresh: true }); return response.data; } export default {retrieve, send};
axios-auth-refresh版本修复方案
同样改为单例实例,同时共享刷新请求Promise避免重复发起:
import axios from 'axios'; import axiosRetry from 'axios-retry'; import config from 'config'; import {isNull, isUndefined} from 'underscore'; import Cookies from 'universal-cookie'; import createAuthRefreshInterceptor from 'axios-auth-refresh'; const cookies = new Cookies(); const client = axios.create({baseURL: config.server}); // 缓存刷新请求Promise,避免重复发起 let refreshRequest = null; client.interceptors.request.use(request => { const cookieToken = cookies.get('token'); const cookieUser = cookies.get('user'); if(!isNull(cookieToken) && !isNull(cookieUser) && !isUndefined(cookieToken.token)) { request.headers['Authorization'] = `Bearer ${cookieToken.token}`; } request.withCredentials = true; request.headers['Content-Type'] = 'application/json' request.headers['X-Fname'] = cookieUser?.username; return request; }, error => Promise.reject(error)); const getRefreshAuthToken = () => { if (refreshRequest) return refreshRequest; const cookieToken = cookies.get('token'); const cookieUser = cookies.get('user'); if((isNull(cookieToken) || isNull(cookieUser)) || isUndefined(cookieToken.refreshToken)) { return Promise.reject('无可用刷新token'); } refreshRequest = axios.post(`${config.server}/user/refresh?refreshToken=${cookieToken.refreshToken}`); refreshRequest.finally(() => refreshRequest = null); return refreshRequest; } const handleRefreshAuth = failedRequest => { return getRefreshAuthToken().then(response => { const {token, refreshToken} = response.data; cookies.remove("token", { domain: window.location.hostname, path: "/grt" }); cookies.remove("token", { domain: window.location.hostname, path: "/" }); cookies.set("token", { token, refreshToken }, { domain: window.location.hostname, path: "/grt", expires: new Date(Date.now() + 259200 * 1000) }); failedRequest.response.config.headers['Authorization'] = `Bearer ${token}`; return Promise.resolve(); }) } const options = { pauseInstanceWhileRefreshing: true, skipWhileRefreshing: false } createAuthRefreshInterceptor(client, handleRefreshAuth, options); export const retrieve = async (url, data = undefined) => { const response = await client.get(url, data); return response.data; } export const send = async (url, data = undefined) => { const response = await client.post(url, data, { skipAuthRefresh: true }); return response.data; } export default {retrieve, send};
内容的提问来源于stack exchange,提问作者Alvin Quezon
相关产品推荐
相关产品推荐

