在前后端分离的项目中,前端通常通过 accessToken 来访问业务接口。
但由于 accessToken 有有效期限制,过期后的处理方式,直接影响系统的安全性和用户体验。
本文将介绍一种 基于 Axios 响应拦截器 + 请求队列 的 Token 自动刷新方案,适用于实际生产环境。
核心目标只有三个:
项目中后端返回统一的数据结构:
{
code: number;
msg: string;
data: any;
}
在 Axios 的响应拦截器的 成功回调 中:
if (code === ApiCodeEnum.SUCCESS) {
return data;
}
// 业务错误,主动抛出
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
在 Axios 响应拦截器的 error 回调中:
async (error) => {
const { response, config } = error;
if (!response) {
ElMessage.error("网络连接失败");
return Promise.reject(error);
}
const { code, msg } = response.data;
switch (code) {
case ApiCodeEnum.ACCESS_TOKEN_INVALID:
return refreshTokenAndRetry(config, service);
case ApiCodeEnum.REFRESH_TOKEN_INVALID:
await redirectToLogin("登录已过期");
return Promise.reject(error);
default:
ElMessage.error(msg || "系统出错");
return Promise.reject(error);
}
};
如果多个接口同时返回 ACCESS_TOKEN_INVALID:
解决方案:请求队列 + 刷新锁
通过组合式函数 useTokenRefresh 实现:
let isRefreshingToken = false;
const pendingRequests = [];
async function refreshTokenAndRetry(config, httpRequest) {
return new Promise((resolve, reject) => {
const retryRequest = () => {
const newToken = AuthStorage.getAccessToken();
config.headers.Authorization = `Bearer ${newToken}`;
httpRequest(config).then(resolve).catch(reject);
};
pendingRequests.push({ resolve, reject, retryRequest });
if (!isRefreshingToken) {
isRefreshingToken = true;
useUserStoreHook()
.refreshToken()
.then(() => {
pendingRequests.forEach((req) => req.retryRequest());
pendingRequests.length = 0;
})
.catch(async () => {
pendingRequests.forEach((req) =>
req.reject(new Error("Token refresh failed"))
);
pendingRequests.length = 0;
await redirectToLogin("登录已失效");
})
.finally(() => {
isRefreshingToken = false;
});
}
});
}
在创建 Axios 函数中要提前初始化刷新函数
const { refreshTokenAndRetry } = useTokenRefresh();
isRefreshingTokenpendingRequestsimport type { InternalAxiosRequestConfig } from "axios";
import { useUserStoreHook } from "@/store/modules/user.store";
import { AuthStorage, redirectToLogin } from "@/utils/auth";
/**
* 等待请求的类型接口
*/
type PendingRequest = {
resolve: (_value: any) => void;
reject: (_reason?: any) => void;
retryRequest: () => void;
};
/**
* Token刷新组合式函数
*/
export function useTokenRefresh() {
// Token 刷新相关状态s
let isRefreshingToken = false;
const pendingRequests: PendingRequest[] = [];
/**
* 刷新 Token 并重试请求
*/
async function refreshTokenAndRetry(
config: InternalAxiosRequestConfig,
httpRequest: any
): Promise<any> {
return new Promise((resolve, reject) => {
// 封装需要重试的请求
const retryRequest = () => {
const newToken = AuthStorage.getAccessToken();
if (newToken && config.headers) {
config.headers.Authorization = `Bearer ${newToken}`;
}
httpRequest(config).then(resolve).catch(reject);
};
// 将请求加入等待队列
pendingRequests.push({ resolve, reject, retryRequest });
// 如果没有正在刷新,则开始刷新流程
if (!isRefreshingToken) {
isRefreshingToken = true;
useUserStoreHook()
.refreshToken()
.then(() => {
// 刷新成功,重试所有等待的请求
pendingRequests.forEach((request) => {
try {
request.retryRequest();
} catch (error) {
console.error("Retry request error:", error);
request.reject(error);
}
});
// 清空队列
pendingRequests.length = 0;
})
.catch(async (error) => {
console.error("Token refresh failed:", error);
// 刷新失败,先 reject 所有等待的请求,再清空队列
const failedRequests = [...pendingRequests];
pendingRequests.length = 0;
// 拒绝所有等待的请求
failedRequests.forEach((request) => {
request.reject(new Error("Token refresh failed"));
});
// 跳转登录页
await redirectToLogin("登录状态已失效,请重新登录");
})
.finally(() => {
isRefreshingToken = false;
});
}
});
}
return {
refreshTokenAndRetry,
};
}