314 lines
8.8 KiB
TypeScript
Raw Normal View History

2025-12-17 10:38:15 +08:00
// api/axios.ts
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { ElMessage } from 'element-plus';
import { Session } from './storage';
// token 键定义
export const accessTokenKey = 'access-token';
export const refreshAccessTokenKey = `x-${accessTokenKey}`;
// 清除 token
export const clearAccessTokens = () => {
Session.remove(accessTokenKey);
Session.remove(refreshAccessTokenKey);
};
2026-04-03 17:49:06 +08:00
// Token刷新状态和请求队列
let isRefreshing = false;
let requestQueue: Array<(token: string) => void> = [];
2025-12-17 10:38:15 +08:00
/**
* 检查并存储授权信息
* @param res 响应对象
*/
export function checkAndStoreAuthentication(res: any): void {
// 读取响应报文头 token 信息
const accessToken = res.headers[accessTokenKey];
const refreshAccessToken = res.headers[refreshAccessTokenKey];
// 判断是否是无效 token
if (accessToken === 'invalid_token') {
clearAccessTokens();
}
// 判断是否存在刷新 token,如果存在则存储在本地
else if (refreshAccessToken && accessToken && accessToken !== 'invalid_token') {
Session.set(accessTokenKey, accessToken);
Session.set(refreshAccessTokenKey, refreshAccessToken);
}
}
/**
* 解密 JWT token 的信息
* @param token jwt token 字符串
* @returns <any>object
*/
export function decryptJWT(token: string): any {
token = token.replace(/_/g, '/').replace(/-/g, '+');
const json = decodeURIComponent(escape(window.atob(token.split('.')[1])));
return JSON.parse(json);
}
/**
* 将 JWT 时间戳转换成 Date
* @description 主要针对 `exp`,`iat`,`nbf`
* @param timestamp 时间戳
* @returns Date 对象
*/
export function getJWTDate(timestamp: number): Date {
return new Date(timestamp * 1000);
}
class Axios {
// axios 实例
readonly instance: AxiosInstance;
// 基础配置,url和超时时间
baseConfig: AxiosRequestConfig = { baseURL: import.meta.env.VITE_API_URL, headers: { 'Content-Type': 'application/json' }, timeout: 30000 };
constructor(config?: AxiosRequestConfig) {
// 使用axios.create创建axios实例
this.instance = axios.create(Object.assign(this.baseConfig, config));
this.instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = Session.get<string>(accessTokenKey);
if (accessToken) {
// 将 token 添加到请求报文头中
config.headers!['Authorization'] = `Bearer ${accessToken}`;
// 判断 accessToken 是否过期
const jwt: any = decryptJWT(accessToken);
const exp = getJWTDate(jwt.exp as number);
//token已过期
if (new Date() >= exp) {
// 获取刷新 token
const refreshAccessToken = Session.get<string>(refreshAccessTokenKey);
// 携带刷新 token
if (refreshAccessToken) {
config.headers!['X-Authorization'] = `Bearer ${refreshAccessToken}`;
}
}
}
return config;
},
(err: any) => {
return Promise.reject(err);
}
);
this.instance.interceptors.response.use(
async (res: AxiosResponse) => {
// 检查并存储授权信息
checkAndStoreAuthentication(res);
2026-04-03 17:49:06 +08:00
// 如果正在刷新Token,且响应中包含新Token,说明刷新成功
if (isRefreshing && res.headers[accessTokenKey]) {
const newToken = res.headers[accessTokenKey];
isRefreshing = false;
requestQueue.forEach((callback) => callback(newToken));
requestQueue = [];
}
2025-12-17 10:38:15 +08:00
const data = res.data;
if (res.config.url?.toLocaleLowerCase() === '/file/upload'.toLowerCase()) {
return data;
}
// code为200 或 返回的是文件流 直接返回
if (data.statusCode === 200 || res.config.responseType === 'blob') {
return data;
}
let message = '';
switch (data.statusCode) {
case 401:
clearAccessTokens();
message = '您已登出,请重新登录';
break;
case 403:
message = '您没有权限,无法执行此项操作!';
break;
default:
message = JSON.stringify(data.errors);
break;
}
ElMessage({
showClose: true,
message: `${message}`,
type: 'error',
});
if (data.statusCode === 401) {
clearAccessTokens();
2026-04-03 17:49:06 +08:00
isRefreshing = false;
requestQueue = [];
2025-12-17 10:38:15 +08:00
window.location.href = '/'; // 去登录页
}
return Promise.reject(new Error(data.errors || 'Error'));
},
2026-04-03 17:49:06 +08:00
async (err: any) => {
const originalRequest = err.config;
// 如果是401错误且不是重试请求
if (err.response && err.response.status === 401 && !originalRequest._retry) {
// 如果已经在刷新Token,将请求加入队列
if (isRefreshing) {
return new Promise((resolve) => {
requestQueue.push((newToken: string) => {
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
resolve(this.instance(originalRequest));
});
});
}
// 开始刷新Token
originalRequest._retry = true;
isRefreshing = true;
// 检查是否有刷新Token
const refreshAccessToken = Session.get<string>(refreshAccessTokenKey);
if (!refreshAccessToken) {
clearAccessTokens();
isRefreshing = false;
window.location.href = '/';
return Promise.reject(err);
}
try {
// 重新发送原请求,Furion会自动处理Token刷新
originalRequest.headers['X-Authorization'] = `Bearer ${refreshAccessToken}`;
return this.instance(originalRequest);
} catch (refreshErr) {
// 刷新失败,清除所有Token并跳转登录页
clearAccessTokens();
isRefreshing = false;
requestQueue = [];
window.location.href = '/';
return Promise.reject(refreshErr);
}
}
2025-12-17 10:38:15 +08:00
// 这里用来处理http常见错误,进行全局提示
let message = err.message;
if (err.response && err.response.status) {
switch (err.response.status) {
case 400:
message = '请求错误(400)';
break;
case 401:
message = '未授权,请重新登录(401)';
2026-04-03 17:49:06 +08:00
clearAccessTokens();
window.location.href = '/';
2025-12-17 10:38:15 +08:00
break;
case 403:
message = '拒绝访问(403)';
break;
case 404:
message = '请求出错(404)';
break;
case 408:
message = '请求超时(408)';
break;
case 500:
message = '服务器错误(500)';
break;
case 501:
message = '服务未实现(501)';
break;
case 502:
message = '网络错误(502)';
break;
case 503:
message = '服务不可用(503)';
break;
case 504:
message = '网络超时(504)';
break;
case 505:
message = 'HTTP版本不受支持(505)';
break;
default:
message = `连接出错(${err.response.status})!`;
}
}
// 这里错误消息可以使用全局弹框展示出来
if (message) {
ElMessage({
showClose: true,
message: `${message},请检查网络或联系管理员!`,
type: 'error',
});
}
// 这里是AxiosError类型,所以一般我们只reject我们需要的响应即可
return Promise.reject(err);
}
);
}
/**
* get请求
* @param url 请求地址
* @param config 配置参数
* @returns
*/
get = <T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResult<T>> => {
2026-03-25 02:16:50 +08:00
console.log('get', url, config);
2025-12-17 10:38:15 +08:00
return this.instance.get(url, config);
};
/**
* post请求
* @param url 请求地址
* @param data 请求数据
* @param config 配置参数
* @returns
*/
post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResult<T>> => {
return this.instance.post(url, data, config);
};
/**
* put请求
* @param url 请求地址
* @param data 请求数据
* @param config 配置参数
* @returns
*/
put = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResult<T>> => {
return this.instance.put(url, data, config);
};
/**
* patch请求
* @param url 请求地址
* @param data 请求数据
* @param config 配置参数
* @returns
*/
patch = <T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResult<T>> => {
return this.instance.patch(url, data, config);
};
/**
* delete请求
* @param url 请求地址
* @param config 配置参数
* @returns
*/
delete = <T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResult<T>> => {
return this.instance.delete(url, config);
};
/**
* 上传文件
* @param url 上传地址
* @param data 文件
* @returns
*/
upload = <T = any>(url: string, data?: any): Promise<T> => {
return this.instance.post(url, data, { headers: { 'Content-Type': 'multipart/form-data' } });
};
}
// 对于使用非此默认配置的,可传参 再暴露出多个配置axios
export default new Axios();