import CryptoJS from "crypto-js"; /** Token 有效期(毫秒):60分钟 */ const TOKEN_TTL_MS = 60 * 60 * 1000; /** Token 过期前提前刷新时间(毫秒):30秒 */ const TOKEN_REFRESH_BEFORE_EXPIRY_MS = 30 * 1000; /** Token 更新失败最大重试次数 */ const MAX_RETRY_COUNT = 3; /** 重试间隔(毫秒) */ const RETRY_INTERVAL_MS = 1000; /** Token 信息结构 */ export interface TokenInfo { /** Authorization 请求头值,格式: appId/signature/time */ token: string; /** Token 过期时间(ISO格式) */ expiresAt: string; } /** * 生成 Token * - 使用 HMAC-SHA256 算法,以 appKey 为密钥,对 appId + 时间戳 进行签名 * - 返回 TokenInfo 包含 token 字符串和过期时间 */ export function generateToken(appId: string, appKey: string): TokenInfo { if (!appId || !appKey) { throw new Error("[Token] appId 和 appKey 不能为空"); } const expiresAt = new Date(Date.now() + TOKEN_TTL_MS).toISOString(); const message = appId + expiresAt; const signature = CryptoJS.HmacSHA256(message, appKey).toString( CryptoJS.enc.Hex, ); const token = `${appId}/${signature}/${expiresAt}`; console.info(`[Token] 生成成功,过期时间: ${expiresAt}`); return { token, expiresAt }; } /** * 检查 Token 是否即将过期(剩余有效期不足30秒) */ export function isTokenExpiring(expiresAt: string): boolean { const expiryTime = new Date(expiresAt).getTime(); const remaining = expiryTime - Date.now(); return remaining <= TOKEN_REFRESH_BEFORE_EXPIRY_MS; } /** * 检查 Token 是否已过期 */ export function isTokenExpired(expiresAt: string): boolean { return new Date(expiresAt).getTime() <= Date.now(); } /** * 检查 HTTP 状态码是否为授权错误 */ export function isAuthError(status: number): boolean { return status === 401 || status === 403; } /** * Token 管理器:封装 Token 生成、自动更新、重试机制 * * 使用方式: * const manager = new TokenManager(appId, appKey); * const tokenInfo = manager.getTokenInfo(); // 获取当前 Token * manager.startAutoRefresh(); // 启动自动刷新 * manager.stopAutoRefresh(); // 停止自动刷新 * manager.refreshOnAuthError(401); // 授权错误时触发刷新 */ export class TokenManager { private appId: string; private appKey: string; private tokenInfo: TokenInfo | null = null; private refreshTimer: ReturnType | null = null; private isRefreshing = false; // 原子锁,防止并发刷新 private retryCount = 0; /** Token 更新回调列表 */ private onTokenRefreshedCallbacks: Array<(tokenInfo: TokenInfo) => void> = []; constructor(appId: string, appKey: string) { if (!appId || !appKey) { throw new Error("[TokenManager] appId 和 appKey 不能为空"); } this.appId = appId; this.appKey = appKey; // 初始化时立即生成一个 Token this.tokenInfo = generateToken(this.appId, this.appKey); } /** * 获取当前 TokenInfo(如果即将过期则自动刷新) */ getTokenInfo(): TokenInfo { if (this.tokenInfo && !isTokenExpiring(this.tokenInfo.expiresAt)) { return this.tokenInfo; } // 即将过期或已过期,同步刷新 this.refreshSync(); return this.tokenInfo!; } /** * 获取当前 token 字符串 */ getToken(): string { return this.getTokenInfo().token; } /** * 注册 Token 更新回调 */ onTokenRefreshed(callback: (tokenInfo: TokenInfo) => void): void { this.onTokenRefreshedCallbacks.push(callback); } /** * 启动自动刷新定时器 * - 每10秒检查一次 Token 是否即将过期 * - 过期前30秒自动触发刷新 */ startAutoRefresh(): void { this.stopAutoRefresh(); console.info("[TokenManager] 启动自动刷新定时器"); this.refreshTimer = setInterval(() => { if (this.tokenInfo && isTokenExpiring(this.tokenInfo.expiresAt)) { console.info("[TokenManager] Token 即将过期,触发自动刷新"); this.refreshWithRetry(); } }, 10_000); } /** * 停止自动刷新定时器 */ stopAutoRefresh(): void { if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null; console.info("[TokenManager] 停止自动刷新定时器"); } } /** * 授权错误时触发 Token 刷新 * - 检测到 401/403 时调用此方法 */ async refreshOnAuthError(status: number): Promise { if (!isAuthError(status)) return; console.info(`[TokenManager] 检测到授权错误 ${status},触发 Token 刷新`); await this.refreshWithRetry(); } /** * 同步刷新 Token(内部使用,无重试) */ private refreshSync(): void { try { this.tokenInfo = generateToken(this.appId, this.appKey); this.retryCount = 0; } catch (error) { console.error("[TokenManager] Token 生成失败:", error); throw error; } } /** * 带重试的异步刷新 * - 最多重试 MAX_RETRY_COUNT 次,每次间隔 RETRY_INTERVAL_MS * - 使用原子锁防止并发刷新 */ private async refreshWithRetry(): Promise { // 原子锁:防止并发刷新 if (this.isRefreshing) { console.info("[TokenManager] Token 刷新进行中,跳过本次请求"); return; } this.isRefreshing = true; try { while (this.retryCount < MAX_RETRY_COUNT) { try { this.tokenInfo = generateToken(this.appId, this.appKey); this.retryCount = 0; console.info("[TokenManager] Token 刷新成功"); // 通知所有回调 this.onTokenRefreshedCallbacks.forEach((cb) => { try { cb(this.tokenInfo!); } catch (e) { console.error("[TokenManager] Token 更新回调执行失败:", e); } }); return; } catch (error) { this.retryCount++; console.warn( `[TokenManager] Token 刷新失败(第 ${this.retryCount}/${MAX_RETRY_COUNT} 次):`, error, ); if (this.retryCount < MAX_RETRY_COUNT) { await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS), ); } } } console.error( `[TokenManager] Token 刷新失败,已达到最大重试次数 ${MAX_RETRY_COUNT}`, ); } finally { this.isRefreshing = false; } } }