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 发起的请求,经过 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 证书绑定是必备措施