当 Token 过期触发 401 响应时,若刷新令牌请求(/refresh-token)本身也被同一 axiosInstance 拦截并附带失效的 Authorization 头,就会导致递归调用、无限重试。本文详解根本原因及安全可靠的解决方案。
当 token 过期触发 401 响应时,若刷新令牌请求(`/refresh-token`)本身也被同一 `axiosinstance` 拦截并附带失效的 authorization 头,就会导致递归调用、无限重试。本文详解根本原因及安全可靠的解决方案。
在你提供的代码中,无限循环的根本原因在于:刷新令牌请求(GET /refresh-token)被同一个 axiosInstance 发起,而该实例的请求拦截器会自动为其添加 Authorization: Bearer
关键问题出现在这一行:
const response = await axiosInstance.get(REFRESH_TOKEN_ENDPOINT);
你使用了 被拦截的 axiosInstance 去请求刷新接口,而它的请求拦截器会为所有请求(包括 /refresh-token)注入 token —— 即使逻辑上该请求本应只携带 refresh token(且不应带 access token)。
✅ 正确解法:为刷新令牌请求单独创建一个“纯净”的 axios 实例(不挂载任何认证拦截器),确保它完全绕过认证逻辑,仅用于内部 token 刷新。
以下是优化后的完整实现:
import axios from "axios";
import { logout } from "../stores/actions/authActions";
import store from "../stores";
import userEnv from "userEnv";
import { REFRESH_TOKEN_ENDPOINT } from "../constants/apiUrlConst";
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "../constants/tokenConst";
// ✅ 纯净实例:仅用于刷新 token,不带任何请求/响应拦截器
const refreshAxios = axios.create({
baseURL: userEnv.apiUrl,
});
// 主 axios 实例(带拦截器)
const axiosInstance = axios.create({
baseURL: userEnv.apiUrl,
});
// ✅ 请求拦截器:跳过 refresh endpoint 的 access token 注入
axiosInstance.interceptors.request.use(
(config) => {
const url = config.url.toLowerCase();
const method = config.method.toLowerCase();
// 只对非 refresh 接口注入 access token
if (url !== REFRESH_TOKEN_ENDPOINT.toLowerCase() || method !== "get") {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
// ✅ refresh 请求显式使用 refresh token(如后端要求)
if (url === REFRESH_TOKEN_ENDPOINT.toLowerCase() && method === "get") {
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
if (refreshToken) {
config.headers["X-Refresh-Token"] = refreshToken; // 或按后端约定传递
}
}
return config;
},
(error) => Promise.reject(error)
);
// ✅ 响应拦截器:使用纯净实例发起刷新,避免循环
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// ? 使用纯净实例(无拦截器)发起刷新请求
const response = await refreshAxios.get(REFRESH_TOKEN_ENDPOINT);
const { access_token, refresh_token } = response.data;
localStorage.setItem(ACCESS_TOKEN_KEY, access_token);
localStorage.setItem(REFRESH_TOKEN_KEY, refresh_token);
// 重发原始请求
return axiosInstance(originalRequest);
} catch (refreshError) {
// 刷新失败 → 清理状态并跳转登录页
store.dispatch(logout());
localStorage.clear();
window.location.href = "/";
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default axiosInstance;
? 关键注意事项:
- ❌ 不要使用 axiosInstance 自身调用刷新接口;✅ 必须使用无拦截器的独立实例(如 refreshAxios);
- ✅ 在请求拦截器中主动排除 REFRESH_TOKEN_ENDPOINT,避免错误注入 access token;
- ✅ 若后端需 refresh token 认证,应通过 headers(如 X-Refresh-Token)显式传递,而非依赖全局 Authorization;
- ✅ 务必设置 _retry = true 防止多次重试,并在刷新失败时彻底退出流程(清空 storage + 跳转);
- ⚠️ refreshAxios 实例不可复用主实例的拦截器,否则问题重现。
此方案彻底解耦认证流与刷新流,兼顾安全性与健壮性,是企业级前端 Token 管理的标准实践。











