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