baidu-digital-human/src/utils/tokenManager.ts
2026-06-11 13:46:14 +08:00

226 lines
6.4 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 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<typeof setInterval> | 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<void> {
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<void> {
// 原子锁:防止并发刷新
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;
}
}
}