Axios搭配.NET5 WebAPI的JWT令牌刷新及请求重试问题
Axios JWT令牌自动刷新拦截器异常修正方案
问题背景
现有技术栈如下:
- 前端为集成Redux的React应用,通过Axios与.NET 5 Web API后端通信
- 身份认证采用JWT access token + refresh token双令牌机制
- 服务端令牌生成逻辑基于aspnet-core-3-jwt-refresh-tokens-api开源实现,与参考代码差异极小
当前存在的问题:access token过期触发无响应的Network Error(该场景下的典型报错)时,Axios响应拦截器未按预期执行刷新令牌、重试原请求的逻辑,而是直接抛出错误。预期实现逻辑为:请求触发无响应的Network Error时,自动调用刷新令牌接口获取新令牌,之后重试此前失败的原请求。
现有代码实现
应用内所有业务API请求均使用自定义authApi Axios实例发起,自动在请求头携带Bearer令牌,代码如下:
const authApi = axios.create({ baseURL : API_URL }); authApi.interceptors.request.use((config) => { store.dispatch({type: REQUEST_STARTED}) return setBearerToken(config); }); authApi.interceptors.response.use( (res) => { return res; }, (err) => { return onRejected(err); } ); function setBearerToken (config){ const state = store.getState(); // 读取redux store config.headers['Authorization'] = `Bearer ${state ? state.auth.token : null}`; return config; } async function onRejected(err){ if (!err.config) return Promise.reject(err); if (err.config.url !== "/login") { if (err.response) { return handleErrorWithResponse(err); } else { return handleErrorWithoutResponse(err); } } return Promise.reject(err); } function handleErrorWithResponse(err) { const originalConfig = err.config; if (err.response.status === 401 && !originalConfig._retry) { return refreshAccessTokenThenRetry(originalConfig); } // 无关代码已省略 return Promise.reject(err); } // 该处理函数当前会直接把错误抛到前端 function handleErrorWithoutResponse(err) { if (err.isAxiosError && err.message === 'Network Error') { return refreshAccessTokenThenRetry(err.config); } return Promise.reject(err); } async function refreshAccessTokenThenRetry(config) { config._retry = true; try { await Authenticationservice.refreshAccessToken(); return baseApi(config); } catch (error) { return Promise.reject(error); } }
对应的认证服务代码如下,注意刷新令牌接口使用了独立的baseApi Axios实例:
class AuthenticationService { refreshAccessToken = async() => { const state = store.getState(); const rs = await baseApi.post("/users/refreshtoken", { refreshToken: state.auth.refreshToken, }); const accessAndRefreshTokens = rs.data; store.dispatch({ type : NEW_ACCESS_TOKEN, payload : accessAndRefreshTokens }); } }
问题根因
现有实现存在两个核心问题:
- 重试请求使用了错误的Axios实例:
refreshAccessTokenThenRetry函数中重试原请求时调用的是baseApi实例,而非原请求使用的authApi。baseApi既没有配置和authApi一致的baseURL、请求拦截器逻辑,也不会自动为请求附加刷新后拿到的新access token,甚至可能因为实例配置差异直接触发Network Error,导致错误直接抛到前端。 - 缺失并发请求锁与等待队列:如果页面同时发起多个请求,且token恰好在请求时过期,会同时触发多次刷新令牌接口调用。多数后端刷新令牌逻辑会在调用成功后作废旧refresh token,并发调用会导致除第一次之外的刷新请求全部失败,最终触发异常登出。
注:刷新令牌使用独立baseApi实例的设计是正确的——如果用authApi调用刷新接口,会被请求拦截器自动附加已经过期的access token,反而可能导致刷新请求被后端拦截,这部分不需要修改。
修正方案
第一步:新增全局刷新锁与等待队列
在Axios实例定义的同级作用域新增两个变量,处理并发请求场景:
// 令牌刷新状态锁,避免重复调用刷新接口 let isRefreshing = false; // 令牌刷新期间的等待请求队列 let failedRequestQueue = [];
第二步:修正错误处理逻辑
修改两个错误处理函数,增加重试标记判断与等待队列逻辑:
function handleErrorWithResponse(err) { const originalConfig = err.config; if (err.response.status === 401 && !originalConfig._retry) { // 正在刷新时将请求加入队列 if (isRefreshing) { return new Promise(resolve => { failedRequestQueue.push((newToken) => { originalConfig.headers.Authorization = `Bearer ${newToken}`; resolve(authApi(originalConfig)); }) }) } return refreshAccessTokenThenRetry(originalConfig); } return Promise.reject(err); } function handleErrorWithoutResponse(err) { const originalConfig = err.config; if (err.isAxiosError && err.message === 'Network Error' && !originalConfig._retry) { // 正在刷新时将请求加入队列 if (isRefreshing) { return new Promise(resolve => { failedRequestQueue.push((newToken) => { originalConfig.headers.Authorization = `Bearer ${newToken}`; resolve(authApi(originalConfig)); }) }) } return refreshAccessTokenThenRetry(originalConfig); } return Promise.reject(err); }
第三步:修正令牌刷新重试逻辑
修改refreshAccessTokenThenRetry函数,使用正确的authApi实例重试,同时处理队列请求与异常场景:
async function refreshAccessTokenThenRetry(config) { config._retry = true; isRefreshing = true; try { await Authenticationservice.refreshAccessToken(); // 从Redux取刷新后的最新令牌 const latestState = store.getState(); const newToken = latestState.auth.token; // 更新当前重试请求的认证头 config.headers.Authorization = `Bearer ${newToken}`; // 执行队列中所有等待的请求 failedRequestQueue.forEach(callback => callback(newToken)); failedRequestQueue = []; // 必须使用原请求所属的authApi实例发起重试 return authApi(config); } catch (error) { // 刷新失败(如refresh token过期)时清空队列,执行登出逻辑 failedRequestQueue = []; store.dispatch({type: LOGOUT}); // 替换为项目实际的登出action return Promise.reject(error); } finally { isRefreshing = false; } }
修正后即可实现预期效果:无论是401响应还是token过期触发的Network Error,都会自动触发令牌刷新,拿到新令牌后自动重试原请求,同时兼容并发请求场景,不会出现重复刷新、错误抛出的问题。
内容的提问来源于stack exchange,提问作者Scott
相关产品推荐
相关产品推荐

