如何用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
相关产品推荐
相关产品推荐

