Web:访问令牌 10 分钟;活跃时自动轮换刷新令牌;连续无操作 30 分钟后清除登录并跳转登录页。 App:会话窗口 3 天;打开 App、恢复前台或请求接口时自动刷新并重新顺延 3 天。 普通登录和 SSO 使用同一策略。 刷新令牌只以 SHA-256 摘要入库,每次刷新都会轮换,旧令牌无法再次使用;退出登录会吊销刷新令牌。 网络临时故障不会误清登录状态,多标签页同时刷新也做了竞争处理。
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import axios from 'axios'
|
||
import { goLogin } from '../utils/navigate'
|
||
import {
|
||
authStorageKeys,
|
||
clearAuthSession,
|
||
markActivity,
|
||
refreshAuthSession,
|
||
refreshIfNeeded,
|
||
} from '../auth/session'
|
||
|
||
const http = axios.create({
|
||
baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api',
|
||
timeout: 15000,
|
||
})
|
||
|
||
http.interceptors.request.use(async (config) => {
|
||
const isAuthenticationRequest =
|
||
config.url?.endsWith('/auth/login') ||
|
||
config.url?.endsWith('/auth/refresh') ||
|
||
config.url?.endsWith('/auth/logout') ||
|
||
config.url?.endsWith('/auth/sso/exchange') ||
|
||
config.url?.endsWith('/auth/sso/bind')
|
||
if (!isAuthenticationRequest) {
|
||
const activeToken = await refreshIfNeeded(true)
|
||
if (activeToken) markActivity()
|
||
}
|
||
const token = localStorage.getItem(authStorageKeys.token)
|
||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||
return config
|
||
})
|
||
|
||
http.interceptors.response.use(
|
||
(response) => response,
|
||
async (error) => {
|
||
const isAuthenticationRequest =
|
||
error.config?.url?.endsWith('/auth/login') ||
|
||
error.config?.url?.endsWith('/auth/refresh') ||
|
||
error.config?.url?.endsWith('/auth/logout') ||
|
||
error.config?.url?.endsWith('/auth/sso/exchange') ||
|
||
error.config?.url?.endsWith('/auth/sso/bind')
|
||
const retryableConfig = error.config as
|
||
(typeof error.config & { _jiaowuRetried?: boolean }) | undefined
|
||
if (error.response?.status === 401 && !isAuthenticationRequest &&
|
||
!retryableConfig?._jiaowuRetried) {
|
||
const token = await refreshAuthSession()
|
||
if (token && retryableConfig) {
|
||
retryableConfig._jiaowuRetried = true
|
||
retryableConfig.headers.Authorization = `Bearer ${token}`
|
||
return http.request(retryableConfig)
|
||
}
|
||
}
|
||
if (error.response?.status === 401 && !isAuthenticationRequest) {
|
||
clearAuthSession()
|
||
goLogin(location.pathname + location.search + location.hash)
|
||
}
|
||
return Promise.reject(error)
|
||
},
|
||
)
|
||
|
||
export function apiErrorMessage(error: unknown) {
|
||
if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。'
|
||
const status = error.response?.status
|
||
const data = error.response?.data
|
||
if (data?.errors && typeof data.errors === 'object') {
|
||
const validationMessage = Object.values(data.errors)
|
||
.flat()
|
||
.map((item) => String(item).trim())
|
||
.filter(Boolean)
|
||
.join(';')
|
||
if (validationMessage) return validationMessage
|
||
}
|
||
const detail = typeof data?.detail === 'string' ? data.detail.trim() : ''
|
||
if (detail) return detail
|
||
const title = typeof data?.title === 'string' ? data.title.trim() : ''
|
||
if (title) return title
|
||
if (status === 403) return '您没有权限执行此操作,请联系管理员。'
|
||
if (status === 404) return '请求的资源不存在。'
|
||
if (status === 401) return '登录已过期,请重新登录。'
|
||
if (status && status >= 500) return '服务器繁忙,请稍后重试。'
|
||
return '操作失败,请检查网络连接。'
|
||
}
|
||
|
||
export default http
|