24、指纹认证与WebView:WebView中的指纹认证、JavaScript Bridge交互、安全风险防范

说实话,WebView 里的指纹认证,是我在混合开发中踩坑最多的一个点。

很多同学觉得,App 里能调指纹,WebView 里不也一样吗?嗯,还真不一样。WebView 是个沙盒,它没法直接调用系统底层的 BiometricPrompt。你得通过 JavaScript Bridge 搭一座桥,让 H5 页面和原生代码“对话”。

今天我们就聊聊这座桥怎么搭,以及搭桥的时候有哪些坑——尤其是安全方面的坑。

WebView 指纹认证的架构

先看整体流程。我习惯把 WebView 指纹认证分成三层:

  • H5 层:前端页面,负责 UI 展示和用户交互。它调用 JS 接口,等待回调。
  • Bridge 层:JavaScript Bridge,负责消息的序列化与传递。说白了,就是 H5 和 Native 之间的翻译官。
  • Native 层:Android 原生代码,调用 BiometricPrompt,执行真正的指纹认证。

这三层的关系,我画了一张图,你看一眼就明白了:

H5 页面 调用 JS 接口 等待回调结果 JavaScript Bridge 消息序列化 / 反序列化 权限校验 / 来源校验 Native 代码 BiometricPrompt 调用 指纹认证执行 JS 调用 Native 调用 回调结果 JS 回调 ⚠️ 安全风险高发区 中间人攻击 | JS 注入 | 回调伪造 | 来源校验缺失 图:WebView 指纹认证三层架构与安全风险

你看,H5 发起的请求,经过 Bridge 转发到 Native,Native 认证完再把结果传回去。这个链路看起来简单,但每个环节都可能出问题。

JavaScript Bridge 的实现

实现 Bridge 的方式有好几种。我个人最常用的是 @JavascriptInterface 注解的方式。为什么?因为它简单、稳定,而且 Android 官方一直支持。

先看 Native 端的代码:

public class FingerprintBridge {
    private Context context;
    private WebView webView;

    public FingerprintBridge(Context context, WebView webView) {
        this.context = context;
        this.webView = webView;
    }

    @JavascriptInterface
    public void startFingerprintAuth(final String requestId, final String payload) {
        // 注意:@JavascriptInterface 方法运行在子线程
        new Handler(Looper.getMainLooper()).post(() -> {
            BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(context)
                    .setTitle("指纹认证")
                    .setSubtitle("请验证您的指纹")
                    .setNegativeButton("取消", context.getMainExecutor(),
                            (dialog, which) -> {
                                callJsCallback(requestId, "cancel", "用户取消");
                            })
                    .build();

            biometricPrompt.authenticate(
                    new BiometricPrompt.AuthenticationCallback() {
                        @Override
                        public void onAuthenticationSucceeded(
                                BiometricPrompt.AuthenticationResult result) {
                            callJsCallback(requestId, "success", "认证成功");
                        }

                        @Override
                        public void onAuthenticationError(int errorCode, CharSequence errString) {
                            callJsCallback(requestId, "error", errString.toString());
                        }

                        @Override
                        public void onAuthenticationFailed() {
                            callJsCallback(requestId, "fail", "指纹不匹配");
                        }
                    });
        });
    }

    private void callJsCallback(String requestId, String status, String message) {
        String js = String.format(
                "javascript:onFingerprintResult('%s', '%s', '%s')",
                requestId, status, message);
        webView.post(() -> webView.evaluateJavascript(js, null));
    }
}

然后是在 WebView 中注册这个 Bridge:

webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new FingerprintBridge(this, webView), "AndroidFingerprint");

H5 端调用就很简单了:

// H5 端 JS 代码
function requestFingerprintAuth() {
    const requestId = generateUUID();
    const payload = JSON.stringify({ action: "login", timestamp: Date.now() });

    // 调用 Native 的指纹认证
    AndroidFingerprint.startFingerprintAuth(requestId, payload);
}

// Native 认证完成后会回调这个方法
function onFingerprintResult(requestId, status, message) {
    console.log("认证结果:", requestId, status, message);
    if (status === "success") {
        // 执行登录成功逻辑
    } else {
        // 处理失败逻辑
    }
}
💡 我的小技巧:

我习惯在 requestId 里带上时间戳和随机数,这样能防止回调被重放。另外,payload 里不要放敏感信息,比如密码或 token。指纹认证只负责「验人」,不负责「传数据」。

安全风险防范

说到安全,我得认真讲讲。WebView 的指纹认证,最大的风险其实不在指纹本身,而在 Bridge 的通信过程。

我遇到过最典型的一个案例:某 App 的 H5 页面通过 Bridge 调了指纹认证,认证成功后 Native 直接返回了用户的登录 token。结果呢?攻击者通过 XSS 注入,在 H5 页面里插了一段恶意 JS,直接拦截了回调,拿到了 token。

你看,问题出在哪?出在「认证结果」和「敏感数据」没有分离。

风险一:JS 注入攻击

攻击者可以通过 WebView 的漏洞,往页面里注入恶意 JS。一旦注入成功,你的 Bridge 回调就完全暴露了。

防范措施:

  • 永远不要用 addJavascriptInterface 暴露敏感方法。只暴露认证相关的方法。
  • 对 H5 页面来源做校验。只允许白名单域名调用 Bridge。
  • 使用 @JavascriptInterface 时,确保 WebView 的 setAllowFileAccess 为 false。

风险二:回调伪造

H5 页面可以伪造回调吗?理论上不行,因为回调是 Native 发起的。但如果攻击者控制了 H5 页面,他可以伪造一个假的 onFingerprintResult 函数,让 Native 的回调被覆盖。

防范措施:

  • 回调函数名不要固定。我习惯在每次认证时动态生成回调函数名。
  • 在回调参数中加入签名。Native 返回的结果用 HMAC 签名,H5 端验签。

风险三:中间人攻击

如果 WebView 加载的是 HTTP 页面,或者 HTTPS 证书校验不严格,攻击者可以拦截 Bridge 的通信内容。

防范措施:

  • 强制使用 HTTPS,并且做证书绑定(Certificate Pinning)。
  • Bridge 传递的数据做加密。我一般用 AES-GCM 加密 payload,密钥通过其他安全通道协商。
⚠️ 重要警告:

千万不要在 Bridge 中传递用户的生物特征数据(比如指纹模板)。指纹数据是敏感生物信息,一旦泄露无法重置。Bridge 只传递「认证请求」和「认证结果」,不传递生物特征本身。

最佳实践总结

说了这么多,我总结几条我一直在用的原则:

原则 说明
最小暴露原则 Bridge 只暴露认证相关方法,不暴露任何数据读写接口
来源校验 对 H5 页面 URL 做白名单校验,非白名单拒绝调用
结果签名 认证结果用 HMAC 签名,防止回调被篡改
数据分离 认证结果和敏感数据分开传递,认证成功后 H5 再通过其他接口获取数据
日志脱敏 Bridge 的日志不要打印 requestId 和 payload 的完整内容

我曾经在一个项目中,因为没做来源校验,导致第三方 H5 页面也能调用我们的指纹认证。虽然没出大问题,但那次之后,我把所有 Bridge 方法都加上了域名白名单校验。

嗯,WebView 指纹认证其实不难,难的是把安全细节做到位。你想想看,用户把手指按在传感器上,是对你的 App 的信任。我们不能辜负这份信任。

核心要点回顾:

  • WebView 指纹认证需要 JavaScript Bridge 桥接 H5 和 Native
  • 使用 @JavascriptInterface 注解实现 Bridge,注意线程切换
  • 安全风险集中在 JS 注入、回调伪造、中间人攻击三个方面
  • 认证结果和敏感数据必须分离传递
  • 来源校验、结果签名、HTTPS 证书绑定是必备措施

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