2026-04-03 17:49:06 +08:00

314 lines
8.8 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.

// 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);
};
// Token刷新状态和请求队列
let isRefreshing = false;
let requestQueue: Array<(token: string) => void> = [];
/**
* 检查并存储授权信息
* @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);
// 如果正在刷新Token,且响应中包含新Token,说明刷新成功
if (isRefreshing && res.headers[accessTokenKey]) {
const newToken = res.headers[accessTokenKey];
isRefreshing = false;
requestQueue.forEach((callback) => callback(newToken));
requestQueue = [];
}
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();
isRefreshing = false;
requestQueue = [];
window.location.href = '/'; // 去登录页
}
return Promise.reject(new Error(data.errors || 'Error'));
},
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);
}
}
// 这里用来处理http常见错误,进行全局提示
let message = err.message;
if (err.response && err.response.status) {
switch (err.response.status) {
case 400:
message = '请求错误(400)';
break;
case 401:
message = '未授权,请重新登录(401)';
clearAccessTokens();
window.location.href = '/';
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>> => {
console.log('get', url, config);
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();