235 lines
7.7 KiB
TypeScript
235 lines
7.7 KiB
TypeScript
import axios, { AxiosError, AxiosRequestConfig } from 'axios'
|
||
import { Message } from '@arco-design/web-vue'
|
||
import router from '@/router'
|
||
import { OpenAPI, clearAuthToken, OmsLoginType } from './index'
|
||
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||
import { resolvePostLogoutLogin } from '@/utils/login-portal'
|
||
|
||
// 创建自定义 axios 实例
|
||
// baseURL 为空;远端由 OpenAPI.BASE / omsUrl 按 env 拼接(本地走 Vite 代理到测试环境)
|
||
export const axiosInstance = axios.create({
|
||
baseURL: '', // 始终为空,URL 由 OpenAPI.BASE + path 构建
|
||
timeout: 30000,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
|
||
// 是否正在刷新 Token
|
||
let isRefreshing = false
|
||
// 等待刷新的请求队列
|
||
let refreshSubscribers: ((token: string) => void)[] = []
|
||
|
||
// 添加请求到等待队列
|
||
function subscribeTokenRefresh(callback: (token: string) => void) {
|
||
refreshSubscribers.push(callback)
|
||
}
|
||
|
||
// 通知所有等待的请求
|
||
function onTokenRefreshed(token: string) {
|
||
refreshSubscribers.forEach(callback => callback(token))
|
||
refreshSubscribers = []
|
||
}
|
||
|
||
function redirectToLoginPage(
|
||
userStore: ReturnType<typeof useRbacUserStore>,
|
||
loginType?: OmsLoginType | null
|
||
) {
|
||
const current = router.currentRoute.value
|
||
const target = resolvePostLogoutLogin({
|
||
loginType: loginType !== undefined ? loginType : userStore.resolveLoginTypeForRedirect(),
|
||
path: current.fullPath,
|
||
})
|
||
if (target.path) {
|
||
router.replace({
|
||
path: target.path,
|
||
query: target.query,
|
||
})
|
||
return
|
||
}
|
||
router.replace({
|
||
name: target.name,
|
||
query: target.query,
|
||
})
|
||
}
|
||
|
||
function logoutAndRedirectToLogin(userStore: ReturnType<typeof useRbacUserStore>, message: string) {
|
||
const loginType = userStore.resolveLoginTypeForRedirect()
|
||
userStore.logout()
|
||
Message.error(message)
|
||
redirectToLoginPage(userStore, loginType)
|
||
}
|
||
|
||
// 请求拦截器
|
||
axiosInstance.interceptors.request.use(
|
||
async (config: AxiosRequestConfig) => {
|
||
const userStore = useRbacUserStore()
|
||
|
||
// 确保 headers 存在
|
||
if (!config.headers) {
|
||
config.headers = {}
|
||
}
|
||
|
||
const requestUrl = String(config.url || '')
|
||
// COS 预签名只签 host;带 Bearer 会 CORS 预检失败 / 签名校验失败
|
||
const isCosAsset =
|
||
/^https?:\/\/[^/]*cos\./i.test(requestUrl) ||
|
||
/myqcloud\.com/i.test(requestUrl) ||
|
||
requestUrl.startsWith('/__cos__/')
|
||
|
||
// 优先使用 userStore 的 accessToken(最可靠)
|
||
let token: string | undefined
|
||
|
||
if (!isCosAsset) {
|
||
if (userStore.accessToken) {
|
||
token = userStore.accessToken
|
||
} else if (OpenAPI.TOKEN) {
|
||
// 兼容:从 OpenAPI 配置中获取 token
|
||
token = typeof OpenAPI.TOKEN === 'function'
|
||
? await OpenAPI.TOKEN({} as any)
|
||
: OpenAPI.TOKEN
|
||
}
|
||
}
|
||
|
||
// 如果找到了 token,设置 Authorization header
|
||
if (token) {
|
||
// 如果token已经包含Bearer前缀,直接使用;否则添加Bearer前缀
|
||
if (typeof token === 'string' && token.startsWith('Bearer ')) {
|
||
config.headers.Authorization = token
|
||
} else if (typeof token === 'string' && token.startsWith('Bearer_')) {
|
||
// 处理 Bearer_xxx 格式,转换为 Bearer xxx
|
||
config.headers.Authorization = `Bearer ${token.replace('Bearer_', '')}`
|
||
} else {
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
|
||
} else {
|
||
// COS 资源或无 token:确保不带 Authorization
|
||
if (isCosAsset && config.headers) {
|
||
delete (config.headers as any).Authorization
|
||
delete (config.headers as any).authorization
|
||
}
|
||
}
|
||
|
||
// FormData 须由浏览器自动带 boundary,勿保留默认 application/json
|
||
if (config.data instanceof FormData) {
|
||
delete (config.headers as any)['Content-Type']
|
||
delete (config.headers as any)['content-type']
|
||
}
|
||
// GET / 二进制下载勿带 application/json,避免多余 CORS 预检与中间件误判
|
||
const method = String(config.method || 'get').toLowerCase()
|
||
if (method === 'get' || method === 'head' || isCosAsset) {
|
||
delete (config.headers as any)['Content-Type']
|
||
delete (config.headers as any)['content-type']
|
||
}
|
||
|
||
return config
|
||
},
|
||
(error: AxiosError) => {
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
// 响应拦截器
|
||
axiosInstance.interceptors.response.use(
|
||
(response) => {
|
||
return response
|
||
},
|
||
async (error: AxiosError) => {
|
||
const originalRequest = error.config as AxiosRequestConfig & {
|
||
_retry?: boolean
|
||
silentError?: boolean
|
||
}
|
||
const userStore = useRbacUserStore()
|
||
// 登录接口由页面内联 errorBox 展示,避免拦截器再弹 Message
|
||
const requestUrl = String(originalRequest?.url || '')
|
||
const isAuthLoginRequest = /\/rbac\/auth\/login(?:\?|$)/.test(requestUrl)
|
||
const silentError = Boolean(originalRequest?.silentError) || isAuthLoginRequest
|
||
|
||
if (error.response) {
|
||
const status = error.response.status
|
||
const data: any = error.response.data
|
||
|
||
// 401:保活窗内(含 F5 从 lastActivity 恢复的 idle 窗)才允许续期
|
||
if (status === 401 && !originalRequest._retry) {
|
||
if (!userStore.isKeepAliveActive()) {
|
||
if (!silentError) logoutAndRedirectToLogin(userStore, '登录已过期,请重新登录')
|
||
return Promise.reject(error)
|
||
}
|
||
|
||
// 如果没有 refreshToken,直接登出
|
||
if (!userStore.refreshToken) {
|
||
if (!silentError) logoutAndRedirectToLogin(userStore, '未授权,请重新登录')
|
||
return Promise.reject(error)
|
||
}
|
||
|
||
// 如果正在刷新,将请求加入队列
|
||
if (isRefreshing) {
|
||
return new Promise(resolve => {
|
||
subscribeTokenRefresh((token: string) => {
|
||
if (!originalRequest.headers) {
|
||
originalRequest.headers = {}
|
||
}
|
||
originalRequest.headers.Authorization = `Bearer ${token}`
|
||
resolve(axiosInstance(originalRequest))
|
||
})
|
||
})
|
||
}
|
||
|
||
originalRequest._retry = true
|
||
isRefreshing = true
|
||
|
||
try {
|
||
const success = await userStore.refreshAccessToken()
|
||
|
||
if (success) {
|
||
// 刷新成功,重试原请求
|
||
onTokenRefreshed(userStore.accessToken)
|
||
if (!originalRequest.headers) {
|
||
originalRequest.headers = {}
|
||
}
|
||
originalRequest.headers.Authorization = `Bearer ${userStore.accessToken}`
|
||
return axiosInstance(originalRequest)
|
||
} else {
|
||
// 刷新失败,登出
|
||
if (!silentError) logoutAndRedirectToLogin(userStore, '登录已过期,请重新登录')
|
||
return Promise.reject(error)
|
||
}
|
||
} catch {
|
||
if (!silentError) logoutAndRedirectToLogin(userStore, '登录已过期,请重新登录')
|
||
return Promise.reject(error)
|
||
} finally {
|
||
isRefreshing = false
|
||
}
|
||
}
|
||
|
||
if (silentError) {
|
||
return Promise.reject(error)
|
||
}
|
||
|
||
switch (status) {
|
||
case 401:
|
||
// 如果走到这里,说明401没有被上面的逻辑处理(可能是没有retry标记的情况)
|
||
logoutAndRedirectToLogin(userStore, '未授权,请重新登录')
|
||
break
|
||
case 403:
|
||
Message.error('没有权限访问')
|
||
break
|
||
case 404:
|
||
Message.error('请求的资源不存在')
|
||
break
|
||
case 500:
|
||
Message.error(data?.message || '服务器错误')
|
||
break
|
||
default:
|
||
Message.error(data?.message || '请求失败')
|
||
}
|
||
} else if (error.request) {
|
||
if (!silentError) Message.error('网络错误,请检查网络连接')
|
||
} else {
|
||
if (!silentError) Message.error('请求配置错误')
|
||
}
|
||
return Promise.reject(error)
|
||
}
|
||
)
|