25、人脸识别与WebView:WebView中调用原生人脸识别、JavaScript Bridge实现、安全风险防范

说实话,WebView 这玩意儿在移动开发里真是又爱又恨。爱的是它让 H5 页面能跑在 App 里,恨的是它跟原生代码之间总隔着一层纱。尤其是人脸识别这种敏感操作,你总不能把活体检测的算法丢到 JS 里跑吧?那安全性也太差了。

我早年做过一个金融类 App,客户要求人脸识别流程全部在 WebView 里完成。当时产品经理一拍脑袋说「让前端直接调摄像头不就行了?」——我赶紧拦住了。你想想看,JS 直接操作摄像头,且不说权限问题,光是视频流被中间人截获的风险就够喝一壶的。

所以今天咱们就聊聊,怎么在 WebView 里优雅地调用原生人脸识别,以及这中间有哪些坑。

为什么非要用原生人脸识别?

说白了,WebView 里的 JS 跑在沙箱里,它拿不到系统级的硬件能力。人脸识别需要的东西——摄像头驱动、活体检测算法、安全芯片的密钥存储——这些 JS 一个都碰不到。

我见过一些团队尝试用纯 H5 方案,比如调用 getUserMedia 拍张照片,然后传给后端做比对。结果呢?照片被篡改、视频被替换、甚至有人拿一张打印的照片就骗过去了。嗯,活体检测这关就过不了。

所以正确的做法是:WebView 只负责展示 UI 和业务逻辑,真正的识别工作交给原生代码。这就是我们今天要讲的 JavaScript Bridge 的核心价值。

核心原则: 敏感操作(人脸采集、活体检测、加密比对)必须走原生层,WebView 只做结果展示和交互反馈。

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 类里维护一个 Handler 引用,所有需要操作 UI 的方法都 post 到主线程执行。这样既安全又清晰。

安全风险防范——这块才是重点

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;
    }
    // 启动识别
    ...
}
注意: 永远不要相信 JS 传来的数据。攻击者可以通过调试工具修改 JS 代码,伪造任何参数。所以服务端也要做二次校验。

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 人脸识别调用流程 H5 页面(JS) JavaScript Bridge 原生层(Android) 1. 用户点击「开始识别」 4. 收到回调结果 5. 更新 UI / 提交业务 2. 调用 Bridge 接口 6. 执行 JS 回调 3. 启动人脸识别 Activity 3.1 活体检测 + 特征提取 3.2 加密上传服务端比对 安全校验点 参数校验 / 域名校验 nonce 防重放 / HTTPS

避坑指南:我踩过的几个雷

最后分享几个实战中容易翻车的地方,都是我用真金白银换来的教训。

  • WebView 内存泄漏:Activity 销毁时一定要调用 webView.destroy(),否则 WebView 会持有 Activity 引用导致泄漏。我建议在 onDestroy 里加上 webView.removeAllViews() 和 webView.destroy()。
  • JS 回调时机:人脸识别是异步操作,用户可能识别到一半就按返回键了。这时候如果还执行 JS 回调,页面可能已经销毁。记得在回调前检查 WebView 是否还存活。
  • 混淆规则:如果你用了 ProGuard 或 R8,记得保留 @JavascriptInterface 方法不被混淆。否则上线后 Bridge 调用会静默失败,排查起来特别痛苦。
我曾经 遇到过一个问题:某个机型上人脸识别总是闪退,查了半天发现是 WebView 的 JS 线程和原生线程死锁了。解决方案是:所有 Bridge 方法内部不要直接操作 WebView,用 Handler post 到主线程执行。

好了,关于 WebView 调原生人脸识别,核心就是这些。记住一句话:Bridge 是通道,不是保险箱。通道搭得再漂亮,数据不安全也是白搭。下次咱们聊聊更深入的话题——人脸识别与多线程并发处理。


公众号:蓝海资料掘金营,微信deep3321