feat: 增强国际化支持与安全头配置
- 在 .env.example 中新增 i18next 相关配置项以支持多语言功能 - 在 next.config.ts 中添加安全头配置以支持 iframe 嵌入 - 更新 Providers 组件以引入 i18n 配置 - 在 PlayerAppShell 中集成 LanguageSwitcher 组件以实现语言切换功能 - 优化 HallWalletStrip 组件的网络状态管理逻辑 - 更新多个组件以支持国际化文本
This commit is contained in:
211
src/hooks/use-token-refresh.ts
Normal file
211
src/hooks/use-token-refresh.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||||
import { useErrorStore } from "@/stores/error-store";
|
||||
|
||||
/** Token 刷新间隔(毫秒):5 分钟 - 30 秒缓冲 = 4.5 分钟 */
|
||||
const TOKEN_REFRESH_INTERVAL = 4.5 * 60 * 1000;
|
||||
|
||||
/** Token 过期前警告阈值(毫秒) */
|
||||
const TOKEN_WARNING_THRESHOLD = 60 * 1000; // 1 分钟
|
||||
|
||||
/** 最大重试次数 */
|
||||
const MAX_RETRY = 3;
|
||||
|
||||
/**
|
||||
* Token 自动续签 Hook
|
||||
*
|
||||
* 功能:
|
||||
* 1. 监听主站 postMessage 发送的新 Token
|
||||
* 2. 自动检测 Token 过期时间并在过期前静默续签
|
||||
* 3. 支持手动触发刷新
|
||||
* 4. 刷新失败时提示用户返回主站
|
||||
*/
|
||||
export function useTokenRefresh(): {
|
||||
/** 手动触发 Token 刷新 */
|
||||
refreshToken: () => Promise<void>;
|
||||
/** 当前 Token 剩余有效时间(毫秒),-1 表示未知 */
|
||||
getTokenRemainingTime: () => number;
|
||||
/** 是否即将过期 */
|
||||
isTokenExpiringSoon: () => boolean;
|
||||
} {
|
||||
const bearerToken = usePlayerSessionStore((state) => state.bearerToken);
|
||||
const setBearerToken = usePlayerSessionStore((state) => state.setBearerToken);
|
||||
const setServerError = useErrorStore((state) => state.setServerError);
|
||||
const clearServerError = useErrorStore((state) => state.clearServerError);
|
||||
|
||||
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const retryCountRef = useRef(0);
|
||||
|
||||
/**
|
||||
* 解析 JWT 的 exp 字段
|
||||
* @returns exp 时间戳(秒),解析失败返回 null
|
||||
*/
|
||||
const parseTokenExp = useCallback((token: string | null): number | null => {
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
// JWT 格式:header.payload.signature
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
// Base64 解码 payload
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
return payload.exp ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 获取 Token 剩余有效时间
|
||||
* @returns 剩余毫秒数,-1 表示未知
|
||||
*/
|
||||
const getTokenRemainingTime = useCallback((): number => {
|
||||
const exp = parseTokenExp(bearerToken);
|
||||
if (!exp) return -1;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return Math.max(0, (exp - now) * 1000);
|
||||
}, [bearerToken, parseTokenExp]);
|
||||
|
||||
/**
|
||||
* 检查 Token 是否即将过期(1 分钟内)
|
||||
*/
|
||||
const isTokenExpiringSoon = useCallback((): boolean => {
|
||||
const remaining = getTokenRemainingTime();
|
||||
return remaining > 0 && remaining < TOKEN_WARNING_THRESHOLD;
|
||||
}, [getTokenRemainingTime]);
|
||||
|
||||
/**
|
||||
* 请求主站刷新 Token
|
||||
* 通过 postMessage 向父窗口(主站)发送刷新请求
|
||||
*/
|
||||
const requestParentRefresh = useCallback((): void => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// 向主站请求新 Token
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "LOTTERY_TOKEN_REFRESH_REQUEST",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
"*", // 或指定主站域名
|
||||
);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 手动触发 Token 刷新
|
||||
*/
|
||||
const refreshToken = useCallback(async (): Promise<void> => {
|
||||
if (retryCountRef.current >= MAX_RETRY) {
|
||||
setServerError(true, "Token 刷新失败,请返回主站重新进入");
|
||||
return;
|
||||
}
|
||||
|
||||
clearServerError();
|
||||
retryCountRef.current++;
|
||||
|
||||
// 向主站请求新 Token
|
||||
requestParentRefresh();
|
||||
|
||||
// 等待主站响应(通过 postMessage)
|
||||
// 实际逻辑在下面的 useEffect 中处理
|
||||
}, [clearServerError, requestParentRefresh, setServerError]);
|
||||
|
||||
/**
|
||||
* 监听主站 postMessage 发送的新 Token
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const handleMessage = (event: MessageEvent): void => {
|
||||
// 安全检查:验证来源
|
||||
const allowedOrigins = [
|
||||
process.env.NEXT_PUBLIC_MAIN_SITE_URL,
|
||||
// 开发环境允许本地
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
].filter(Boolean);
|
||||
|
||||
if (
|
||||
allowedOrigins.length > 0 &&
|
||||
!allowedOrigins.includes(event.origin)
|
||||
) {
|
||||
console.warn("[TokenRefresh] Ignored message from unknown origin:", event.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = event;
|
||||
if (!data || typeof data !== "object") return;
|
||||
|
||||
// 处理主站发送的新 Token
|
||||
if (data.type === "LOTTERY_TOKEN_REFRESH_RESPONSE" && data.token) {
|
||||
console.log("[TokenRefresh] Received new token from parent");
|
||||
setBearerToken(data.token);
|
||||
retryCountRef.current = 0; // 重置重试计数
|
||||
}
|
||||
|
||||
// 处理主站通知 Token 即将过期
|
||||
if (data.type === "LOTTERY_TOKEN_EXPIRING_WARNING") {
|
||||
console.log("[TokenRefresh] Token expiring warning from parent");
|
||||
// 可以在这里显示提示或自动刷新
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [setBearerToken]);
|
||||
|
||||
/**
|
||||
* 自动刷新逻辑
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!bearerToken) {
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
refreshTimerRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const exp = parseTokenExp(bearerToken);
|
||||
if (!exp) return;
|
||||
|
||||
const now = Date.now();
|
||||
const expMs = exp * 1000;
|
||||
const remaining = expMs - now;
|
||||
|
||||
// 如果已经过期,立即请求刷新
|
||||
if (remaining <= 0) {
|
||||
console.warn("[TokenRefresh] Token already expired, requesting refresh");
|
||||
requestParentRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// 在过期前 30 秒刷新
|
||||
const refreshDelay = Math.max(0, remaining - TOKEN_WARNING_THRESHOLD);
|
||||
|
||||
console.log(
|
||||
`[TokenRefresh] Token expires in ${Math.floor(remaining / 1000)}s, ` +
|
||||
`will refresh in ${Math.floor(refreshDelay / 1000)}s`,
|
||||
);
|
||||
|
||||
refreshTimerRef.current = setTimeout(() => {
|
||||
console.log("[TokenRefresh] Auto-refreshing token");
|
||||
requestParentRefresh();
|
||||
}, refreshDelay);
|
||||
|
||||
return () => {
|
||||
if (refreshTimerRef.current) {
|
||||
clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [bearerToken, parseTokenExp, requestParentRefresh]);
|
||||
|
||||
return {
|
||||
refreshToken,
|
||||
getTokenRemainingTime,
|
||||
isTokenExpiringSoon,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user