
当 token 过期触发 401 响应时,若刷新令牌请求本身也被同一拦截器捕获,就会反复重试导致无限循环;根本解法是用独立的、无响应拦截器的 axios 实例处理刷新请求。
当 token 过期触发 401 响应时,若刷新令牌请求本身也被同一拦截器捕获,就会反复重试导致无限循环;根本解法是用独立的、无响应拦截器的 axios 实例处理刷新请求。
在使用 Axios 拦截器实现自动 Token 刷新时,一个常见但隐蔽的陷阱是:刷新令牌的请求(如 /auth/refresh-token)被当前配置了响应拦截器的 axiosInstance 自身发起,从而再次进入 response.interceptor 的错误处理逻辑。由于该请求也返回 401(例如因 Refresh Token 无效或过期),且未标记 _retry,就会触发新一轮 refreshAccessToken() 调用——形成「请求失败 → 触发刷新 → 刷新请求又被拦截 → 再次失败 → 再次触发刷新」的无限递归,最终导致页面卡死或重定向失效。
? 核心问题定位
你代码中的关键隐患在于:
const response = await axiosInstance.get(REFRESH_TOKEN_ENDPOINT); // ❌ 危险!
axiosInstance 已配置了 response.interceptor,而 REFRESH_TOKEN_ENDPOINT 请求一旦失败(如返回 401),就会再次进入 interceptors.response.use(...) 的 error 分支,进而重复调用 refreshAccessToken() —— 此时 originalRequest._retry 对刷新请求本身不生效(因它是新请求),循环就此产生。
✅ 正确解决方案:隔离刷新请求
必须确保刷新 Token 的请求完全绕过当前拦截器链。最佳实践是创建一个纯净的、无任何自定义拦截器的 Axios 实例专用于认证相关操作:
// authApi.js —— 纯净实例,仅用于认证接口
import axios from 'axios';
import { REFRESH_TOKEN_ENDPOINT } from '../constants/apiUrlConst';
// ✅ 关键:不添加任何 request/response interceptor
export const authApi = axios.create({
baseURL: process.env.REACT_APP_API_URL || '/api',
timeout: 10000,
});
然后在 refreshAccessToken 中使用它:
import { authApi } from './authApi'; // 引入纯净实例
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '../constants/tokenConst';
import { logout } from '../stores/actions/authActions';
import store from '../stores';
const refreshAccessToken = async () => {
const refresh_token = localStorage.getItem(REFRESH_TOKEN_KEY);
if (!refresh_token) {
store.dispatch(logout());
localStorage.clear();
window.location.href = '/';
throw new Error('No refresh token available');
}
try {
// ✅ 使用无拦截器的实例,彻底避免循环
const response = await authApi.get(REFRESH_TOKEN_ENDPOINT);
const { access_token, refresh_token: newRefreshToken } = response.data;
localStorage.setItem(ACCESS_TOKEN_KEY, access_token);
localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken);
return access_token;
} catch (error) {
// 刷新失败:清理状态并强制登出
store.dispatch(logout());
localStorage.clear();
window.location.href = '/login';
throw error; // 确保上层能捕获
}
};
同时,务必确保主 axiosInstance 的请求拦截器正确跳过刷新接口的 Token 注入(你已做到),且响应拦截器中只对非刷新请求启用重试:
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error;
// ✅ 仅对非刷新请求且未重试过的情况触发刷新
if (
response?.status === 401 &&
!config._retry &&
config.url !== REFRESH_TOKEN_ENDPOINT // 额外保险:显式排除
) {
config._retry = true;
try {
const newToken = await refreshAccessToken();
config.headers.Authorization = `Bearer ${newToken}`;
return axiosInstance(config); // 重发原始请求
} catch (e) {
return Promise.reject(e);
}
}
return Promise.reject(error);
}
);
⚠️ 注意事项与最佳实践
- 永远不要在刷新逻辑中使用带响应拦截器的实例:这是循环根源,无例外。
- 添加 _retry 标记 + URL 排除双重防护:防止极端场景下误触发。
- 刷新失败必须彻底清理状态:清除 localStorage、dispatch logout、跳转登录页,避免残留无效 Token。
- 服务端配合:确保 /refresh-token 接口不校验 Authorization 头,仅验证 Refresh Token 本身有效性。
- 考虑并发请求处理:若多个请求几乎同时 401,可引入 Promise 缓存机制(如 let pendingRefresh = null),避免多次并发刷新。
通过将认证流程与业务请求完全解耦,即可一劳永逸地消除无限循环,让 Token 刷新健壮、可预测、易调试。











