314 lines
8.8 KiB
TypeScript
314 lines
8.8 KiB
TypeScript
// 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();
|