front/src/api/rbac/axios-config.ts
2026-09-23 11:29:53 +08:00

235 lines
7.7 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.

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)
}
)