Android中使用Webview SSL 自签名CA证书安全校验方案

前言:     

        因为我们的项目是Android + H5,之前的做法是把H5所需要的资源和html下载到本地这样证书校验不会走系统浏览器层只需要项目中预埋根证书就可以了,但是如果用webview加载线上的域名自签名证书就会走系统级校验在onReceivedSslError中返回ssl证书不受信,从而导致出现白页的情况

本篇文章贵在直接给提供一个工具类按照步骤直接使用:

使用方法:

1、在自己app的gradle中添加okhttp依赖,项目中如有可以忽略 

    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
    implementation 'com.squareup.okio:okio:1.14.0'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.11.0'

2、把服务器端给你的CA根证书(.cer后缀的,貌似.pem的不行但是自己改一下后缀好像可以用)放到自己的assets目录下

3、在自己的webview中setWebViewClient的时候重写一下onReceivedSslError方法,在该方法中处理ssl握手失败的场景可以通过打印error.getPrimaryError()去SslError类中对比一下报错原因

mWebview.setWebViewClient(new WebViewClient() {
    ...
    @Override
			public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
//				super.onReceivedSslError(view, handler, error);
				//自签名证书校验失败
				new CheckoutServerCaUtil().checkoutServerCA(handler,view.getUrl(),view.getContext(),"chain.cer");
			}
}

4、使用提供的工具类直接校验,工具如下创建名为CheckoutServerCaUtil的类直接ctrl + v 粘贴调用checkoutServerCA方法,参数参考工具中的注释部分就可完成证书的校验和主机名校验:

package com.citicbank.cbframeworkcore.util;


import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.webkit.SslErrorHandler;


import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.x500.X500Principal;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
 * author : jsxin
 * e-mail : jsxin0816@163.com
 * time   : 2022/11/17
 * desc   : 分行自签名证书校验工具:
 *          功能:1、根证书校验 2、主机名校验
 *          用法:自己的webview.setWebViewClient(new WebViewClient() {
 *              new CheckoutServerCaUtil().checkoutServerCA(handler,view.getUrl(),view.getContext());
 *          }
 */
public class CheckoutServerCaUtil {
    private final String TAG = "CheckoutServerCaUtil";
    public CheckoutServerCaUtil() {
    }

    /**
     * 根证书校验:onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
     * @param handler :   onReceivedSslError回调:SslErrorHandler
     * @param url :       onReceivedSslError回调:WebView.getUrl
     * @param context:    onReceivedSslError回调:WebView.getContext
     * @param caName:    放在assets目录下的根证书名称:例如:"chain.cer"
     */
    public void checkoutServerCA(final SslErrorHandler handler, String url, Context context,String caName) {
        OkHttpClient.Builder builder;
        try {
            InputStream inputStream = context.getAssets().open(caName);
            Log.d(TAG,"jsxin--->>>执行:--->>>00--->>>url:" + url);
            builder = setCertificates(new OkHttpClient.Builder(), inputStream);

        } catch (IOException e) {
            Log.d(TAG,"jsxin--->>>异常:--->>>01");
            builder = new OkHttpClient.Builder();
        }
        Request request = new Request.Builder().url(url)
                .build();
        builder.build().newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException error) {
                Log.d(TAG,"jsxin--->>>自签名证书校验结果:---error:----" + error.toString());
                handler.cancel();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG,"jsxin--->>>自签名证书校验结果:---onResponse:----" + response.code());
                handler.proceed();
            }
        });
    }

    private OkHttpClient.Builder setCertificates(OkHttpClient.Builder client, InputStream... certificates) {

        try {
            Log.d(TAG,"jsxin--->>>执行:--->>>01");

            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

            keyStore.load(null);

            int index = 0;

            for (InputStream certificate : certificates) {

                String certificateAlias = Integer.toString(index++);

                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

                try {

                    if (certificate != null)

                        certificate.close();

                } catch (IOException e) {
                    Log.d(TAG,"jsxin--->>>异常:--->>>02");
                    e.printStackTrace();
                }

            }
            Log.d(TAG,"jsxin--->>>执行:--->>>02");
            SSLContext sslContext = SSLContext.getInstance("TLS");

            TrustManagerFactory trustManagerFactory =

                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(keyStore);

            sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                Log.d(TAG,"jsxin--->>>异常:--->>>03");
                throw new IllegalStateException("Unexpected default trust managers:"
                        + Arrays.toString(trustManagers));
            }
            X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
            Log.d(TAG,"jsxin--->>>执行:--->>>03");

            client.sslSocketFactory(sslSocketFactory, trustManager);
            hostNameVerifier(client);
        } catch (Exception e) {
            Log.d(TAG,"jsxin--->>>异常:--->>>04");
            e.printStackTrace();

        }

        return client;

    }

    private void hostNameVerifier(OkHttpClient.Builder client) {
        Log.d(TAG,"jsxin--->>>执行:--->>>04");
        client.hostnameVerifier(new HostnameVerifier() {
            @SuppressLint("BadHostnameVerifier")
            @Override
            public boolean verify(String hostname, SSLSession session) {
                String peerHost = session.getPeerHost();//服务器返回的域名
                Log.d(TAG,"jsxin--->>>执行:--->>>05--->>>服务器返回域名 peerHost:" + peerHost + "--->>>主机名:" + hostname);
                try {
                    X509Certificate[] peerCertificates = (X509Certificate[]) session.getPeerCertificates();
                    for (X509Certificate c : peerCertificates) {
                        X500Principal subjectX500Principal = c.getSubjectX500Principal();
//                        String name = new X500Principal(subjectX500Principal).getName();
                        String name = subjectX500Principal.getName();
                        Log.d(TAG,"jsxin--->>>执行:--->>>06--->>>subjectX500Principal.getName():" + name);
                        String[] split = name.split(",");
                        for (String s : split) {
                            if (s.startsWith("CN")) {
                                if (s.contains(hostname) && s.contains(peerHost)) {
                                    Log.d(TAG,"jsxin--->>>执行:--->>>07");
                                    return true;
                                }
                            }
                        }
                    }
                } catch (SSLPeerUnverifiedException e) {
                    Log.d(TAG,"jsxin--->>>异常:--->>>05");
                    e.printStackTrace();
                }
                Log.d(TAG,"jsxin--->>>主机名校验失败");
                return false;
            }
        });
    }
}

如果想系统的学习一下可以参考这个链接,网上也基本上都是抄的这个但是里面有的一些方法过时了,所以可以直接梭哈我的工具:Android Webview SSL 自签名安全校验解决方案 - 熠然 - 博客园

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值