226 lines
6.4 KiB
TypeScript
226 lines
6.4 KiB
TypeScript
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;
|
||
}
|
||
}
|
||
}
|