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]
This commit is contained in:
@@ -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}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user