引子:几个反复出现的提问
社区里关于原生与 H5 交互的提问,翻来覆去大致是这几类:
-
「H5 里的请求怎么自动带上 App 的登录态,总不能让用户再登一次?」
-
「H5 点个按钮要拉起原生页面,token 直接拼在 URL 后面传可以吗?」
-
「原生退出登录了,H5 怎么立刻跟着失效?」
-
「H5 支付回调要拉起收银台,订单号能不能由前端传?」
-
「第三方活动页里放了个按钮,能不能复用同一个 WebView 容器?」
-
「切换账号后,之前预加载的 H5 为什么还在显示上一个人的数据?」
-
「埋点要带用户 ID,是直接把 token 给前端,还是另给一个 ID?」
讨论通常从「token 放哪儿」开始,也停在那儿。于是同一类缺陷换个位置又冒出来:这次是 URL 里的 token,下次是退出后没清干净的 Cookie,再下次是 H5 传来的一个路由名被直接塞进页面栈。
七个问题指向同一件事:原生和 H5 之间,信任到哪里为止,状态由谁做主。下面把提问拆成三类,给出一张风险矩阵和一份评审清单,再对照一套典型实现,把十个常见场景过一遍。
一、把高频问法拆成三类命题
1.1 三个命题,难度完全不同
身份贯通和失效传播其实是同一件事的两端。一个凭据方案好不好,主要看回收能力,而不是传递能力。
任何一次原生与 H5 的交互都能归进其中一类或几类,组合方式决定了风险落在哪。
1.2 四个信任域
很多人把「原生到 H5」想成一根管道,实际上中间隔了两次界:
原生进程 ──①──> Web 内核(Cookie 库 / 网络栈) ──②──> 页面脚本 ──③──> 服务端
可信 半可信(系统管理) 不可信 可信
两个常见误判都来自忽略中间那道界:
-
以为「Cookie 注入到内核」就安全了。脚本域能不能读,取决于有没有
HttpOnly,与注入方式无关。 -
以为「桥只在我打开的页面里生效」。桥对象注册在整个 WebView 实例上,实例里加载的任何页面都能调到同一个桥对象,导航到站外之后也一样,除非用
permission白名单收敛(见 4.1 ④)。
1.3 四条通道,一张风险矩阵
跨域传递只有四条路。把它们按「谁能看见 / 是否落盘 / 脚本可读 / 能否撤销」摊开,选型就有了尺子:
选型时该看最后两列。URL 和 HttpOnly Cookie 的差距不在「能不能被看见」,而在能不能收回。JSBridge 常被当成更安全的替代,但它的可撤销性比 Cookie 更差:返回值一旦进了脚本域,原生就管不着那些副本了。
1.4 三条原则
矩阵里的约束可以收成三条:
-
凭据默认不下发到脚本域。能只放在内核就不放脚本,能完全不传递最好。
-
能力按意图授权,不按对象或路由授权。H5 的任何入参都不作为可信输入。
-
状态只有一个源,其余都是副本。失效必须扇出到每一个副本,并且副本要能自证过期。
第三条展开说:副本是必然存在的,Cookie 一份、H5 的 localStorage 可能又存一份、内存缓存一份、服务端会话一份。所以「立刻失效」不是删掉一份就完事,要三层联动:服务端作废让残留副本即使还在也无效,本地清理让后续请求不再携带,通知页面自清让界面立即对上。这是纵深防御,不是重复劳动。
二、身份贯通:凭据怎么给,更要怎么收
2.1 一个流传很广的说法
社区里有一种说法:
至于 token 是拼在 URL 后面还是放在 cookie 都一样的。对于不怀好心的人来说都是一样的透明。
这句话只对了一半,错的那一半很关键。
对的部分:如果 Cookie 没加 HttpOnly,页面里任何一段 JS(包括被 XSS 注入的)都能用 document.cookie 读走。这种情况下,Cookie 和 URL 在「可被脚本读取」上确实等价。
错的部分:它漏掉了不依赖脚本的泄漏路径。
Referer 和 access log 这两条是自动发生的:Referer 会把查询串带给页面里的每一个第三方资源(统计、CDN、字体),access log 里的 URL 会长期躺在日志平台上。跟攻击者有没有心无关,是设计本身在往外送数据。
准确结论:URL 传凭据会把凭据交给所有能看日志、看历史、看 Referer 的系统;HttpOnly Cookie 只暴露给服务端和网络链路。两者不是一回事。
2.2 五种模式,以及怎么选
按暴露面从小到大,实际有五档:
打个比方:
A 把钥匙贴在门上,B 把钥匙交给客人保管,C 把钥匙放进只有门能开的锁孔,D 只给一张一次性的临时门禁卡,E 干脆不给钥匙,客人要进门时由主人替他刷。
选型不用背表,按两个问题走即可:
H5 与 App 后端是否同域(或可控的父子域)?
├─ 否 → E(请求代理)或 D(一次性票据)
└─ 是 → 凭据是否允许落到任何持久化介质?
├─ 否 → D
└─ 是 → C,且必须 HttpOnly + Secure + 正确的 SameSite
A 在任何情况下都不该用于凭据;它可以传非敏感的业务参数(如 skuId、活动 ID)。B 适合传非敏感、可公开的用户态(昵称、主题、语言),不适合传 token。
2.3 方案 C 的两个隐藏前提
域关系可控是第一个前提。Cookie 能自动携带,靠的是同源策略。H5 与 Cookie 的 domain 必须匹配;如果 H5 与 App 后端不在同一域名下,服务端要配合做跨域 Cookie 写入,复杂度会迅速逼近方案 E。这时候请求拦截代理反而更省事:原生代发请求并附加鉴权头,不受同源策略约束。
请求代理值得单独说:ArkTS 侧用 onInterceptRequest 事件拦截 H5 数据请求,Native 侧对应 ArkWeb_WebSchemeHandler 系列 C API,原生用 @kit.NetworkKit 代发并附加 Authorization。token 全程只在原生侧,H5 完全无感知。
-
H5 里没有凭据可窃取,泄漏路径直接消失
-
token 刷新、过期重试由原生统一管理,从根上解决「过期不同步」
-
不受同源策略约束
代价是所有 H5 请求都要过原生一层,要处理请求体解析、流式响应、文件下载等边界。适合「安全优先且请求形态可控」的场景,不适合有大量媒体流或第三方 SDK 自由发请求的页面。
第二个前提是失效联动,清的地方也要对,见 3.2。
2.4 把「生命周期」写成一张表
「URL 泄露」和「过期不同步」常被当成两件事,其实都在问同一个问题:凭据传出去之后,怎么保证随时能作废。
四条里失效最容易跳过,后果也最重。前三条做不好,用户大概只是觉得有点不安全;失效做不好,就是别人已经拿我账号下单了。
三、能力开放与失效传播:同一枚硬币的两面
这两件事常被分开讨论,但前提是一样的:原生握着状态和权限,H5 只是副本和调用方。这个前提一破,两边都会出问题。
3.1 「拉起一个原生页」不是跳转需求,是一次能力授权
「H5 点个按钮调起原生页面」这类需求,最省事的实现是开放一个 goToAppPage(pageUrl)。这也是不少线上实现的做法。问题在于,它把「能跳哪些页」的决定权交给了 H5。
按意图授权,一次 H5 → 原生的调用应该过四层:
能力本身也要分级,不同级别要求不同:
两种写法对比:
// 反例:决定权在 H5 手里
goToAppPage(pageUrl: string, params?: ESObject) {
RouterManager.push({ builderName: pageUrl, param: params });
}
// 正例:决定权在原生手里
openMyOrders(): void {
RouterManager.push({
routerName: RouterConstants.ENTRY_HAP,
builderName: 'mall:StoreMyOrderPage', // 常量,不接受 H5 入参
});
}
openProductDetail(skuId: string): void {
if (!/^[A-Za-z0-9_-]{1,32}$/.test(skuId)) {
return; // 参数 schema 校验,静默拒绝
}
RouterManager.push({
routerName: RouterConstants.ENTRY_HAP,
builderName: 'mall:MallStoreDetailsPage',
param: { skuId: skuId },
});
}
第二种写法还顺带解决了一个隐性问题:H5 不需要知道原生路由表长什么样,路由重构不会打断线上 H5。
3.2 失效传播:三层联动,顺序不能反
「清 Cookie + runJavaScript 通知」方向对,但少了一层,也没说顺序。完整链路:
① 调服务端登出接口 → 会话立即作废(安全层,根本保障)
② 清 WebView Cookie → 后续请求不再携带(本地层)
③ runJavaScript 通知 H5 → 立即跳未登录页(体验层)
先清本地再调服务端,中间有一个窗口期凭据仍然有效。凭据如果已经被复制出去(比如通过 Referer 泄漏),只有服务端作废能真正断掉。
多数实现只覆盖「用户主动退出」,还有三处必须补齐:
第一处是被动踢下线,走同一套。token 在别处登录被挤下线、服务端主动作废时,用户根本没点退出按钮。只监听退出按钮,H5 会一直保持「看起来已登录」。工程里通常已有踢下线事件(例如 APP_LOGIN_KICK_OUT 一类广播),缺的是把它接到 WebView。
第二处是 H5 侧的本地缓存,要一起清。
window.onNativeLogout = function (reason) {
localStorage.clear();
sessionStorage.clear();
if (window.caches) {
caches.keys().then(keys => keys.forEach(k => caches.delete(k)));
}
location.replace('/mall/login-required?reason=' + reason);
};
很多 H5 会把 token 或用户信息存进 localStorage。只清 Cookie 不通知页面,会出现「Cookie 没了但页面还显示已登录、点按钮报 401」的割裂状态。
第三处是幂等与竞态。主动退出、被动踢下线、网络层 401 三个入口可能在几秒内同时触发,扇出动作必须幂等。H5 如果已经带着旧 Cookie 把请求发出去了,本地清理来不及,服务端作废是唯一兜底。第 ① 步不能省。
3.3 两个容易被当成「边缘情况」的主流程
多 WebView 实例。clearAllCookiesSync() 是全局的。App 里若有活动页、客服页、支付中间页各自依赖不同域的 Cookie,一刀清空会把它们一起登出,而且现象分散、很难归因。会话 Cookie + clearSessionCookieSync() 是更精准的选择。要注意的是,ArkTS 侧没有按域、按名删除 Cookie 的接口,清理粒度只有「会话」和「全部」两档,所以域的规划要在设计期做:登录态统一用会话 Cookie,让会话级清理足够精准;确需强隔离的独立业务(如第三方落地页)可以用隐私模式 WebView,incognito 模式的 Cookie 只存内存、与正常模式隔离、组件销毁即清,但要接受不落盘、页面重开需重新注入的代价。
切换账号不是登出,是「换身份」。它要求「清旧态 + 注新态」原子完成,中间不能让 H5 用旧 Cookie 向新账号发请求。两个可行做法:换号时销毁并重建 WebView(最简单可靠),或者用一次性票据让 H5 在新会话建立前完全无凭据可用。预加载的 WebView 尤其危险:它注入的是上一个账号的 Cookie,复用前必须校验账号一致性,否则就是串号。
四、实现细节与 API 校准
框架要落地还得过 API 这一关。网上流传的示例里有几处会让人照着写、跑起来了、但没生效,下面逐条校准,再补几条同样容易踩的细节。代码示例只说明调用时机和参数结构,不是可直接编译的完整单元。
4.1 四处常见的 API 误用
① 注入时机:webview.once('webInited') 不是最稳的写法。
// 官方 FAQ 给出的写法
webview.once('webInited', () => {
webview.WebCookieManager.configCookieSync(/* ... */)
})
这套写法对「进程内第一个 WebView」成立(见官方 FAQ:登录信息的 cookie 应该在什么时机注入),但有两个坑。其一,once() 是全局一次性事件:内核已完成初始化后,第二个及之后打开的 WebView 再执行这段代码,回调不再触发,注入静默失效。这是接口说明写明的行为(When other Web components are loaded in the same application, once() is not triggered)。其二,WebCookieManager 是所有 Web 组件共享的全局单例,configCookieSync 调用本身就会触发内核初始化,并不依赖 webInited 事件。真正需要保证的是「注入发生在该实例的首次请求之前」,不是「事件已触发」。
更稳的做法是放进组件自己的 onControllerAttached,并且在 loadUrl 之前执行。对每一个 WebView 实例都成立,首个 HTML 请求就能带上登录态:
Web({ src: '', controller: this.controller })
.onControllerAttached(() => {
webview.WebCookieManager.configCookieSync(domain, cookieValue);
this.controller.loadUrl(this.mallUrl);
})
这也是放 setUrlTrustList 的合理位置:该接口要求 controller 已关联组件(否则报 17100001),与 Cookie 注入一样,都应在 onControllerAttached 里、loadUrl 之前完成。
② 刷盘:写入成功不等于已持久化,必要时显式 saveCookieAsync()。
多数示例没提刷盘,而这正是「注入成功但偶发失效」的成因。官方说明是 cookies 每 30 秒周期性保存到磁盘,configCookieSync 只写进内核内存。进程在这 30 秒内被杀(用户划掉、系统回收),Cookie 就丢了。要立刻持久化得显式刷盘:
webview.WebCookieManager.configCookieSync(domain, cookieValue);
await webview.WebCookieManager.saveCookieAsync(); // 自 API 9 起提供
版本对应关系容易记错,这里按 WebCookieManager 接口文档 校准一次:saveCookieAsync() 自 API 9 就有;同步版本 saveCookieSync() 反而是 API 15 才新增,且会阻塞调用线程直到落盘完成。API 9→11 之间被废弃的是另一批旧接口:setCookie / getCookie / deleteEntireCookie / deleteSessionCookie。网上「saveCookieSync 已更名」的说法是错的。
③ 退出时清 Cookie:方法名和清理范围都容易写错。
选哪个取决于 Cookie 类型:登录态写成会话 Cookie(不带 expires / Max-Age)用 clearSessionCookieSync(),精准且不影响其他域;写成持久 Cookie(带 expires / Max-Age)则会话清理方法清不掉。ArkTS 侧没有按域、按名删除单条 Cookie 的接口,只能退到 clearAllCookiesSync() 全量清理,代价是把其他业务的登录态一起带走。
④ permission 白名单:参数给对了,写法是空的。
const BRIDGE_PERMISSION: string = JSON.stringify({
javascriptProxyPermission: {
urlPermissionList: [
{ scheme: 'https', host: 'mall.example.com', port: '', path: '' }
],
methodList: [] // ← 空数组
}
});
methodList: [] 表示没有为任何方法单独配置权限。permission 分两层:urlPermissionList 是对象级(匹配则授权该对象的所有方法),methodList 是方法级(针对单个方法收紧)。空数组意味着方法级管控没生效,对象级仍然放行,但「只允许 getToken 从商城域调用」这类精细控制就没了。正确做法是给敏感方法单独配:
const BRIDGE_PERMISSION: string = JSON.stringify({
javascriptProxyPermission: {
urlPermissionList: [
{ scheme: 'https', host: 'mall.example.com', port: '', path: '' }
],
methodList: [
{
methodName: 'getToken',
urlPermissionList: [
{ scheme: 'https', host: 'mall.example.com', port: '', path: '' }
]
}
]
}
});
匹配规则三条:scheme 和 host 精确匹配、不能为空、不支持通配符(通配符是新版本 setUrlTrustList 才支持);port 留空表示不检查端口;path 是前缀匹配,留空表示不检查路径。还有一条容易漏的交集原则:方法 A 的最终白名单是对象级与方法级的交集,两侧必须同时配,只配一侧该方法就调不通。以上均出自官方 Jsb Permission Denied 问题定位。
4.2 八条实现细节
-
SameSite默认是 Lax,不是 None。跨站跳转还想携带 Cookie 要显式设None; Secure,否则 Cookie 不会发送。这一条要跟 H5 确认跳转来源后再定。 -
Cookie 每 30 秒自动落盘,崩溃会丢最近 30 秒。另外官方说明:在 PC / 二合一 / 平板上,即使是会话 Cookie 也不会被
saveCookieAsync()写入磁盘。多设备形态要单独验证。 -
configCookieSync的「多条 Cookie」要分清是哪种多。官方两处说法看着矛盾:「Cookie 注入时机」FAQ 的示例把多个键值对塞进一次调用(configCookie('', 'a=b,c=d,e=f'),还是用逗号拼的);而接口说明和「多个 Cookie 如何进行批量设置」FAQ 都明确「接口不支持一次性批量设置,应多次调用」。原因是value必须遵循Set-Cookie响应头格式,key=value后面跟的是分号分隔的属性列表(如key=value; Max-Age=100; Secure)。所以c=d、e=f会被内核当成前一条 Cookie 的未知属性,逗号拼法更糟:逗号是合法的 Cookie 值字符,整串会被当成a一个 Cookie 的值,实际都只写入一个 Cookie。// 反例:看着像写了三个,实际只写入一个 configCookieSync(url, 'a=b;c=d;e=f'); // 正例:多个 Cookie 就多次调用 configCookieSync(url, 'a=b; Path=/; Secure'); configCookieSync(url, 'c=d; Path=/; Secure');附带规则:相同 host、path、名称的 Cookie 会被新值替换;已过期的 Cookie 不会被存储;带
Secure属性时 url 必须是https://,否则写入失败;要覆盖已有的HttpOnlyCookie,value里同样要带HttpOnly属性。最后这条是「Cookie 注入后 H5 还是未登录」最隐蔽的成因之一。 -
methodList和asyncMethodList不能混。methodList放同步方法,返回值直接回传 H5;asyncMethodList放返回 Promise 的方法,其resolve值才回传。把异步方法误登记到methodList,H5 侧await bridge.xxx()的.then/await永远不触发,表现是「回调不触发」,极易被误判成「桥没注入」。排查时这两个方向要分开验证。 -
动态注册要记得
refresh()。用registerJavaScriptProxy()注册后必须调refresh()才生效,否则前端报「对象未定义」,容易误以为是方法名写错。 -
组件销毁时释放桥对象。调
deleteJavaScriptRegister(name),防内存泄漏。 -
测试用例必须覆盖「未登录态」。很多线上事故是「退出后 H5 还在用旧态」,而自测时从来都是登录状态,这条路径根本没跑过。加一条:登录 → 打开 H5 → 原生退出 → 返回 H5 → 断言处于未登录。
-
多端统一抽象一层。iOS
WKWebView、AndroidWebView、HarmonyOS ArkWeb 的 Cookie 同步机制各不相同。抽一个H5SessionBridge接口,各端分别实现,业务侧只调接口,不然同一套逻辑要在三端各踩一遍坑。
五、把框架用到别处
下面把框架抽成一份可以直接贴进评审单的清单,再套到十个场景上。
5.1 新需求来了,先过这三步
① 归类:这次交互属于身份贯通 / 能力开放 / 失效传播 里的哪几类?
② 选通道:涉及凭据就查 1.3 的风险矩阵,重点看「可撤销」列;涉及调用就查 3.1 的四层校验
③ 过清单:把 5.2 的 14 条对着需求打勾,缺哪条补哪条
5.2 通用评审清单
可以直接复制进任何 hybrid 需求的评审单。三组各四条,共 12 项,再加 2 条跨组项,合计 14 项。
身份贯通
-
[ ] URL 里没有 token、cookie、sessionId、手机号、身份证号等敏感项(非敏感业务参数可以留)
-
[ ] 凭据若落到 Web 内核,带齐
HttpOnly+Secure+ 明确的SameSite;Secure对应的 URL 是 https -
[ ] 凭据没有通过 JSBridge 返回值下发到脚本域;确实需要下发时,是一次性票据而非长期 token
-
[ ] 域关系明确(同域 / 父子域 / 跨域),跨域场景已选定代理或票据方案
能力开放
-
[ ] 桥对象注册时配置了
permission,且methodList对敏感方法单独收紧 -
[ ] 加载域名有系统级白名单(
setUrlTrustList或等价手段),覆盖location.href、302、iframe 导航 -
[ ] 暴露的是意图方法(
openMyOrders()),不是通用路由 /builderName透传 -
[ ] 所有 H5 入参做 schema 校验;能力按读 / 跳 / 写 / 敏感分级,敏感级不下放
失效传播
-
[ ] 退出、被动踢下线、网络层 401 三个入口都接到了同一套扇出逻辑,且动作幂等
-
[ ] 顺序是「服务端作废 → 清内核 Cookie → 通知页面」,没有反向
-
[ ] 通知 H5 时清了
localStorage/sessionStorage/ Cache API,界面立即切未登录 -
[ ] 会话 Cookie 用会话级清理方法,没有误用全局
clearAllCookiesSync()波及其他 WebView
再加两条跨组的,最容易在迭代中被破坏:
-
[ ] 切换账号、WebView 预加载复用前,校验了账号一致性
-
[ ] 「登录 → 打开 H5 → 退出 → 返回 H5」有用例覆盖
5.3 把清单套到十个同类场景
十行都是同一套清单在不同组合下的输出。1.4 的三条原则和 1.3 的最后两列才是判据。
5.4 这套框架什么时候不适用
有几种场景不适用:
-
纯原生页面:没有 WebView,四条通道不存在,直接走原生鉴权即可。
-
系统浏览器、小程序等不受自己控制的容器:域模型不同,无法注入 Cookie、也无法注册桥,只能走标准 OAuth / 深链回跳。
-
请求形态高度不可控的页面(WebRTC、大范围流式下载、三方 SDK 自由发请求):请求代理方案 E 的维护成本会超过收益,应转向票据方案 D。
-
完全自有、同域、同发布链路且已有严格 XSS 防护的 H5:框架仍适用,但能力分级可以适当放宽;放宽要写进评审记录,而不是默认省略。
六、对照典型实现:六个现状与三步改造
下面是一套混合应用里很常见的实现形态,具体标识和代码位置已略去,只保留影响安全判断的部分。用第五章的清单对照,六条都不通过。
6.1 URL 里直接拼 cookie 和手机号
页面加载 URL 的方法里,参数是这么拼的:
if (this.userInfoData?.userId) {
let cleanedString = this.userInfoData.name?.replace(/\n/g, '') ?? ''
const url =
this.data?.url + (this.data?.url.includes('?') ? '&' : '?') + 'userId=' +
this.userInfoData.userId +
'&userName=' + encodeURIComponent(cleanedString ?? '') +
'&userPhone=' +
this.userInfoData.phone + '&cookie=' + AppStorage.get('cookie')
this.viewModel.loadUrl(url, undefined, this.data?.needShowLoading)
}
cookie(完整登录态)和 userPhone(手机号明文)同时进了 URL。按 2.1 的泄漏表,这两项会同时进 access log 和 Referer。
6.2 JSBridge 注册方法,且能直接读到 cookie
桥对象的 methodList 里列了 多 个方法,getCookie 排在第一个。它的实现直接返回完整登录态:
let userInfo: User = {
userId: ...,
userPhone: ...,
userName: encodeURIComponent(...),
cookie: AppStorage.get('cookie') ?? '',
deviceSN: getDeviceInfo().deviceSn,
}
cookie 是 HttpOnly 的反面:凭据直接进了脚本域。而且 javaScriptProxy 注册时没有传 permission 参数,默认不做来源校验。
6.3 完全没有使用 WebCookieManager
整套实现里没有用到 configCookieSync / WebCookieManager。H5 拿登录态全靠 URL 拼接和 JSBridge。
好处是迁移到方案 C 属于纯增量改造,不用先清理存量 Cookie 逻辑。
6.4 路由能力对 H5 完全敞开
路由方法的实现:
goToAppPage(pageUrl: string, params?: ESObject) {
this.logH5Call('goToAppPage', JSON.stringify({ pageUrl, params }));
RouterManager.push({
routerName: RouterConstants.ENTRY_HAP,
builderName: pageUrl, // 直接透传 H5 入参,无白名单
param: params,
})
}
6.5 退出登录没有通知 H5 的链路
退出登录的 clearLoginInfo 只做了三件事:清内存、清部分缓存、发一个改皮肤的事件。没有任何「通知 WebView」的动作。
所以「原生退出后 H5 立刻失效」这一条没有实现。网络层其实已经会发 APP_LOGIN_KICK_OUT 一类的踢下线事件,缺的只是把它接到 WebView。
6.6 没有域名白名单
onLoadIntercept 只拦了微信支付(weixin://wap/pay)和支付宝(alipays://)两种 scheme,没有域名校验,也没有用 setUrlTrustList。桥对象注册在整个 WebView 实例上,WebView 一旦被导航到站外,那个页面同样能调到 commonWebObj.getCookie()。
6.7 三步改造,每步都能独立交付
第一步,URL 里摘掉敏感项,成本最低。把 6.1 里的 cookie 和 userPhone 从 URL 摘掉,改为 H5 按需通过桥方法取脱敏信息。改动极小,但直接消掉 Referer 和 access log 两条自动化泄漏路径。不需要服务端配合,也没有兼容风险。
第二步,Cookie 注入加失效联动。
// 1. 注入(onControllerAttached 里,loadUrl 之前)
webview.WebCookieManager.configCookieSync(
mallDomain,
`SESSION_ID=${sessionId}; Domain=${host}; Path=/; Secure; HttpOnly; SameSite=Lax`
);
await webview.WebCookieManager.saveCookieAsync();
// 2. 退出时清理(三个入口都要接)
async function onNativeLogout(reason: 'manual' | 'kicked' | 'expired') {
// 服务端失效优先(由登录模块负责)
webview.WebCookieManager.clearSessionCookieSync();
await webview.WebCookieManager.saveCookieAsync();
controller.runJavaScript(`window.onNativeLogout && window.onNativeLogout('${reason}')`);
}
要接的三个入口:
第三步,路由与桥方法白名单,可以独立推进。
// 过渡:先给 goToAppPage 加白名单,堵住任意路由跳转
private static readonly H5_ALLOWED_ROUTES: string[] = [
'mall:StoreMyOrderPage',
'mall:MallStoreDetailsPage',
'service:ChargeOrderListPage',
];
goToAppPage(pageUrl: string, params?: ESObject) {
if (!H5_ALLOWED_ROUTES.includes(pageUrl)) {
webViewLogger.warn(`H5 尝试跳转未授权路由: ${pageUrl}`);
return; // 静默拒绝,不给攻击者反馈
}
RouterManager.push({ routerName: RouterConstants.ENTRY_HAP, builderName: pageUrl, param: params });
}
// 目标:逐步废弃 goToAppPage,换成 3.1 的意图方法
Web 组件侧的配置:
Web({ src: '', controller: this.controller })
// 域名信任列表:比 onLoadIntercept 手工判断更彻底
.onControllerAttached(() => {
const trustList = '{"UrlPermissionList":[{"scheme":"https","host":"mall.example.com"}]}';
this.controller.setUrlTrustList(trustList);
})
// 桥方法来源白名单
.javaScriptProxy({
object: this.viewModel.commonWebObj,
name: this.viewModel.name,
methodList: this.viewModel.methodList,
controller: this.viewModel.controller,
permission: BRIDGE_PERMISSION, // 见 4.1 ④ 的正确结构
})
setUrlTrustList 的优势是系统级全量校验:所有导航(包括 H5 内部的 location.href 跳转、302 重定向)都会被拦,不依赖你在 onLoadIntercept 里是否漏写了某个分支。注意它要求 controller 已关联组件(否则报 17100001),所以同样放在 onControllerAttached 里调用;通配符支持(*.example.com)要等三参重载(allowOpaqueOrigin + supportWildcard,API 24 起)。
三步做完,清单里三组共 12 项都能打勾。剩下 2 条跨组项要单独补:切换账号 / 预加载复用前的账号一致性校验,以及「登录 → 打开 H5 → 退出 → 返回 H5」的用例覆盖。顺序按收益和成本排,不按依赖,任意一步都能先上。
七、结语:管的是生命周期,不是通道
「URL 泄露」和「token 过期不同步」看着是传输问题,落到实现上都是生命周期问题:凭据传出去之后,怎么保证随时能作废。
那句「放 URL 和放 Cookie 都一样」,如果只针对「都能被 JS 读走」,说的是对的。但 HttpOnly Cookie 消掉了四条不依赖脚本的泄漏路径,这个差别在安全评审里足以改变结论。
混合应用里的事故,多数不是某个 API 用错,而是四个域之间的信任关系没定清楚。凭据落到脚本域、通用路由下放给前端、退出时只清了一处副本,发生在三个完全不同的需求里,根子是一样的。
-
凭据默认不下发到脚本域,判断方案看能不能收回;
-
能力按意图授权,不按路由授权;
-
状态只有一个源,失效要扇出到每一个副本,服务端作废是最后兜底。
5.2 那 14 条清单可以直接贴进评审单。下次再遇到「H5 支付回调怎么接」「切换账号后页面为什么串了」,至少不用从「token 放哪儿」重新讲一遍。
附录 A:API 名称与版本速查
技术文章容易在这里翻车,单独列一张表(版本信息已按 SDK .d.ts 核对):
不同 API 版本的方法名与签名存在差异,尤其
configCookieSync的参数个数和clearSessionCookieSync的可用版本。落地前请以目标 SDK 的.d.ts为准,不要只依据网络文章。