10、WebView证书绑定:WebView安全配置、WebView证书校验、WebView证书绑定实现

聊到Android网络攻防,WebView绝对是个绕不开的坎儿。我见过太多应用因为WebView配置不当,被中间人攻击搞得体无完肤。说白了,WebView就是个浏览器内核的壳,你如果不对它做证书校验,那跟裸奔没啥区别。

今天咱们就深入聊聊WebView的证书绑定。我个人习惯把这块分成三个层次:安全配置、证书校验、证书绑定实现。一层比一层严格,一层比一层安全。

WebView安全配置:基础防线

先说说最基础的安全配置。很多开发者觉得WebView默认就是安全的,其实不然。我遇到过不少项目,WebView直接允许了混合内容,结果HTTPS页面里加载了HTTP的图片,整个加密通道就被污染了。

核心配置要点:

  • 禁止混合内容:setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW)
  • 启用JavaScript时注意XSS风险
  • 关闭文件访问:getSettings().setAllowFileAccess(false)
  • 禁用地理定位等敏感API
// 基础安全配置示例
WebView webView = findViewById(R.id.webview);
WebSettings settings = webView.getSettings();

// 禁止混合内容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    webView.getSettings().setMixedContentMode(
        WebSettings.MIXED_CONTENT_NEVER_ALLOW
    );
}

// 关闭文件访问
settings.setAllowFileAccess(false);
settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowUniversalAccessFromFileURLs(false);

// 谨慎启用JavaScript
settings.setJavaScriptEnabled(true);
// 但关闭JavaScript接口的自动绑定
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");

我的经验:曾经有个项目,测试环境一切正常,上线后用户反馈页面加载异常。查了半天,发现是测试服务器用了自签名证书,而生产环境是正规CA证书。WebView默认对自签名证书会报错,但很多人直接重写了onReceivedSslError并调用了proceed()——这是大忌!

WebView证书校验:拦截中间人

基础配置只是第一步。真正的安全防线在于证书校验。WebView默认会校验服务器证书的合法性,但有个问题:它信任系统根证书。如果攻击者在用户手机上安装了恶意CA证书,那WebView就会信任伪造的服务器。

为什么会这样?因为Android的证书信任链是基于系统根证书的。一旦根证书被污染,整个信任体系就崩塌了。所以我们需要自己实现证书校验逻辑。

// 自定义WebViewClient进行证书校验
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        // 不要直接调用handler.proceed()
        // 应该根据错误类型做判断
        switch (error.getPrimaryError()) {
            case SslError.SSL_UNTRUSTED:
                // 证书不受信任
                handler.cancel();
                break;
            case SslError.SSL_EXPIRED:
                // 证书过期
                handler.cancel();
                break;
            case SslError.SSL_IDMISMATCH:
                // 域名不匹配
                handler.cancel();
                break;
            case SslError.SSL_NOTYETVALID:
                // 证书尚未生效
                handler.cancel();
                break;
            default:
                handler.cancel();
                break;
        }
    }
});

注意:千万不要在onReceivedSslError中直接调用handler.proceed()。我曾经审计过一个金融类应用,他们为了测试方便,直接忽略了所有SSL错误。结果上线后,攻击者用伪造证书就能劫持所有WebView流量。这个漏洞被评为高危。

WebView证书绑定实现:终极方案

证书绑定(Certificate Pinning)才是真正的终极方案。它的原理很简单:在客户端硬编码服务器证书的公钥或证书指纹,只信任这些特定的证书。就算系统根证书被污染,攻击者也无法用其他证书冒充你的服务器。

我建议使用公钥指纹进行绑定,而不是整个证书。因为证书会过期轮换,但公钥可以保持不变。这样你更新证书时,客户端代码不需要跟着改。

// 证书绑定实现
public class PinningWebViewClient extends WebViewClient {
    
    // 预置的合法证书公钥指纹(SHA-256)
    private static final Set<String> VALID_PINS = new HashSet<>(Arrays.asList(
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    ));
    
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        // 获取服务器证书链
        X509Certificate[] certChain = getCertificateChain(error);
        
        if (certChain != null && verifyPins(certChain)) {
            // 证书指纹匹配,放行
            handler.proceed();
        } else {
            // 证书不匹配,拦截
            handler.cancel();
            // 可以在这里记录日志或上报异常
            Log.w("PinningWebView", "Certificate pinning failed!");
        }
    }
    
    private boolean verifyPins(X509Certificate[] chain) {
        try {
            for (X509Certificate cert : chain) {
                // 计算公钥的SHA-256指纹
                byte[] publicKey = cert.getPublicKey().getEncoded();
                MessageDigest md = MessageDigest.getInstance("SHA-256");
                byte[] digest = md.digest(publicKey);
                String pin = "sha256/" + Base64.encodeToString(digest, Base64.NO_WRAP);
                
                // 检查是否在预置列表中
                if (VALID_PINS.contains(pin)) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    
    private X509Certificate[] getCertificateChain(SslError error) {
        // 从SslError中提取证书链
        // 具体实现依赖于Android版本,这里简化处理
        return null;
    }
}

关键点总结:

  • 绑定公钥指纹而非整个证书,便于证书轮换
  • 至少绑定两个备用公钥,一个当前用,一个备用
  • 不要只绑定叶子证书,也要考虑中间CA证书
  • 定期更新绑定的指纹列表

知识体系结构图

WebView证书绑定知识体系 WebView安全通信 第一层:安全配置 第二层:证书校验 第三层:证书绑定 安全配置要点 • 禁止混合内容 • 关闭文件访问 • 谨慎启用JavaScript • 移除危险接口 • 禁用地理定位 • 限制WebViewClient 证书校验要点 • 重写onReceivedSslError • 区分错误类型 • 不信任自签名证书 • 检查域名匹配 • 验证证书有效期 • 记录异常日志 证书绑定要点 • 绑定公钥指纹 • 至少两个备用公钥 • 支持证书轮换 • 校验完整证书链 • 定期更新指纹 • 失败时拒绝连接

避坑指南:我曾经在一个项目中,只绑定了叶子证书的公钥。结果证书过期后,新证书的公钥变了,所有用户都无法访问。后来我改成绑定两个公钥——一个当前用,一个备用。这样证书轮换时,客户端不需要更新。记住,永远给自己留条后路。

嗯,WebView证书绑定这块,说白了就是让应用只信任你指定的证书。三层防护层层递进:安全配置打基础,证书校验做拦截,证书绑定上保险。我个人建议,如果应用涉及敏感数据传输,直接上证书绑定,别犹豫。

最后提醒一句:证书绑定虽然安全,但也要考虑运维成本。证书轮换、紧急回滚、多环境支持,这些都要提前设计好。别等到线上出问题了才手忙脚乱。

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