App 内嵌 H5 的信任边界设计(ArkWeb/HarmonyOS 实战)

引子:几个反复出现的提问

社区里关于原生与 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」想成一根管道,实际上中间隔了两次界:

bash 复制代码
原生进程  ──①──>  Web 内核(Cookie 库 / 网络栈)  ──②──>  页面脚本  ──③──>  服务端
  可信              半可信(系统管理)                 不可信            可信

两个常见误判都来自忽略中间那道界:

  • 以为「Cookie 注入到内核」就安全了。脚本域能不能读,取决于有没有 HttpOnly,与注入方式无关。

  • 以为「桥只在我打开的页面里生效」。桥对象注册在整个 WebView 实例上,实例里加载的任何页面都能调到同一个桥对象,导航到站外之后也一样,除非用 permission 白名单收敛(见 4.1 ④)。

1.3 四条通道,一张风险矩阵

跨域传递只有四条路。把它们按「谁能看见 / 是否落盘 / 脚本可读 / 能否撤销」摊开,选型就有了尺子:

选型时该看最后两列。URL 和 HttpOnly Cookie 的差距不在「能不能被看见」,而在能不能收回。JSBridge 常被当成更安全的替代,但它的可撤销性比 Cookie 更差:返回值一旦进了脚本域,原生就管不着那些副本了。

1.4 三条原则

矩阵里的约束可以收成三条:

  1. 凭据默认不下发到脚本域。能只放在内核就不放脚本,能完全不传递最好。

  2. 能力按意图授权,不按对象或路由授权。H5 的任何入参都不作为可信输入。

  3. 状态只有一个源,其余都是副本。失效必须扇出到每一个副本,并且副本要能自证过期。

第三条展开说:副本是必然存在的,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 干脆不给钥匙,客人要进门时由主人替他刷。

选型不用背表,按两个问题走即可:

bash 复制代码
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 → 原生的调用应该过四层:

能力本身也要分级,不同级别要求不同:

两种写法对比:

bash 复制代码
// 反例:决定权在 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 通知」方向对,但少了一层,也没说顺序。完整链路:

bash 复制代码
① 调服务端登出接口  →  会话立即作废(安全层,根本保障)
② 清 WebView Cookie →  后续请求不再携带(本地层)
③ runJavaScript 通知 H5 →  立即跳未登录页(体验层)

先清本地再调服务端,中间有一个窗口期凭据仍然有效。凭据如果已经被复制出去(比如通过 Referer 泄漏),只有服务端作废能真正断掉。

多数实现只覆盖「用户主动退出」,还有三处必须补齐:

第一处是被动踢下线,走同一套。token 在别处登录被挤下线、服务端主动作废时,用户根本没点退出按钮。只监听退出按钮,H5 会一直保持「看起来已登录」。工程里通常已有踢下线事件(例如 APP_LOGIN_KICK_OUT 一类广播),缺的是把它接到 WebView。

第二处是 H5 侧的本地缓存,要一起清。

bash 复制代码
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') 不是最稳的写法。

bash 复制代码
// 官方 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 请求就能带上登录态:

bash 复制代码
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 就丢了。要立刻持久化得显式刷盘:

bash 复制代码
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 白名单:参数给对了,写法是空的。

bash 复制代码
const BRIDGE_PERMISSION: string = JSON.stringify({
  javascriptProxyPermission: {
    urlPermissionList: [
      { scheme: 'https', host: 'mall.example.com', port: '', path: '' }
    ],
    methodList: []      // ← 空数组
  }
});

methodList: [] 表示没有为任何方法单独配置权限。permission 分两层:urlPermissionList 是对象级(匹配则授权该对象的所有方法),methodList 是方法级(针对单个方法收紧)。空数组意味着方法级管控没生效,对象级仍然放行,但「只允许 getToken 从商城域调用」这类精细控制就没了。正确做法是给敏感方法单独配:

bash 复制代码
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 八条实现细节

  1. SameSite 默认是 Lax,不是 None。跨站跳转还想携带 Cookie 要显式设 None; Secure,否则 Cookie 不会发送。这一条要跟 H5 确认跳转来源后再定。

  2. Cookie 每 30 秒自动落盘,崩溃会丢最近 30 秒。另外官方说明:在 PC / 二合一 / 平板上,即使是会话 Cookie 也不会被 saveCookieAsync() 写入磁盘。多设备形态要单独验证。

  3. 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://,否则写入失败;要覆盖已有的 HttpOnly Cookie,value 里同样要带 HttpOnly 属性。最后这条是「Cookie 注入后 H5 还是未登录」最隐蔽的成因之一。

  4. methodList 和 asyncMethodList 不能混。methodList 放同步方法,返回值直接回传 H5;asyncMethodList 放返回 Promise 的方法,其 resolve 值才回传。把异步方法误登记到 methodList,H5 侧 await bridge.xxx() 的 .then / await 永远不触发,表现是「回调不触发」,极易被误判成「桥没注入」。排查时这两个方向要分开验证。

  5. 动态注册要记得 refresh()。用 registerJavaScriptProxy() 注册后必须调 refresh() 才生效,否则前端报「对象未定义」,容易误以为是方法名写错。

  6. 组件销毁时释放桥对象。调 deleteJavaScriptRegister(name),防内存泄漏。

  7. 测试用例必须覆盖「未登录态」。很多线上事故是「退出后 H5 还在用旧态」,而自测时从来都是登录状态,这条路径根本没跑过。加一条:登录 → 打开 H5 → 原生退出 → 返回 H5 → 断言处于未登录。

  8. 多端统一抽象一层。iOS WKWebView、Android WebView、HarmonyOS ArkWeb 的 Cookie 同步机制各不相同。抽一个 H5SessionBridge 接口,各端分别实现,业务侧只调接口,不然同一套逻辑要在三端各踩一遍坑。


五、把框架用到别处

下面把框架抽成一份可以直接贴进评审单的清单,再套到十个场景上。

5.1 新需求来了,先过这三步

bash 复制代码
① 归类:这次交互属于身份贯通 / 能力开放 / 失效传播 里的哪几类?
② 选通道:涉及凭据就查 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:框架仍适用,但能力分级可以适当放宽;放宽要写进评审记录,而不是默认省略。


六、对照典型实现:六个现状与三步改造

下面是一套混合应用里很常见的实现形态,具体标识和代码位置已略去,只保留影响安全判断的部分。用第五章的清单对照,六条都不通过。

页面加载 URL 的方法里,参数是这么拼的:

bash 复制代码
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。

桥对象的 methodList 里列了 多 个方法,getCookie 排在第一个。它的实现直接返回完整登录态:

bash 复制代码
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 完全敞开

路由方法的实现:

bash 复制代码
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 注入加失效联动。

bash 复制代码
// 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}')`);
}

要接的三个入口:

第三步,路由与桥方法白名单,可以独立推进。

bash 复制代码
// 过渡:先给 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 组件侧的配置:

bash 复制代码
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 用错,而是四个域之间的信任关系没定清楚。凭据落到脚本域、通用路由下放给前端、退出时只清了一处副本,发生在三个完全不同的需求里,根子是一样的。

  1. 凭据默认不下发到脚本域,判断方案看能不能收回;

  2. 能力按意图授权,不按路由授权;

  3. 状态只有一个源,失效要扇出到每一个副本,服务端作废是最后兜底。

5.2 那 14 条清单可以直接贴进评审单。下次再遇到「H5 支付回调怎么接」「切换账号后页面为什么串了」,至少不用从「token 放哪儿」重新讲一遍。


附录 A:API 名称与版本速查

技术文章容易在这里翻车,单独列一张表(版本信息已按 SDK .d.ts 核对):

不同 API 版本的方法名与签名存在差异,尤其 configCookieSync 的参数个数和 clearSessionCookieSync 的可用版本。落地前请以目标 SDK 的 .d.ts 为准,不要只依据网络文章。

附录 B:官方文档索引

相关推荐
booboosui3 小时前
Android 悬浮窗藏不住自己:一个标志位与 140 毫秒
android
传奇开心果3 小时前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter
.痞老板.6 小时前
mysql初阶--数据类型
android·数据库·mysql
StarsGreen6 小时前
Android 启动流程详解
android·linux·单片机·嵌入式硬件
booboosui1 天前
Android 上来电怎么判:7 个条件加 5 个中文词
android
石头慢慢说1 天前
不写 XPath,让 AI 在真机上跑测试:Appium MCP 全流程实战
android
90后的晨仔1 天前
解决 Android 构建报错:Unable to delete file .../file-map.txt
android·前端
mg6681 天前
安卓玩机工具----安卓手机变成桌面电脑!多窗口镜像+音频投射+游戏操控,无需 Root
android·智能手机·电脑
peakmain91 天前
异常处理 - Native 层的崩溃捕获机制及实现 (一)
android·性能优化