JSBridge 是什么?WebView 页面如何与原生或小程序通信,怎样做安全校验?
说明 JSBridge 的两种调用方向和常见实现(注入对象、消息处理器、URL 拦截),小程序 web-view 的通信限制,以及来源校验、能力白名单和参数校验。
参考答案
简答
- JSBridge 是 WebView 中网页 JS 与宿主(App 原生代码或小程序)之间的通信层:网页调用宿主能力,宿主把结果或事件回传给网页。
- 网页调宿主常见三种方式:宿主注入的 JS 对象(Android
addJavascriptInterface)、平台提供的消息接口(AndroidaddWebMessageListener、iOSwindow.webkit.messageHandlers,iOS 14+ 可用带回复的处理器)、拦截自定义协议 URL;宿主调网页则在 WebView 中执行一段 JS(AndroidevaluateJavascript、iOSevaluateJavaScript)。 - 两边只能传可序列化的数据,异步调用要用回调 ID 或 Promise 把请求和响应对应起来。
- 微信小程序的 web-view 不能直接调用小程序 API,只能用 JSSDK 提供的
wx.miniProgram.navigateTo、postMessage等接口,postMessage的消息也只在后退、组件销毁、分享等特定时机送达。 - 安全上要把网页当作不可信输入:宿主只在可信页面开放 bridge,能拿到来源时(Android
sourceOrigin、iOSframeInfo.securityOrigin)逐条检查,只开放白名单能力、逐个校验参数,敏感操作再经过用户确认或服务端校验。
通信的两个方向
网页调用宿主:
- 注入对象:Android 用
webView.addJavascriptInterface(obj, "bridge")把 Java 对象挂到页面上,网页直接调用它的方法。官方文档说明:对象会注入到页面的所有 frame(包括 iframe);目标 API 17(Android 4.2)及以上时只有加了@JavascriptInterface注解的公共方法可调用;注入要到下一次页面加载后才生效;JS 调用发生在 WebView 的后台线程上。 - 带来源的消息接口:AndroidX 的
WebViewCompat.addWebMessageListener(webView, "bridge", allowedOriginRules, listener)只向 origin 匹配规则的 frame 注入对象,网页调用bridge.postMessage(data),原生回调onPostMessage会拿到sourceOrigin、isMainFrame,并通过replyProxy.postMessage回复。使用前要用WebViewFeature.isFeatureSupported(WEB_MESSAGE_LISTENER)检查。 - 消息处理器:iOS 用
userContentController.add(handler, name: "bridge")安装实现WKScriptMessageHandler的处理器,网页调用window.webkit.messageHandlers.bridge.postMessage(data),WebKit 回调userContentController(_:didReceive:)。name在同一个WKUserContentController内必须唯一且不能为空,函数定义在页面的默认 content world(WKContentWorld.page)。收到的WKScriptMessage.body只允许NSNumber、NSString、NSDate、NSArray、NSDictionary、NSNull;frameInfo是发消息的 frame,可读取isMainFrame和securityOrigin(协议、主机名、端口;iOS 9+)。 - 带回复的消息处理器(iOS 14+):用
addScriptMessageHandler(_:contentWorld:name:)安装WKScriptMessageHandlerWithReply,函数会定义在指定 content world 的所有 frame 中。原生在userContentController(_:didReceive:replyHandler:)里调用replyHandler(reply, nil)回复,出错时reply传nil并给出错误字符串;reply同样限于上述几种类型。不需要回复时用WKScriptMessageHandler即可。 - 预先注入脚本:iOS 的
WKUserScript(source:injectionTime:forMainFrameOnly:)可以在页面里先挂好 bridge 封装。atDocumentStart在文档元素创建后、其他内容加载前注入,atDocumentEnd在文档加载完成后、其他子资源加载前注入;forMainFrameOnly为true只注入主 frame,为false注入所有 frame。官方要求在创建 WebView 之前把脚本加到配置的WKUserContentController中。 - URL 拦截:网页跳转一个自定义协议地址(如
myapp://share?...),宿主在导航回调中拦截并解析。这种方式受 URL 长度限制,现在多作为兼容方案。
宿主调用网页:宿主在 WebView 中执行 JS,例如调用网页事先挂好的 window.onNativeEvent(json)。Android 用 evaluateJavascript(script, callback),它在当前页面上下文中异步执行,必须在 UI 线程调用,回调也在 UI 线程。iOS 用 webView.evaluateJavaScript(script, completionHandler:)(另有 async 版本),执行结束后无论成功或失败都会调用回调,回调总在主线程运行,参数是执行结果或错误。拼接脚本时要把数据先编码成 JSON,不要直接把字符串拼进代码。
一个最小的请求-响应封装
JavaScript
// 网页侧:给每次调用分配 ID,宿主处理完后按 ID 回调
const pending = new Map();
let seq = 0;
export function callNative(method, params) {
return new Promise((resolve, reject) => {
const id = ++seq;
pending.set(id, { resolve, reject });
window.webkit.messageHandlers.bridge.postMessage({ id, method, params });
});
}
// 宿主执行 window.__bridgeReply({ id, error, result }) 回传结果
window.__bridgeReply = ({ id, error, result }) => {
const task = pending.get(id);
if (!task) return;
pending.delete(id);
error ? task.reject(new Error(error)) : task.resolve(result);
};
实际项目还需要超时清理 pending,并在宿主不存在时(普通浏览器里打开)给出降级。
小程序 web-view 的边界
- web-view 加载的网页要在小程序管理后台配置业务域名,个人类型小程序暂不支持。
- 网页引入 JSSDK 后,只能使用
wx.miniProgram.navigateTo、navigateBack、switchTab、reLaunch、redirectTo、postMessage、getEnv等接口,以及部分 JSSDK 能力(如图片、定位、扫一扫)。 wx.miniProgram.postMessage不会实时送达,组件的bindmessage只在小程序后退、组件销毁、分享、复制链接等时机触发,e.detail.data是多次消息组成的数组。需要实时交互时,常见做法是让网页跳转到小程序页面,或通过服务端中转。- 网页可以通过
window.__wxjs_environment === 'miniprogram'或wx.miniProgram.getEnv判断自己是否运行在小程序中。
安全校验
WebView 里可能加载第三方页面、广告 iframe,或者页面存在 XSS。只要 bridge 对所有页面开放,攻击者就能调用宿主能力。
- 校验来源:能拿到调用方来源时,检查发起页面或 frame 的 origin 是否在白名单内。
addJavascriptInterface注入到所有 frame,回调里拿不到调用方 origin,只能靠控制加载的页面(只加载白名单地址、拦截跳转)来限制。需要按来源区分时改用addWebMessageListener:用allowedOriginRules限定注入范围(官方建议只用 HTTPS 规则,通配符*会注入所有 frame,消息要按不可信数据处理),并在onPostMessage里再检查sourceOrigin和isMainFrame。iOS 在处理器里检查message.frameInfo.isMainFrame和message.frameInfo.securityOrigin的协议、主机名、端口;WKUserScript不需要进入 iframe 时设forMainFrameOnly: true。只比较 URL 前缀容易被https://trusted.com.evil.com之类的地址绕过,应解析后比较协议和主机名。 - 最小能力:按页面来源开放不同的方法集合,不提供"执行任意命令""读写任意文件"之类的通用方法。
- 校验参数:类型、长度、枚举值逐一检查;文件路径、跳转地址同样按白名单校验。
- 敏感操作二次确认:支付、获取手机号等操作由宿主弹出原生确认,关键结果以服务端校验为准,不信任网页传回的"已支付"状态。
- 限制导航:WebView 只允许跳转白名单域名,防止页面被导航到恶意站点后继续使用 bridge。
Electron 中的 preload 和 IPC 是同一类问题,见《Electron 应用的主进程、渲染进程和 IPC 有哪些安全边界?》。
常见追问
- 怎么判断 bridge 是否已就绪?宿主注入通常有时机差,可以由宿主在注入完成后派发一个自定义事件,网页监听该事件后再调用;微信 web-view 中可以监听
WeixinJSBridgeReady。 addJavascriptInterface在旧系统上有什么风险?官方文档指出,目标 API 16(Android 4.1)及以下的应用,或运行在 Android 4.2 以下设备上的应用,页面 JS 可以通过反射访问注入对象,以宿主权限执行 Java 代码;包含不可信内容的 WebView 中要格外谨慎。- 用
prompt、alert拦截做通信可以吗?早期有这种做法,但会和真实弹窗冲突,且同步阻塞页面,优先使用平台提供的消息接口。