mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 00:30:32 +00:00
fix: 修复腾讯验证码票据过期与区域切换
This commit is contained in:
@@ -33,8 +33,12 @@ let instance: { show(): void; destroy(): void } | null = null
|
||||
let pending: Promise<TencentCaptchaProof | null> | null = null
|
||||
let cancelPending: (() => void) | null = null
|
||||
let cachedProof: TencentCaptchaProof | null = null
|
||||
let cachedProofCreatedAt = 0
|
||||
let isMounted = false
|
||||
|
||||
// 腾讯国际站票据官方有效期为 5 分钟,提前 1 分钟放弃缓存,避免提交时刚好过期。
|
||||
const cachedProofMaxAgeMs = 4 * 60 * 1000
|
||||
|
||||
function createVerificationPromise(revealInternational: boolean = true): Promise<TencentCaptchaProof | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
@@ -106,9 +110,17 @@ function createVerificationPromise(revealInternational: boolean = true): Promise
|
||||
|
||||
function verify(): Promise<TencentCaptchaProof | null> {
|
||||
if (cachedProof) {
|
||||
const proof = cachedProof
|
||||
cachedProof = null
|
||||
return Promise.resolve(proof)
|
||||
if (Date.now() - cachedProofCreatedAt >= cachedProofMaxAgeMs) {
|
||||
// 过期票据不能再次提交;先同步销毁旧实例,再在本次调用中创建新验证。
|
||||
reset(false)
|
||||
} else {
|
||||
const proof = cachedProof
|
||||
cachedProof = null
|
||||
cachedProofCreatedAt = 0
|
||||
// 预加载 promise 已经完成,消费缓存时同步清除引用,避免同一票据被并发复用。
|
||||
pending = null
|
||||
return Promise.resolve(proof)
|
||||
}
|
||||
}
|
||||
|
||||
if (pending) {
|
||||
@@ -119,7 +131,10 @@ function verify(): Promise<TencentCaptchaProof | null> {
|
||||
if (pending === verification) instance?.show()
|
||||
})
|
||||
return verification.then((proof) => {
|
||||
if (cachedProof === proof) cachedProof = null
|
||||
if (cachedProof === proof) {
|
||||
cachedProof = null
|
||||
cachedProofCreatedAt = 0
|
||||
}
|
||||
return proof
|
||||
})
|
||||
}
|
||||
@@ -146,7 +161,10 @@ function preload(): void {
|
||||
pending = verification
|
||||
void verification
|
||||
.then((proof) => {
|
||||
if (proof) cachedProof = proof
|
||||
if (proof) {
|
||||
cachedProof = proof
|
||||
cachedProofCreatedAt = Date.now()
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
@@ -154,18 +172,19 @@ function preload(): void {
|
||||
})
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
function reset(reinitialize: boolean = true): void {
|
||||
instance?.destroy()
|
||||
instance = null
|
||||
cancelPending?.()
|
||||
cancelPending = null
|
||||
pending = null
|
||||
cachedProof = null
|
||||
cachedProofCreatedAt = 0
|
||||
internationalContainerVisible.value = isInternational.value
|
||||
|
||||
// 国际站的票据一次性使用,登录失败后需要立即创建新的 checkbox,
|
||||
// 不能等下一次点击登录才重新初始化。卸载阶段不再启动预加载。
|
||||
if (isMounted && isInternational.value) {
|
||||
if (reinitialize && isMounted && isInternational.value) {
|
||||
void nextTick().then(() => {
|
||||
if (isMounted) preload()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import TencentCaptchaGate from '@/components/TencentCaptchaGate.vue'
|
||||
import { resetTencentCaptchaLoaderForTest } from '@/utils/tencentCaptcha'
|
||||
import { loadTencentCaptcha, resetTencentCaptchaLoaderForTest } from '@/utils/tencentCaptcha'
|
||||
|
||||
const locale = { value: 'zh' }
|
||||
|
||||
@@ -197,6 +197,43 @@ describe('TencentCaptchaGate', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('refreshes a preloaded proof before the provider ticket expires', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
let callback: ((result: CaptchaResult) => void) | undefined
|
||||
let constructorCount = 0
|
||||
window.TCaptchaGlobal = true
|
||||
window.TencentCaptcha = class {
|
||||
constructor(...received: unknown[]) {
|
||||
constructorCount += 1
|
||||
callback = received[2] as (result: CaptchaResult) => void
|
||||
}
|
||||
show = vi.fn()
|
||||
destroy = vi.fn()
|
||||
} as unknown as typeof window.TencentCaptcha
|
||||
const wrapper = mount(TencentCaptchaGate, {
|
||||
props: { appId: '123456789', region: 'intl' }
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
callback?.({ ret: 0, ticket: 'expired-ticket', randstr: 'expired-rand' })
|
||||
await flushPromises()
|
||||
vi.advanceTimersByTime(4 * 60 * 1000)
|
||||
|
||||
const verification = wrapper.vm.verify()
|
||||
await flushPromises()
|
||||
expect(constructorCount).toBe(2)
|
||||
|
||||
callback?.({ ret: 0, ticket: 'fresh-ticket', randstr: 'fresh-rand' })
|
||||
await expect(verification).resolves.toEqual({
|
||||
ticket: 'fresh-ticket',
|
||||
randstr: 'fresh-rand'
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the three-argument form for the Chinese mainland site', async () => {
|
||||
const args: unknown[][] = []
|
||||
window.TencentCaptcha = class {
|
||||
@@ -248,6 +285,26 @@ describe('TencentCaptchaGate', () => {
|
||||
).not.toBeNull()
|
||||
})
|
||||
|
||||
it('does not inject a second SDK when the region changes in one page', async () => {
|
||||
const constructor = class {
|
||||
constructor() {}
|
||||
show = vi.fn()
|
||||
destroy = vi.fn()
|
||||
} as unknown as typeof window.TencentCaptcha
|
||||
|
||||
const firstLoad = loadTencentCaptcha('cn')
|
||||
const firstScript = document.head.querySelector<HTMLScriptElement>('script[src*="TJCaptcha.js"]')
|
||||
expect(firstScript).not.toBeNull()
|
||||
window.TencentCaptcha = constructor
|
||||
firstScript?.dispatchEvent(new Event('load'))
|
||||
await expect(firstLoad).resolves.toBe(constructor)
|
||||
|
||||
await expect(loadTencentCaptcha('intl')).rejects.toThrow(
|
||||
'Tencent Captcha region changed; reload the page to apply it'
|
||||
)
|
||||
expect(document.head.querySelector('script[src*="TJNCaptcha-global.js"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('settles a pending verification when reset', async () => {
|
||||
const destroy = vi.fn()
|
||||
window.TencentCaptcha = class {
|
||||
|
||||
@@ -79,6 +79,11 @@ export function loadTencentCaptcha(
|
||||
if (window.TencentCaptcha && (loadedRegion === region || globalRegion === region)) {
|
||||
return Promise.resolve(window.TencentCaptcha)
|
||||
}
|
||||
if (window.TencentCaptcha && loadedRegion !== null && loadedRegion !== region) {
|
||||
// 两个站点共享 TencentCaptcha 全局且构造签名不兼容,不能在同一页面注入第二份 SDK。
|
||||
// 管理员切换区域后由部署流程刷新页面,刷新前直接失败可避免全局构造函数被污染。
|
||||
return Promise.reject(new Error('Tencent Captcha region changed; reload the page to apply it'))
|
||||
}
|
||||
if (scriptPromise && loadedRegion === region) return scriptPromise
|
||||
|
||||
loadedRegion = region
|
||||
|
||||
Reference in New Issue
Block a user