70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
/**
|
||
* 企微 WwLogin:iframe 内授权完成后通过 postMessage 把「完整回调 URL」交给父窗口,
|
||
* 随后官方脚本会执行 window.location.href = url 触发整页刷新。
|
||
* 部分环境下刷新后地址栏没有 code,Vue 不会走 URL 分支 → bindWorkWechat 从不发起。
|
||
* 在 window 上以 capture 监听 message,抢先解析 code 并 stopImmediatePropagation,避免整页跳转。
|
||
*/
|
||
|
||
const DEFAULT_STATE = 'bind_wxwork'
|
||
|
||
function isWecomPostMessageOrigin(origin: string): boolean {
|
||
return origin.includes('work.weixin.qq.com') || origin.includes('tencent.com')
|
||
}
|
||
|
||
function parseOAuthAccept(
|
||
params: URLSearchParams,
|
||
wxBindState: string
|
||
): { code: string } | null {
|
||
const wxCode = (params.get('code') || '').trim()
|
||
if (!wxCode) return null
|
||
const wxState = (params.get('state') ?? '').trim()
|
||
const stateOk =
|
||
wxState === wxBindState ||
|
||
wxState === 'admin_bind_wx' ||
|
||
wxState.toLowerCase() === wxBindState.toLowerCase()
|
||
const bindFlagOk = params.get('bind_wxwork') === '1'
|
||
const implicitOk = wxState === ''
|
||
if (!(stateOk || bindFlagOk || implicitOk)) return null
|
||
return { code: wxCode }
|
||
}
|
||
|
||
export type WecomOauthCaptureOptions = {
|
||
/** 回调 URL 的 pathname 须包含此片段,如 bind-work-wechat、user/setting */
|
||
pathIncludes: string
|
||
lockKey: string
|
||
wxBindState?: string
|
||
onCode: (code: string) => void | Promise<void>
|
||
}
|
||
|
||
/**
|
||
* @returns 卸载时调用以 removeEventListener
|
||
*/
|
||
export function attachWecomOAuthMessageCapture(options: WecomOauthCaptureOptions): () => void {
|
||
const wxBindState = options.wxBindState ?? DEFAULT_STATE
|
||
const handler = (event: MessageEvent) => {
|
||
if (!isWecomPostMessageOrigin(event.origin)) return
|
||
const data = event.data
|
||
if (typeof data !== 'string' || !/^https?:\/\//i.test(data)) return
|
||
let u: URL
|
||
try {
|
||
u = new URL(data)
|
||
} catch {
|
||
return
|
||
}
|
||
if (u.host !== window.location.host) return
|
||
if (!u.pathname.includes(options.pathIncludes)) return
|
||
|
||
const parsed = parseOAuthAccept(new URLSearchParams(u.search), wxBindState)
|
||
if (!parsed) return
|
||
if (sessionStorage.getItem(options.lockKey) === parsed.code) return
|
||
|
||
event.stopImmediatePropagation()
|
||
sessionStorage.setItem(options.lockKey, parsed.code)
|
||
void Promise.resolve(options.onCode(parsed.code)).finally(() => {
|
||
sessionStorage.removeItem(options.lockKey)
|
||
})
|
||
}
|
||
window.addEventListener('message', handler, true)
|
||
return () => window.removeEventListener('message', handler, true)
|
||
}
|