diff --git a/src/components/iframe-bridge.tsx b/src/components/iframe-bridge.tsx index 247ef64..a0fc1bb 100644 --- a/src/components/iframe-bridge.tsx +++ b/src/components/iframe-bridge.tsx @@ -5,9 +5,10 @@ import { useEffect, useCallback, type ReactNode } from "react"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { setPlayerBearerToken } from "@/lib/lottery-auth"; import { - isIframeOriginAllowed, loadIframeAllowedOrigins, + messageToken, resolvePostMessageTargetOrigin, + resolveTrustedParentMessage, } from "@/lib/iframe-origins"; function sanitizeUrlForParent(href: string): string { @@ -123,39 +124,38 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode { console.log("[IframeBridge] Setting up iframe communication"); const handleMessage = async (event: MessageEvent): Promise => { - if (!isIframeOriginAllowed(event.origin)) { - await loadIframeAllowedOrigins(true); - if (!isIframeOriginAllowed(event.origin)) { - console.warn("[IframeBridge] Rejected message from:", event.origin); - return; - } + const data = await resolveTrustedParentMessage(event); + if (data === null) { + console.warn("[IframeBridge] Rejected untrusted parent message from:", event.origin); + return; } - const { data } = event; - if (!data || typeof data !== "object") return; - console.log("[IframeBridge] Received message:", data.type); switch (data.type) { // 主站发送初始化 Token - case "MAIN_INIT_TOKEN": - if (data.token) { + case "MAIN_INIT_TOKEN": { + const token = messageToken(data); + if (token !== null) { console.log("[IframeBridge] Received initial token"); - setBearerToken(data.token); - setPlayerBearerToken(data.token); + setBearerToken(token); + setPlayerBearerToken(token); // 勿再 notifyReady(),否则主站会重复 MAIN_INIT_TOKEN 导致消息刷屏 } break; + } // 主站刷新 Token - case "MAIN_REFRESH_TOKEN": - if (data.token) { + case "MAIN_REFRESH_TOKEN": { + const token = messageToken(data); + if (token !== null) { console.log("[IframeBridge] Received refreshed token"); - setBearerToken(data.token); - setPlayerBearerToken(data.token); + setBearerToken(token); + setPlayerBearerToken(token); notifyTokenRefreshed(); } break; + } // 主站通知 Token 即将过期 case "MAIN_TOKEN_EXPIRING": @@ -173,7 +173,7 @@ export function IframeBridge({ children }: { children: ReactNode }): ReactNode { // 主站导航请求 case "MAIN_NAVIGATE": - if (data.path && typeof data.path === "string") { + if (typeof data.path === "string") { const nextPath = resolveSafeInAppPath(data.path); if (nextPath) { window.history.pushState({}, "", nextPath); diff --git a/src/hooks/use-token-refresh.ts b/src/hooks/use-token-refresh.ts index 81c779a..0b1ee54 100644 --- a/src/hooks/use-token-refresh.ts +++ b/src/hooks/use-token-refresh.ts @@ -5,9 +5,10 @@ import { parseJwtExp } from "@/lib/jwt-payload"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { useErrorStore } from "@/stores/error-store"; import { - isIframeOriginAllowed, loadIframeAllowedOrigins, + messageToken, resolvePostMessageTargetOrigin, + resolveTrustedParentMessage, } from "@/lib/iframe-origins"; /** Token 过期前警告阈值(毫秒) */ @@ -118,25 +119,21 @@ export function useTokenRefresh(): { void loadIframeAllowedOrigins(); const handleMessage = async (event: MessageEvent): Promise => { - if (!isIframeOriginAllowed(event.origin)) { - await loadIframeAllowedOrigins(true); - if (!isIframeOriginAllowed(event.origin)) { - console.warn("[TokenRefresh] Ignored message from unknown origin:", event.origin); - return; - } + const data = await resolveTrustedParentMessage(event); + if (data === null) { + console.warn("[TokenRefresh] Ignored untrusted parent message from:", event.origin); + return; } - const { data } = event; - if (!data || typeof data !== "object") return; - // 处理主站发送的新 Token(兼容 MAIN_REFRESH_TOKEN 与 LOTTERY_TOKEN_REFRESH_RESPONSE) + const token = messageToken(data); if ( (data.type === "LOTTERY_TOKEN_REFRESH_RESPONSE" || data.type === "MAIN_REFRESH_TOKEN") && - data.token + token !== null ) { console.log("[TokenRefresh] Received new token from parent"); pendingRefreshRef.current = 0; - setBearerToken(data.token); + setBearerToken(token); retryCountRef.current = 0; } diff --git a/src/lib/iframe-origins.ts b/src/lib/iframe-origins.ts index 2dac69a..472adf7 100644 --- a/src/lib/iframe-origins.ts +++ b/src/lib/iframe-origins.ts @@ -111,6 +111,42 @@ export function isIframeOriginAllowed(origin: string): boolean { return allowedOrigins.includes(normalized); } +function isPlainMessageObject(value: unknown): value is Record { + if (Object.prototype.toString.call(value) !== "[object Object]") return false; + + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +/** 仅接收当前 iframe 的真实父窗口发来的白名单消息。 */ +export async function resolveTrustedParentMessage( + event: MessageEvent, +): Promise | null> { + if (typeof window === "undefined" || window.parent === window) { + return null; + } + if (event.source !== window.parent) { + return null; + } + + if (!isIframeOriginAllowed(event.origin)) { + await loadIframeAllowedOrigins(true); + if (!isIframeOriginAllowed(event.origin)) { + return null; + } + } + + return isPlainMessageObject(event.data) ? event.data : null; +} + +export function messageToken(data: Record): string | null { + const token = data.token; + if (typeof token !== "string") return null; + + const trimmed = token.trim(); + return trimmed === "" ? null : trimmed; +} + /** postMessage 目标:优先 referrer 对应白名单,否则取首个白名单 origin;无白名单时拒绝发送 */ export function resolvePostMessageTargetOrigin(): string | null { const allowedOrigins = getKnownIframeAllowedOrigins();