鸿蒙系统中微信网页 JS-SDK 由 iframe 导致 invalid signature 和 permission denied 报错

问题描述

当在鸿蒙系统中使用微信网页 JS-SDK 时,如果页面中存在 iframe,且 iframe 加载与当前页面不同的 url,会导致微信 JS-SDK 报错 invalid signaturepermission denied

此问题的根源在于鸿蒙微信的 bug,已向官方反馈问题,详见

临时解决方案

通过分析问题的成因,可以发现微信 JS-SDK 的签名验证依赖于当前页面的 url,而在鸿蒙系统微信获取的当前页面 url 会因为 iframe 的加载而改变,导致签名验证失败。故我们可以在调用失败后插入一个与当前页面 url 相同的 iframe 来欺骗微信 🤡。

1. 劫持微信 JS-SDK 的调用

通过 navigator.userAgent 是否同时包含 'ArkWeb'MicroMessenger' 判断当前网页应用的运行环境是否为鸿蒙微信。

如果是鸿蒙微信,那么在引入微信 JS-SDK 后,添加下面的代码劫持微信 JS-SDK 的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*
hack: https://developers.weixin.qq.com/community/develop/doc/000c480314c5f0b8368360ed161400?highLine=%25E9%25B8%25BF%25E8%2592%2599%25E9%2580%2582%25E9%2585%258D
纯血鸿蒙微信存在 bug, iframe 加载与当前 url 不同的页面会影响签名校验
这里通过在调用失败后插入一个与当前 url 相同的 iframe 来欺骗微信
*/

// 这里只列出了部分 api,实际项目中请根据需要修改,此列表可直接在 JS-SDK 初始化时传给 jsApiList 配置项
export const ENABLED_WECHAT_JSSDK_API_LIST = [
'getLocation',
'onMenuShareAppMessage',
'onMenuShareQQ',
'chooseImage',
'uploadImage',
'previewImage',
'hideOptionMenu',
'showOptionMenu',
'hideMenuItems',
'showMenuItems',
'hideAllNonBaseMenuItem',
'showAllNonBaseMenuItem',
'scanQRCode',
]

const _wx = window.wx

// 标记 hack 后重试调用
const HACK_RETRY_CALL_TAG = Symbol()
const HACK_RETRY_COUNT_TAG = Symbol()
const MAX_HACK_RETRY = 5
// 处理并发调用,插入 hack iframe 时让后续调用等待
let hackPending: Promise<void> | null = null
let hackPendingResolve: (() => void) | null = null

function resolveHackPending() {
if (!hackPending) return
hackPending = null
hackPendingResolve?.()
hackPendingResolve = null
}

function hackWechat() {
return new Promise((resolve) => {
const iframe = document.createElement('iframe')
// #hack-wechat-harmonyos 用于标记为 hack iframe 加载,提前停止解析避免资源浪费
iframe.src =
window.location.href.split('#')[0] + '#hack-wechat-harmonyos'
iframe.style.display = 'none'

// 不能使用 iframe.onload 事件来判断 iframe 加载完成,因为页面中会通过 window.stop() 阻止后续解析
window.addEventListener('message', function handler(event) {
if (event.data?.type !== 'hackWechatHarmonyosReady') return

iframe.remove()
window.removeEventListener('message', handler)
// 直接调用大概率失败,延迟一下
setTimeout(resolve, 10)
})

document.body.append(iframe)
})
}

window.wx = new Proxy<any>(
{},
{
get(_, prop: any, receiver) {
if (!ENABLED_WECHAT_JSSDK_API_LIST.includes(prop)) {
return (_wx as any)[prop]
}

return async (options: any) => {
options ??= {}
let skipComplete = false
// 等待 hack 完成,放过 hack 后重试调用
if (!options[HACK_RETRY_CALL_TAG]) {
await hackPending
}

const hackOptions = {
...options,
cancel(...args: any[]) {
if (options[HACK_RETRY_CALL_TAG]) {
resolveHackPending()
}
options.cancel?.(...args)
},
success(...args: any[]) {
// 重试调用成功后放行
if (options[HACK_RETRY_CALL_TAG]) {
resolveHackPending()
}
options.success?.(...args)
},
async fail(...args: any[]) {
const errMsg = args[0]?.errMsg
if (errMsg?.includes('fail cancel')) {
if (options[HACK_RETRY_CALL_TAG]) {
resolveHackPending()
}
options.cancel?.(...args)
return
}
if (
!errMsg.endsWith(':fail invalid signature') &&
!errMsg.endsWith(':permission denied')
) {
if (options[HACK_RETRY_CALL_TAG]) {
resolveHackPending()
}
options.fail?.(...args)
return
}

const retryCount =
(options[HACK_RETRY_COUNT_TAG] as number | undefined) ?? 0
if (retryCount >= MAX_HACK_RETRY) {
if (options[HACK_RETRY_CALL_TAG]) {
resolveHackPending()
}
options.fail?.(...args)
return
}

// 等待 hack 完成,放过 hack 后重试调用
if (hackPending && !options[HACK_RETRY_CALL_TAG]) {
await hackPending
receiver[prop](options)
return
}

skipComplete = true
hackPending = new Promise((resolve) => {
hackPendingResolve = resolve
})

await hackWechat()
receiver[prop]({
...options,
[HACK_RETRY_CALL_TAG]: true,
[HACK_RETRY_COUNT_TAG]: retryCount + 1,
})
},
complete(...args: any[]) {
if (skipComplete) return
options.complete?.(...args)
},
}

;(_wx as any)[prop](hackOptions)
}
},
},
)

2. 减少插入额外 iframe 导致的性能开销

插入与当前页面 url 相同的 iframe 会导致额外的性能开销及资源浪费,那么我们怎么能将其影响降到最低呢?以我实际项目为例:

项目介绍

项目为使用 Vue 开发的单页应用,所有的 url 都会通过 index.html 来处理。

解决方案

index.html 中添加如下代码,当检测到当前页面是 hack iframe 时(通过判断 hash 是否为 #hack-wechat-harmonyos),使用 window.stop() 停止后续解析并通过 postMessage 通知父窗口已准备就绪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />

<script>
if (window.parent !== window && location.hash === '#hack-wechat-harmonyos') {
window.stop();
window.parent.postMessage({ type: 'hackWechatHarmonyosReady' }, '*');
}
</script>

...

鸿蒙系统中微信网页 JS-SDK 由 iframe 导致 invalid signature 和 permission denied 报错
https://sun79.github.io/2025/06/25/harmonyos-wechat-iframe-bug/
作者
Sun79
发布于
2025年6月25日
许可协议