25、人脸识别与WebView:WebView中调用原生人脸识别、JavaScript Bridge实现、安全风险防范
说实话,WebView 这玩意儿在移动开发里真是又爱又恨。爱的是它让 H5 页面能跑在 App 里,恨的是它跟原生代码之间总隔着一层纱。尤其是人脸识别这种敏感操作,你总不能把活体检测的算法丢到 JS 里跑吧?那安全性也太差了。
我早年做过一个金融类 App,客户要求人脸识别流程全部在 WebView 里完成。当时产品经理一拍脑袋说「让前端直接调摄像头不就行了?」——我赶紧拦住了。你想想看,JS 直接操作摄像头,且不说权限问题,光是视频流被中间人截获的风险就够喝一壶的。
所以今天咱们就聊聊,怎么在 WebView 里优雅地调用原生人脸识别,以及这中间有哪些坑。
为什么非要用原生人脸识别?
说白了,WebView 里的 JS 跑在沙箱里,它拿不到系统级的硬件能力。人脸识别需要的东西——摄像头驱动、活体检测算法、安全芯片的密钥存储——这些 JS 一个都碰不到。
我见过一些团队尝试用纯 H5 方案,比如调用 getUserMedia 拍张照片,然后传给后端做比对。结果呢?照片被篡改、视频被替换、甚至有人拿一张打印的照片就骗过去了。嗯,活体检测这关就过不了。
所以正确的做法是:WebView 只负责展示 UI 和业务逻辑,真正的识别工作交给原生代码。这就是我们今天要讲的 JavaScript Bridge 的核心价值。
JavaScript Bridge 怎么搭?
Android 里 WebView 和 JS 通信,官方提供了两种方式:
- @JavascriptInterface 注解:把 Java 对象暴露给 JS 调用
- shouldOverrideUrlLoading:通过拦截 URL Scheme 实现通信
我个人习惯用 @JavascriptInterface,因为它更直接,代码可读性也好。不过要注意,这个注解在 API 17 以上才支持,如果你的 App 还要兼容老设备,那就得用 URL 拦截方案了。
先看一个最简单的例子。假设 H5 页面里有个按钮,点击后触发原生人脸识别:
// Java 端:定义 Bridge 接口
public class FaceBridge {
private Context context;
private WebView webView;
public FaceBridge(Context context, WebView webView) {
this.context = context;
this.webView = webView;
}
@JavascriptInterface
public void startFaceRecognition(String requestId) {
// 这里启动原生的人脸识别 Activity
Intent intent = new Intent(context, FaceRecognitionActivity.class);
intent.putExtra("request_id", requestId);
context.startActivity(intent);
}
// 识别完成后,通过 JS 回调通知 H5
public void onFaceResult(String requestId, boolean success, String message) {
webView.post(() -> {
String js = "javascript:onFaceResult('" + requestId + "', " + success + ", '" + message + "')";
webView.loadUrl(js);
});
}
}
// 在 WebView 设置中注册 Bridge
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new FaceBridge(this, webView), "FaceBridge");
JS 端调用就很简单了:
// H5 页面中调用
function startFace() {
const requestId = generateUUID();
// 调用原生方法
window.FaceBridge.startFaceRecognition(requestId);
}
// 原生回调处理
function onFaceResult(requestId, success, message) {
if (success) {
console.log('识别成功:', message);
// 更新 UI 或提交业务数据
} else {
console.error('识别失败:', message);
// 显示错误提示
}
}
你看,流程其实不复杂。但这里有个细节:@JavascriptInterface 方法是在 WebView 的内部线程中执行的,不是主线程。所以如果你要在里面启动 Activity,记得用 Handler 切到主线程,否则会崩。
安全风险防范——这块才是重点
Bridge 搭好了,功能跑通了,但安全漏洞往往就藏在这些看似顺畅的通信里。我曾经接手过一个项目,前任工程师把整个数据库操作都暴露给了 JS——你想想看,只要 H5 页面被 XSS 攻击,攻击者就能直接删库跑路。
人脸识别涉及生物特征,一旦泄露就是终身泄露。密码还能改,你的脸能换吗?所以下面这几个风险点,你一定要盯死。
1. 接口权限控制
不要把所有原生能力都暴露给 JS。只暴露必要的接口,而且每个接口都要做参数校验。
@JavascriptInterface
public void startFaceRecognition(String requestId) {
// 参数校验:requestId 必须符合 UUID 格式
if (requestId == null || !requestId.matches(
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")) {
Log.w("FaceBridge", "非法 requestId: " + requestId);
return;
}
// 只有特定域名下的页面才能调用
String url = webView.getUrl();
if (url == null || !url.startsWith("https://trusted-domain.com")) {
Log.w("FaceBridge", "非法来源: " + url);
return;
}
// 启动识别
...
}
2. 防止中间人攻击
WebView 加载的页面必须使用 HTTPS,并且要校验证书。我见过一些开发为了方便,在测试环境允许 HTTP,结果上线时忘了改回来——这就是个巨大的安全漏洞。
// 强制使用 HTTPS
webView.loadUrl("https://your-secure-domain.com/page");
// 证书校验(可选,但推荐)
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// 不要调用 handler.proceed(),应该拒绝
handler.cancel();
Log.e("WebView", "SSL 错误:" + error.toString());
}
});
3. 敏感数据不经过 JS
人脸特征数据、加密密钥、Token 这些敏感信息,绝对不要通过 Bridge 传给 JS。JS 环境是不安全的,任何在 JS 中传递的数据都可能被截获。
正确的做法是:原生层完成人脸识别后,直接通过安全通道(比如 HTTPS + 签名)把结果传给服务端。JS 只收到一个「成功/失败」的布尔值,连人脸图片的 Base64 都不要传。
| 数据类型 | 能否经过 Bridge | 说明 |
|---|---|---|
| 识别结果(成功/失败) | 可以 | 只传布尔值,不传具体特征 |
| 人脸图片/特征向量 | 绝对不行 | 原生层直接加密上传服务端 |
| 用户 Token | 不行 | 通过 Header 或 Cookie 传递 |
| 错误信息 | 可以 | 只传错误码,不传堆栈信息 |
4. 防止重放攻击
每次调用人脸识别时,都应该带上一个一次性 Token(nonce)。这个 Token 由服务端生成,原生层校验,用完即废。这样即使攻击者截获了 Bridge 的调用,也无法重复使用。
@JavascriptInterface
public void startFaceRecognition(String requestId, String nonce) {
// 校验 nonce 是否有效(与服务端比对)
if (!verifyNonce(nonce)) {
Log.w("FaceBridge", "nonce 无效或已过期");
return;
}
// 使用后立即失效
invalidateNonce(nonce);
// 启动识别
...
}
流程图:WebView 人脸识别完整调用链
下面这张图展示了从 H5 页面发起请求,到原生识别完成,再回调 H5 的完整流程。我建议你保存下来,做架构设计时对照着看。
避坑指南:我踩过的几个雷
最后分享几个实战中容易翻车的地方,都是我用真金白银换来的教训。
- WebView 内存泄漏:Activity 销毁时一定要调用 webView.destroy(),否则 WebView 会持有 Activity 引用导致泄漏。我建议在 onDestroy 里加上 webView.removeAllViews() 和 webView.destroy()。
- JS 回调时机:人脸识别是异步操作,用户可能识别到一半就按返回键了。这时候如果还执行 JS 回调,页面可能已经销毁。记得在回调前检查 WebView 是否还存活。
- 混淆规则:如果你用了 ProGuard 或 R8,记得保留 @JavascriptInterface 方法不被混淆。否则上线后 Bridge 调用会静默失败,排查起来特别痛苦。
好了,关于 WebView 调原生人脸识别,核心就是这些。记住一句话:Bridge 是通道,不是保险箱。通道搭得再漂亮,数据不安全也是白搭。下次咱们聊聊更深入的话题——人脸识别与多线程并发处理。