Files
AI_OJ_FRONTEND/src/plugins/axios.ts
meowrain 8e5558d9a2 feat(auth): 实现认证API和token自动刷新功能
添加认证服务API模块,包括登录、token刷新和验证功能
在axios拦截器中实现token自动刷新机制,处理401错误
更新tsconfig配置以支持ES2020特性
重构API导入路径以使用新的auth模块结构
2026-01-06 22:17:31 +08:00

130 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios, {
type AxiosInstance,
type InternalAxiosRequestConfig,
} from "axios";
import { ENV } from "../config";
import {
getAccessToken,
getRefreshToken,
setTokens,
clearTokens,
} from "@/utils/token";
import { Message } from "@arco-design/web-vue";
// 是否正在刷新 token
let isRefreshing = false;
// 存储因为 token 过期而挂起的请求
let requests: ((token: string) => void)[] = [];
const request: AxiosInstance = axios.create({
baseURL: ENV.API_BASE_URL,
timeout: 10000,
withCredentials: false,
});
// =======================
// 请求拦截器
// =======================
request.interceptors.request.use(
(config: InternalAxiosRequestConfig<any>) => {
// 自动携带 access token
const token = getAccessToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return error;
}
);
// =======================
// 响应拦截器
// =======================
request.interceptors.response.use(
(response) => {
// console.log("响应: ", response);
const data = response.data;
/**TODO: 增加响应码处理 */
return data;
},
async (error) => {
const originalRequest = error.config;
// 处理 401 未授权情况 (Token 过期)
// 确保不是刷新 token 的请求本身 (避免死循环)
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!originalRequest.url.includes("/auth/refresh")
) {
// 如果正在刷新,将当前请求加入队列等待
if (isRefreshing) {
return new Promise((resolve) => {
requests.push((token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(request(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = getRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available");
}
// 使用原生 axios 发送刷新请求,避免拦截器循环
const response = await axios.post(
`${ENV.API_BASE_URL}/v1/auth/refresh`,
null,
{
params: { refreshToken },
}
);
if (response.data?.success) {
const { accessToken, refreshToken: newRefreshToken } =
response.data.data;
// 更新本地存储
setTokens(accessToken, newRefreshToken);
// 执行队列中的请求
requests.forEach((cb) => cb(accessToken));
requests = [];
// 重试当前请求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return request(originalRequest);
} else {
throw new Error("Refresh token failed");
}
} catch (refreshError) {
console.error("Token 刷新失败:", refreshError);
// 清除过期 token
clearTokens();
Message.error("登录已过期,请重新登录");
// 这里可以选择跳转到登录页,例如 window.location.href = '/user/login'
// 建议让路由守卫或者页面自行处理未登录状态
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
/**
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise相当于“主动抛出错误”
*
* 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。
* 它在 异步流程、拦截器、错误处理 中非常常见。
*/
console.error("❌ 网络错误:", error);
return Promise.reject(error);
}
);
export default request;