You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 });
    }
}

问题根因

现有实现存在两个核心问题:

  1. 重试请求使用了错误的Axios实例:refreshAccessTokenThenRetry函数中重试原请求时调用的是baseApi实例,而非原请求使用的authApi。baseApi既没有配置和authApi一致的baseURL、请求拦截器逻辑,也不会自动为请求附加刷新后拿到的新access token,甚至可能因为实例配置差异直接触发Network Error,导致错误直接抛到前端。
  2. 缺失并发请求锁与等待队列:如果页面同时发起多个请求,且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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 02:09:39