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

如何用Redux-Thunk统一处理401错误并避免重复刷新Token?

这确实是前后端分离项目里非常常见的痛点——既要避免每个请求都写重复的401处理逻辑,又要防止并发请求触发多次Token刷新。我来给你一步步拆解解决方案:

一、用Axios拦截器统一处理401错误

与其在每个请求的catch里手动判断401,不如利用Axios的响应拦截器,把全局的错误处理逻辑抽离出来。这样所有请求的401都会被自动捕获,不用重复写代码。

步骤1:创建Axios实例并配置拦截器

先封装一个Axios实例,在响应拦截器里处理401:

import axios from 'axios';
import store from '../store'; // 导入你的Redux store
import { refreshAuth } from '../actions/authActions'; // 导入你的刷新action

// 创建Axios实例
const apiClient = axios.create({
  baseURL: API.path,
  params: API.beaconAPI_client // 把公共参数提前配置好
});

// 响应拦截器
apiClient.interceptors.response.use(
  response => response, // 请求成功直接返回
  async error => {
    const originalRequest = error.config;
    
    // 捕获401,并且排除刷新Token本身的请求(避免死循环)
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true; // 标记该请求已经重试过,防止无限循环
      
      try {
        // 触发Redux的刷新Token action
        await store.dispatch(refreshAuth());
        
        // 获取刷新后的新Token
        const newToken = store.getState().login.accessToken; // 假设你的Token存在login状态里
        
        // 给原请求设置新的Authorization头
        originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
        
        // 重试原请求
        return apiClient(originalRequest);
      } catch (refreshError) {
        // 如果刷新Token也失败了,比如refreshToken过期,就跳转到登录页
        // 这里可以写你的登录跳转逻辑,比如history.push('/login')
        return Promise.reject(refreshError);
      }
    }
    
    // 非401错误或者已经重试过,直接抛出错误
    return Promise.reject(error);
  }
);

export default apiClient;

步骤2:修改你的refreshAuth Action

把原来的refreshAuth调整一下,确保返回一个完整的Promise,这样拦截器里的await能正确等待刷新完成:

export const refreshAuth = () => {
  return (dispatch, getState) => {
    // 直接返回Axios的Promise,让拦截器可以await
    return axios.post(
      `${API.path}auth/refresh/?${API.beaconAPI_client}`,
      { refreshToken: getState().login.refreshToken }
    ).then(response => {
      dispatch({ 
        type: DO_LOGIN_REFRESH, 
        userDetails: response.data 
      });
      // 返回新的Token,方便拦截器直接使用(可选)
      return response.data.accessToken;
    }).catch(error => {
      // 刷新失败时,比如refreshToken过期,这里可以清空登录状态
      dispatch({ type: LOGOUT });
      return Promise.reject(error);
    });
  };
};

这样一来,所有用apiClient发起的请求,遇到401都会自动触发Token刷新,然后重试原请求,完全不用在每个请求里写重复的catch逻辑。


二、解决并发请求重复刷新Token的问题

上面的方案在单请求401时没问题,但如果多个请求同时返回401,就会触发多次refreshAuth,导致后端收到多次刷新请求,甚至可能出现Token被覆盖的问题。解决思路是加一个“刷新锁”:当正在刷新Token时,其他等待的请求都复用同一个刷新Promise,不用重复发起刷新请求。

步骤1:添加全局刷新锁变量

在Axios实例的文件里,定义一个全局变量来存储当前的刷新Promise:

import axios from 'axios';
import store from '../store';
import { refreshAuth } from '../actions/authActions';

const apiClient = axios.create({ /* ... 你的配置 ... */ });

// 全局变量:存储当前正在进行的刷新Token的Promise
let refreshTokenPromise = null;

// 响应拦截器
apiClient.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      // 如果已经有正在进行的刷新请求,直接复用这个Promise
      if (!refreshTokenPromise) {
        refreshTokenPromise = store.dispatch(refreshAuth())
          .finally(() => {
            // 刷新完成后清空Promise,释放锁
            refreshTokenPromise = null;
          });
      }
      
      try {
        // 等待刷新完成,获取新Token
        const newToken = await refreshTokenPromise;
        originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // 刷新失败,跳登录页
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

export default apiClient;

原理说明

  • 第一个触发401的请求会创建refreshTokenPromise,并发起刷新请求;
  • 后续的并发401请求会发现refreshTokenPromise已经存在,就直接等待这个Promise完成,而不是重新发起刷新;
  • 刷新完成后,finally会清空refreshTokenPromise,释放锁,确保下一次401能正常触发刷新。

这样就完美解决了并发请求重复刷新Token的问题。


最后,所有请求都用这个封装好的apiClient发起,就能实现全局统一处理401和避免重复刷新Token的效果啦!

内容的提问来源于stack exchange,提问作者Matt Saunders

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 10:00:01