feat(api): P1.4 web token 60min静默刷新—后端POST /auth/refresh(有效旧token换新,过期401不放行续命)+login/refresh返expires_in+get_token_exp;前端请求拦截器剩余<10min单飞预刷新(裸axios避递归/mock短路,失败静默降级走401兜底);治长回测轮询401跳登录;3新测试 [vps]
CI/CD / test (push) Successful in 18s
CI/CD / nas-deploy (push) Successful in 34s
CI/CD / nas-verify (push) Successful in 11s

This commit is contained in:
2026-08-14 23:09:34 +08:00
parent 61cc01efbe
commit e9aec041db
4 changed files with 138 additions and 3 deletions
+37 -1
View File
@@ -8,9 +8,45 @@ export const apiClient = axios.create({
timeout: 60000,
})
apiClient.interceptors.request.use((config) => {
// P1.4 token 静默刷新:JWT 剩余 <10min 时单飞调 /auth/refresh 换新,
// 长回测轮询不再因 60min 过期被 401 打断跳登录(401 拦截仍是兜底)。
const REFRESH_AHEAD_SEC = 600
let refreshPromise: Promise<void> | null = null
function tokenExpSec(token: string): number | null {
try {
const b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
const payload = JSON.parse(atob(b64)) as { exp?: unknown }
return typeof payload.exp === 'number' ? payload.exp : null
} catch {
return null
}
}
async function refreshToken(): Promise<void> {
const auth = useAuthStore()
try {
// 裸 axios(不走 apiClient):避免拦截器递归/mock 短路
const { data } = await axios.post('/api/v1/auth/refresh', null, {
headers: { Authorization: `Bearer ${auth.token}` },
timeout: 10000,
})
if (data?.token) auth.setToken(data.token as string, auth.username ?? '')
} catch {
// 刷新失败(如后端不可达):本次请求带旧 token 走,401 兜底处理
} finally {
refreshPromise = null
}
}
apiClient.interceptors.request.use(async (config) => {
const auth = useAuthStore()
if (auth.token) {
const exp = tokenExpSec(auth.token)
if (exp !== null && exp - Date.now() / 1000 < REFRESH_AHEAD_SEC) {
refreshPromise ??= refreshToken()
await refreshPromise
}
config.headers.Authorization = `Bearer ${auth.token}`
}