添加认证服务API模块,包括登录、token刷新和验证功能 在axios拦截器中实现token自动刷新机制,处理401错误 更新tsconfig配置以支持ES2020特性 重构API导入路径以使用新的auth模块结构
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
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;
|