Files
Academic-Affairs-System/web/src/api/http.ts
T
biss 5ec62a03ca 滑动续期与自动刷新:
Web:访问令牌 10 分钟;活跃时自动轮换刷新令牌;连续无操作 30 分钟后清除登录并跳转登录页。
App:会话窗口 3 天;打开 App、恢复前台或请求接口时自动刷新并重新顺延 3 天。
普通登录和 SSO 使用同一策略。
刷新令牌只以 SHA-256 摘要入库,每次刷新都会轮换,旧令牌无法再次使用;退出登录会吊销刷新令牌。
网络临时故障不会误清登录状态,多标签页同时刷新也做了竞争处理。
2026-08-03 19:20:39 +08:00

84 lines
2.9 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 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