feat(auth): 将iframe通信从用户名改为令牌验证
This commit is contained in:
@@ -143,20 +143,25 @@
|
||||
<div class="layout">
|
||||
<aside class="panel">
|
||||
<h1>PlayX Host Test</h1>
|
||||
<p>Use this page to test iframe embedding and the <code>PLAYX_READY</code> / <code>IFRAME_CONTEXT</code> handshake flow.</p>
|
||||
<p>Use this page to test iframe embedding and the <code>IFRAME_READY</code> / <code>IFRAME_CONTEXT</code> handshake flow.</p>
|
||||
|
||||
<label>
|
||||
Username
|
||||
<input id="username" value="+60777777777" />
|
||||
<input id="username" value="+60333333333" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Token
|
||||
<input id="token" value="" readonly />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Language
|
||||
<input id="language" value="zh-CN" />
|
||||
<input id="language" value="zh-my" />
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button id="send" class="primary" type="button">Send Context</button>
|
||||
<button id="send" class="primary" type="button">Fetch Token & Send Context</button>
|
||||
<button id="reload" class="secondary" type="button">Reload Iframe</button>
|
||||
</div>
|
||||
|
||||
@@ -170,8 +175,8 @@
|
||||
|
||||
<script>
|
||||
const frame = document.getElementById('playx-frame')
|
||||
const previewFrame = document.getElementById('preview-frame')
|
||||
const usernameInput = document.getElementById('username')
|
||||
const tokenInput = document.getElementById('token')
|
||||
const languageInput = document.getElementById('language')
|
||||
const sendButton = document.getElementById('send')
|
||||
const reloadButton = document.getElementById('reload')
|
||||
@@ -182,6 +187,9 @@
|
||||
const heightLogDelay = 300
|
||||
let pendingHeightLog = null
|
||||
let heightLogTimer = null
|
||||
let currentTokenUsername = ''
|
||||
let autoContextSent = false
|
||||
let sendContextInFlight = false
|
||||
|
||||
function appendLog(message, data) {
|
||||
const stamp = new Date().toLocaleTimeString()
|
||||
@@ -200,7 +208,7 @@
|
||||
}
|
||||
|
||||
heightLogTimer = window.setTimeout(() => {
|
||||
appendLog('Received PLAYX_HEIGHT from iframe', pendingHeightLog)
|
||||
appendLog('Received IFRAME_HEIGHT from iframe', pendingHeightLog)
|
||||
pendingHeightLog = null
|
||||
heightLogTimer = null
|
||||
}, heightLogDelay)
|
||||
@@ -210,35 +218,106 @@
|
||||
return {
|
||||
type: 'IFRAME_CONTEXT',
|
||||
payload: {
|
||||
username: usernameInput.value.trim(),
|
||||
token: tokenInput.value.trim(),
|
||||
language: languageInput.value.trim(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function sendContext() {
|
||||
async function ensureToken() {
|
||||
const username = usernameInput.value.trim()
|
||||
|
||||
if (!username) {
|
||||
throw new Error('Username is required.')
|
||||
}
|
||||
|
||||
const existingToken = tokenInput.value.trim()
|
||||
if (existingToken && currentTokenUsername === username) {
|
||||
return existingToken
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/temLogin?username=${encodeURIComponent(username)}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`temLogin request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const payload = await response.json()
|
||||
const token = payload?.data?.userInfo?.token?.trim()
|
||||
|
||||
console.log('[test-host] temLogin response:', payload)
|
||||
|
||||
if (!token) {
|
||||
throw new Error(`temLogin response missing token: ${JSON.stringify(payload)}`)
|
||||
}
|
||||
|
||||
tokenInput.value = token
|
||||
currentTokenUsername = username
|
||||
appendLog('Fetched token from temLogin', {
|
||||
username,
|
||||
tokenPreview: `${token.slice(0, 12)}...`,
|
||||
})
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
async function sendContext() {
|
||||
if (sendContextInFlight) {
|
||||
return
|
||||
}
|
||||
|
||||
sendContextInFlight = true
|
||||
sendButton.disabled = true
|
||||
|
||||
try {
|
||||
await ensureToken()
|
||||
} catch (error) {
|
||||
appendLog('Failed to fetch token', {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
sendButton.disabled = false
|
||||
sendContextInFlight = false
|
||||
return
|
||||
}
|
||||
|
||||
const message = buildPayload()
|
||||
console.log('[test-host] postMessage payload:', message)
|
||||
frame.contentWindow?.postMessage(message, iframeOrigin)
|
||||
appendLog('Sent IFRAME_CONTEXT to iframe', {
|
||||
language: message.payload.language,
|
||||
tokenPreview: `${message.payload.token.slice(0, 12)}...`,
|
||||
})
|
||||
sendButton.disabled = false
|
||||
sendContextInFlight = false
|
||||
}
|
||||
|
||||
frame.addEventListener('load', () => {
|
||||
autoContextSent = false
|
||||
})
|
||||
|
||||
sendButton.addEventListener('click', () => {
|
||||
sendContext()
|
||||
sendButton.addEventListener('click', async () => {
|
||||
await sendContext()
|
||||
})
|
||||
|
||||
reloadButton.addEventListener('click', () => {
|
||||
autoContextSent = false
|
||||
frame.contentWindow?.location.reload()
|
||||
})
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source === frame.contentWindow && event.data?.type === 'PLAYX_READY') {
|
||||
sendContext()
|
||||
if (event.source === frame.contentWindow && event.data?.type === 'IFRAME_READY') {
|
||||
appendLog('Received IFRAME_READY from iframe')
|
||||
if (autoContextSent) {
|
||||
return
|
||||
}
|
||||
autoContextSent = true
|
||||
void sendContext()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.source === frame.contentWindow && event.data?.type === 'PLAYX_HEIGHT') {
|
||||
if (event.source === frame.contentWindow && event.data?.type === 'IFRAME_HEIGHT') {
|
||||
const height = Number(event.data?.payload?.height)
|
||||
|
||||
if (!Number.isFinite(height) || height <= 0) {
|
||||
|
||||
26
src/App.tsx
26
src/App.tsx
@@ -40,12 +40,6 @@ function App() {
|
||||
})()
|
||||
}, [language, queryClient])
|
||||
|
||||
// 开发/测试阶段如需跳过 iframe 传参,可设置:
|
||||
// VITE_BYPASS_IFRAME_CONTEXT=true
|
||||
// 项目会直接使用测试数据初始化:
|
||||
// username: +60777777777
|
||||
// language: zh
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleMessage = (event: MessageEvent<HostContextMessage>) => {
|
||||
// const message = event.data
|
||||
@@ -54,15 +48,15 @@ function App() {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// const {language, username} = message.payload
|
||||
//
|
||||
// if (typeof username === 'string' && username.trim()) {
|
||||
//
|
||||
// }
|
||||
// const {language, token} = message.payload
|
||||
//
|
||||
// if (typeof language === 'string' && language.trim()) {
|
||||
// // language 由 zustand 存储,供请求头直接读取
|
||||
// }
|
||||
//
|
||||
// if (typeof token === 'string' && token.trim()) {
|
||||
// // token 由宿主机通过 postMessage 传入,供鉴权和请求头直接读取
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// window.addEventListener('message', handleMessage)
|
||||
@@ -77,8 +71,8 @@ function App() {
|
||||
// {
|
||||
// type: 'IFRAME_CONTEXT',
|
||||
// payload: {
|
||||
// username: '+60777777777',
|
||||
// language: 'zh-CN',
|
||||
// token: 'token',
|
||||
// language: 'zh-my',
|
||||
// },
|
||||
// },
|
||||
// window.location.origin
|
||||
@@ -95,7 +89,7 @@ function App() {
|
||||
// const IFRAME_ORIGIN = 'https://your-iframe-app.example.com'
|
||||
//
|
||||
// window.addEventListener('message', (event) => {
|
||||
// if (event.source !== iframe.contentWindow || event.data?.type !== 'PLAYX_READY') {
|
||||
// if (event.source !== iframe.contentWindow || event.data?.type !== 'IFRAME_READY') {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
@@ -103,8 +97,8 @@ function App() {
|
||||
// {
|
||||
// type: 'IFRAME_CONTEXT',
|
||||
// payload: {
|
||||
// username: '+60777777777',
|
||||
// language: 'zh-CN',
|
||||
// token: 'token',
|
||||
// language: 'zh-my',
|
||||
// },
|
||||
// },
|
||||
// IFRAME_ORIGIN
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {type PropsWithChildren, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {login, validateToken} from '@/api/auth.ts'
|
||||
import {validateToken} from '@/api/auth.ts'
|
||||
import {userAssets} from '@/api/user.ts'
|
||||
import {useTranslation} from 'react-i18next'
|
||||
import {normalizeLanguage} from '@/lib/i18n'
|
||||
@@ -14,30 +14,20 @@ const HOST_HEIGHT_MESSAGE = 'IFRAME_HEIGHT'
|
||||
const HOST_READY_INTERVAL = 500
|
||||
const HOST_READY_RETRY_LIMIT = 20
|
||||
const HOST_HEIGHT_REPORT_INTERVAL = 250
|
||||
const TEST_BOOTSTRAP_ENABLED = import.meta.env.VITE_BYPASS_IFRAME_CONTEXT === 'true'
|
||||
const TEST_BOOTSTRAP_USERNAME = '+60777777777'
|
||||
const TEST_BOOTSTRAP_LANGUAGE = 'zh'
|
||||
|
||||
export function AuthGuide({children}: PropsWithChildren) {
|
||||
const {t} = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [username, setUsername] = useState(TEST_BOOTSTRAP_ENABLED ? TEST_BOOTSTRAP_USERNAME : '')
|
||||
const [hostToken, setHostToken] = useState('')
|
||||
const setUserInfo = useUserStore((state) => state.setUserInfo)
|
||||
const setAuthInfo = useUserStore((state) => state.setAuthInfo)
|
||||
const setAssetsInfo = useUserStore((state) => state.setAssetsInfo)
|
||||
const setLanguage = useUserStore((state) => state.setLanguage)
|
||||
const clearUserInfo = useUserStore((state) => state.clearUserInfo)
|
||||
const hasHostContextRef = useRef(false)
|
||||
const activeUsernameRef = useRef('')
|
||||
const activeTokenRef = useRef('')
|
||||
|
||||
useEffect(() => {
|
||||
if (TEST_BOOTSTRAP_ENABLED) {
|
||||
hasHostContextRef.current = true
|
||||
activeUsernameRef.current = TEST_BOOTSTRAP_USERNAME
|
||||
setLanguage(TEST_BOOTSTRAP_LANGUAGE)
|
||||
return
|
||||
}
|
||||
|
||||
const isEmbedded = window.parent !== window
|
||||
|
||||
const notifyParentReady = () => {
|
||||
@@ -59,17 +49,17 @@ export function AuthGuide({children}: PropsWithChildren) {
|
||||
return
|
||||
}
|
||||
|
||||
const {language, username: nextUsername} = message.payload
|
||||
const {language, token} = message.payload
|
||||
|
||||
if (typeof nextUsername === 'string' && nextUsername.trim()) {
|
||||
const normalizedUsername = nextUsername.trim()
|
||||
if (activeUsernameRef.current && activeUsernameRef.current !== normalizedUsername) {
|
||||
if (typeof token === 'string' && token.trim()) {
|
||||
const normalizedToken = token.trim()
|
||||
if (activeTokenRef.current && activeTokenRef.current !== normalizedToken) {
|
||||
clearUserInfo()
|
||||
queryClient.removeQueries({queryKey: ['auth-bootstrap']})
|
||||
}
|
||||
hasHostContextRef.current = true
|
||||
activeUsernameRef.current = normalizedUsername
|
||||
setUsername(normalizedUsername)
|
||||
activeTokenRef.current = normalizedToken
|
||||
setHostToken(normalizedToken)
|
||||
}
|
||||
|
||||
if (typeof language === 'string' && language.trim()) {
|
||||
@@ -106,19 +96,17 @@ export function AuthGuide({children}: PropsWithChildren) {
|
||||
}, [clearUserInfo, queryClient, setLanguage])
|
||||
|
||||
const authBootstrapQuery = useQuery({
|
||||
queryKey: queryKeys.authBootstrap(username),
|
||||
enabled: Boolean(username),
|
||||
queryKey: queryKeys.authBootstrap(hostToken),
|
||||
enabled: Boolean(hostToken),
|
||||
queryFn: async () => {
|
||||
const loginResponse = await login({username})
|
||||
const userInfo = loginResponse.data.userInfo
|
||||
const validateResponse = await validateToken(userInfo.token)
|
||||
const validateResponse = await validateToken(hostToken)
|
||||
const authInfo = validateResponse.data
|
||||
const assetsResponse = await userAssets({
|
||||
session_id: authInfo.session_id,
|
||||
})
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
userInfo: {token: hostToken},
|
||||
authInfo,
|
||||
assetsInfo: assetsResponse.data,
|
||||
}
|
||||
@@ -235,10 +223,10 @@ export function AuthGuide({children}: PropsWithChildren) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!username || authBootstrapQuery.isPending) {
|
||||
if (!hostToken || authBootstrapQuery.isPending) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center text-[14px] text-white/68">
|
||||
{!username && !TEST_BOOTSTRAP_ENABLED ? t('auth.waitingForHostContext') : t('auth.loadingAccountData')}
|
||||
{!hostToken ? t('auth.waitingForHostContext') : t('auth.loadingAccountData')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const en = {
|
||||
malay: 'Malay',
|
||||
},
|
||||
auth: {
|
||||
waitingForHostContext: 'Waiting for host context...',
|
||||
waitingForHostContext: 'Waiting for verification...',
|
||||
loadingAccountData: 'Loading account data...',
|
||||
authenticationFailed: 'Authentication failed',
|
||||
refreshAndTryAgain: 'Please refresh and try again.',
|
||||
|
||||
@@ -16,7 +16,7 @@ const ms = {
|
||||
malay: 'Bahasa Melayu',
|
||||
},
|
||||
auth: {
|
||||
waitingForHostContext: 'Menunggu konteks hos...',
|
||||
waitingForHostContext: 'Menunggu pengesahan...',
|
||||
loadingAccountData: 'Sedang memuatkan data akaun...',
|
||||
authenticationFailed: 'Pengesahan gagal',
|
||||
refreshAndTryAgain: 'Sila muat semula dan cuba lagi.',
|
||||
|
||||
@@ -16,7 +16,7 @@ const zh = {
|
||||
malay: '马来文',
|
||||
},
|
||||
auth: {
|
||||
waitingForHostContext: '等待宿主上下文...',
|
||||
waitingForHostContext: '等待验证...',
|
||||
loadingAccountData: '账户数据加载中...',
|
||||
authenticationFailed: '鉴权失败',
|
||||
refreshAndTryAgain: '请刷新后重试。',
|
||||
|
||||
@@ -4,13 +4,13 @@ export type LoginParams = {
|
||||
}
|
||||
|
||||
export type LoginUserInfo = {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
playx_user_id: string
|
||||
token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
id?: number
|
||||
username?: string
|
||||
nickname?: string
|
||||
playx_user_id?: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
}
|
||||
|
||||
export type LoginData = {
|
||||
|
||||
@@ -9,7 +9,6 @@ export type HostContextMessage = {
|
||||
payload?: {
|
||||
token?: string
|
||||
language?: string
|
||||
username?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1,7 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_BYPASS_IFRAME_CONTEXT?: string
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
readonly VITE_API_ORIGIN?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user