Files
tms-erp-web/src/axios.js

231 lines
7.5 KiB
JavaScript
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.

/**
* 全站http配置
*
* axios参数说明
* isSerialize是否开启form表单提交
* isToken是否需要token
*/
import axios from 'axios';
import store from '@/store/';
import router from '@/router/';
import { serialize } from '@/utils/util';
import { getToken, getRefreshToken, removeToken, removeRefreshToken } from '@/utils/auth';
import { isURL, validatenull } from '@/utils/validate';
import { ElMessage } from 'element-plus';
import website from '@/config/website';
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
import { Base64 } from 'js-base64';
import { baseUrl } from '@/config/env';
import crypto from '@/utils/crypto';
// 刷新令牌授权类型,其自身失败由 401 拦截逻辑统一收口
const REFRESH_GRANT_TYPE = 'refresh_token';
// 会话失效后只提示一次并跳转登录,在重新登录前抑制所有重复的失效提示
let isSessionExpired = false;
// 标记当前是否正在刷新令牌,避免并发请求重复发起刷新
let isRefreshing = false;
// 共享的刷新令牌 Promise令并发的失效请求复用同一次刷新结果
let refreshTokenPromise = null;
// 判断是否为刷新令牌请求
const isRefreshTokenRequest = config =>
(config.url || '').includes('/oauth/token') &&
(config.params || {}).grant_type === REFRESH_GRANT_TYPE;
// 判断是否为IAM统一身份认证登录请求
const isIamSsoLoginRequest = config => (config.url || '').includes('/iam/sso/token');
// 判断是否为重新登录请求:会开启新的会话周期
const isLoginRequest = config => {
const grantType = (config.params || {}).grant_type;
return (
isIamSsoLoginRequest(config) ||
(config.url || '').includes('/oauth/token') && !!grantType && grantType !== REFRESH_GRANT_TYPE
);
};
// 为请求写入最新的访问令牌,供初始请求与刷新后的重放共用
const applyToken = config => {
const meta = config.meta || {};
if (meta.isToken === false) return;
const token = getToken();
if (!token) return;
config.headers[website.tokenHeader] =
config.cryptoToken === true
? 'crypto ' + crypto.encryptAES(token, crypto.cryptoKey)
: 'bearer ' + token;
};
// 会话失效统一出口:整段失效期内只提示一次,清理登录态并跳转登录页
const redirectToLogin = () => {
if (isSessionExpired) return;
isSessionExpired = true;
ElMessage({
message: '登录状态已失效,请重新登录',
type: 'error',
});
removeToken();
removeRefreshToken();
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
};
axios.defaults.timeout = 10000;
//返回其他状态码
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
};
//跨域请求允许保存cookie
axios.defaults.withCredentials = true;
// NProgress Configuration
NProgress.configure({
showSpinner: false,
});
//http request拦截
axios.interceptors.request.use(
config => {
// start progress bar
NProgress.start();
//地址为已经配置状态则不添加前缀
if (!isURL(config.url) && !config.url.startsWith(baseUrl)) {
config.url = baseUrl + config.url;
}
//安全请求header
config.headers['Blade-Requested-With'] = 'BladeHttpRequest';
//设置语言请求头
config.headers['Accept-Language'] = store.getters.language || 'zh-CN';
//headers判断是否需要
const authorization = config.authorization === false;
if (!authorization) {
config.headers['Authorization'] = `Basic ${Base64.encode(
`${website.clientId}:${website.clientSecret}`
)}`;
}
//headers判断请求是否携带token
const meta = config.meta || {};
// 重新登录会开启新的会话周期,解除失效锁以便后续失效提示能再次触发
if (isLoginRequest(config)) {
isSessionExpired = false;
}
// 写入访问令牌
applyToken(config);
//判断传递数据是否加密
const cryptoData = config.cryptoData === true;
// 开启报文加密
if (cryptoData) {
if (config.params) {
const data = crypto.encryptAES(JSON.stringify(config.params), crypto.aesKey);
config.params = { data };
}
if (config.data) {
config.text = true;
config.data = crypto.encryptAES(JSON.stringify(config.data), crypto.aesKey);
}
}
//headers中配置text请求
if (config.text === true) {
config.headers['Content-Type'] = 'text/plain';
}
//headers中配置serialize为true开启序列化
if (config.method === 'post' && meta.isSerialize === true) {
config.data = serialize(config.data);
}
return config;
},
error => {
return Promise.reject(error);
}
);
//http response拦截
axios.interceptors.response.use(
res => {
NProgress.done();
//获取配置信息
const config = res.config;
const cryptoData = config.cryptoData === true;
//解析加密报文
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
//获取状态信息
const status = res.data.error_code || res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
const message = res.data.msg || res.data.error_description || '系统错误';
//如果在白名单里则自行catch逻辑处理
if (statusWhiteList.includes(status)) return Promise.reject(res);
// 令牌失效且尚未重试:尝试刷新令牌后重放原请求
if (status === 401 && !config._retry) {
// 已登出(本地无令牌):残留请求的 401 属预期,后端已拒绝,前端静默忽略即可
if (!getToken()) {
return Promise.reject(new Error(message));
}
config._retry = true;
// 无刷新令牌时不再发起空刷新,直接进入失效流程
if (validatenull(getRefreshToken())) {
redirectToLogin();
return Promise.reject(new Error(message));
}
// 并发的失效请求复用同一次刷新,避免重复刷新与重复提示
if (!isRefreshing) {
isRefreshing = true;
refreshTokenPromise = store
.dispatch('RefreshToken')
.catch(error => {
redirectToLogin();
return Promise.reject(error);
})
.finally(() => {
isRefreshing = false;
});
}
return refreshTokenPromise.then(() => {
applyToken(config);
return axios(config);
});
}
// 刷新后仍然失效:会话已不可恢复,统一跳转登录
if (status === 401 && config._retry) {
redirectToLogin();
return Promise.reject(new Error(message));
}
// 刷新令牌请求:成功响应(200)须放行给刷新动作取用新令牌;仅其自身失败(error_code)时静默丢弃,
// 交由上方 401 逻辑统一收口,避免与失效跳转重复提示
if (isRefreshTokenRequest(config) && status !== 200) {
return Promise.reject(new Error(message));
}
// OAuth2 业务错误码提示
if (status > 2000 && !validatenull(res.data.error_description)) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
// 其余非 200 响应统一提示
if (status !== 200) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
return res;
},
error => {
NProgress.done();
return Promise.reject(new Error(error));
}
);
export default axios;