/** * 主动 Token 刷新模块 * * 在 access_token 过期前主动刷新,避免 API 请求触发 401。 * JWT payload 是 base64 编码的 JSON,无需第三方库即可解码。 */ import { useAuthStore } from "@/store/authStore" import { refreshAccessToken } from "./login" let refreshTimer: ReturnType | null = null /** 正在执行刷新操作的 Promise,防止主动刷新和 401 被动刷新并发竞争 */ let activeRefreshPromise: Promise | null = null /** 提前刷新的缓冲时间(秒) */ const REFRESH_BUFFER_SECONDS = 60 /** * 解码 JWT payload(不验签,仅读取 exp 字段) */ function decodeJwtPayload(token: string): { exp?: number } | null { try { const parts = token.split(".") if (parts.length !== 3) return null // JWT 使用 base64url 编码,需要转换为标准 base64 const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/") const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4) const decoded = atob(padded) return JSON.parse(decoded) } catch { return null } } /** * 取消已调度的主动刷新 */ export function cancelProactiveRefresh(): void { if (refreshTimer) { clearTimeout(refreshTimer) refreshTimer = null } } /** * 执行 token 刷新(带并发锁,供主动刷新和被动 401 共用) * 返回当前刷新操作的 Promise;若已有刷新进行中则复用该 Promise。 */ export function executeTokenRefresh(): Promise | null { // 已有刷新进行中 → 复用 if (activeRefreshPromise) { return activeRefreshPromise } const { user, refreshToken: refreshTokenValue } = useAuthStore.getState() // 安全检查:user 或 refreshToken 为空时跳过刷新 if (!user || !refreshTokenValue) { return null } activeRefreshPromise = (async () => { try { const data = await refreshAccessToken(refreshTokenValue) const newAccessToken = data.access_token const newRefreshToken = data.refresh_token ?? refreshTokenValue // 更新 Zustand store + localStorage useAuthStore.getState().setAuth(user, newAccessToken, newRefreshToken) // 递归调度下一次刷新 scheduleProactiveRefresh() } catch { // 刷新失败 → 清除认证状态,跳转登录页 cancelProactiveRefresh() useAuthStore.getState().clearAuth() window.location.href = "/login" } finally { activeRefreshPromise = null } })() return activeRefreshPromise } /** * 调度主动刷新:在 token 过期前 REFRESH_BUFFER_SECONDS 秒自动刷新 */ export function scheduleProactiveRefresh(): void { cancelProactiveRefresh() // 统一从 Zustand store 读取(与 setAuth 写入保持一致) const { accessToken, refreshToken: refreshTokenValue } = useAuthStore.getState() if (!accessToken || !refreshTokenValue) return const payload = decodeJwtPayload(accessToken) if (!payload?.exp) return const now = Math.floor(Date.now() / 1000) const secondsUntilExpiry = payload.exp - now // 如果 token 已经过期或即将在缓冲时间内过期,立即刷新 const delaySeconds = Math.max(secondsUntilExpiry - REFRESH_BUFFER_SECONDS, 0) refreshTimer = setTimeout(() => { executeTokenRefresh() }, delaySeconds * 1000) }