Android WebView实现HTTPS证书校验

WebViewClient onReceivedSslError

Android中使用WebView加载html,在html中使用ajax请求Https服务,如何进行证书的安全校验呢?本文主要讲解WebView实现Https自制证书的校验。

我们使用Android的WebView的时候会设置一个WebViewClient,而如果请求Https发生错误的时候,就会调用WebViewClientonReceivedSslError方法,如下所示:

WebView webView = new WebView(getContext());
webView.setWebViewClient(new WebViewClient(){
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        super.onReceivedSslError(view, handler, error);

    }
});

super.onReceivedSslError(view, handler, error);默认继承父类的实现,父类的实现是handler.cancel();取消通信。所以我们要做的就是在onReceivedSslError方法中进行证书校验。

onReceivedSslError方法参数分析

  • WebView view参数

即当前的webview对象

  • SslErrorHandler handler参数

点进源码可以看到,就一个构造方法和两个类方法。proceed()方法是允许所有网络访问,cancel()方法是取消所有网络访问。

public class SslErrorHandler extends Handler {

    /**
     * @hide Only for use by WebViewProvider implementations.
     */
    @SystemApi
    public SslErrorHandler() {}

    /**
     * Proceed with the SSL certificate.
     */
    public void proceed() {}

    /**
     * Cancel this request and all pending requests for the WebView that had
     * the error.
     */
    public void cancel() {}
}
  • SslError error参数

点进SslError源码可以看到这个类的属性和方法,主要属性有:

SSL_NOTYETVALID:证书不合法
SSL_EXPIRED:证书超出有效期
SSL_IDMISMATCH:域名不匹配
SSL_UNTRUSTED:不受信的证书
SSL_DATE_INVALID:证书日期无效
SSL_INVALID:一般性错误

除了几个构造方法外,主要方法有:

getUrl():获取当前请求的url
getPrimaryError():获取错误类型
getCertificate():获取当前证书

证书的sha256值校验

判断逻辑是:

1、获取当前webview的证书的sha256值
2、获取客户端证书的sha256值
3、对比两个证书的sha256值,如果相等,则调用handler.proceed()方法,如果不相等,则调用弹出提示框,并退出应用程序。

WebView webView = new WebView(getContext());
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        String certSha256 = SSLSocketCert.getSSLCertSHA256FromCert(view.getContext().getAssets().open("client.crt"));
        String serverSha256 = SSLSocketCert.getSSLCertFromServer(error.getCertificate());
        if (certSha256.equalsIgnoreCase(serverSha256)) {
            handler.proceed();
        } else {
            DialogUtil.showSingleDialog(view.getContext(), "警告", "证书校验失败", false, "退出", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    System.exit(0);
                }
            });
        }
    }
});

SSLSocketCert.java

import android.net.http.SslCertificate;
import android.os.Bundle;

import org.json.JSONArray;
import org.json.JSONException;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

public class SSLSocketCert {

    //获取这个SSLSocketFactory
    public static SSLSocketFactory getSSLSocketFactory(InputStream cerIn) {
        try {
            SSLSocketFactory factory = setCertificates(cerIn);
            return factory;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 载入证书
     */
    private static SSLSocketFactory setCertificates(InputStream... certificates) {
        try {
            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));
                if (certificate != null) {
                    certificate.close();
                }
            }
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //获取HostnameVerifier
    public static HostnameVerifier getHostnameVerifier(final JSONArray trustHosts) {
        HostnameVerifier hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(String host, SSLSession sslSession) {
                if (trustHosts == null || trustHosts.length() == 0) {
                    return false;
                }
                try {
                    for (int i = 0; i < trustHosts.length(); i++) {
                        String trustHost = trustHosts.getString(i);
                        if (host.equalsIgnoreCase(trustHost)) {
                            return true;
                        }
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                return false;
            }
        };
        return hostnameVerifier;
    }

    /**
     * SSL证书错误,手动校验https证书
     *
     * @param cert https证书
     * @return true通过,false失败
     */
    public static String getSSLCertFromServer(SslCertificate cert) {
        Bundle bundle = SslCertificate.saveState(cert);
        if (bundle != null) {
            byte[] bytes = bundle.getByteArray("x509-certificate");
            if (bytes != null) {
                try {
                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
                    Certificate ca = cf.generateCertificate(new ByteArrayInputStream(bytes));
                    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
                    byte[] key = sha256.digest(ca.getEncoded());
                    return bytesToHex(key);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    public static String getSSLCertSHA256FromCert(InputStream cerIn) {
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            Certificate ca = cf.generateCertificate(cerIn);
            MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
            byte[] key = sha256.digest(ca.getEncoded());
            String sha256Str = bytesToHex(key);
            return sha256Str;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 十六进制转字节数组
     *
     * @param hex
     * @return
     */
    public static byte[] hexToBinary(String hex) {
        String string = "0123456789ABCDEF";
        char[] ch = hex.toCharArray();
        int len = ch.length / 2;
        byte byts[] = new byte[len];
        for (int i = 0; i < len; i++) {
            byts[i] = (byte) ((((byte) string.indexOf(Character
                    .toUpperCase(ch[i * 2])) & 0xFF) << 4)
                    | ((byte) string.indexOf(Character.toUpperCase(ch[i * 2 + 1])) & 0xFF));
        }
        return byts;
    }

    /**
     * 字节数组转十六进制字符串
     *
     * @param bytes
     * @return
     */
    public static String bytesToHex(byte[] bytes) {
        final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
                '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        char[] hexChars = new char[bytes.length * 2];
        int v;
        for (int j = 0; j < bytes.length; j++) {
            v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }
}

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/25/implement-https-certificate-verification-in-android-webview/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android WebView实现HTTPS证书校验
WebViewClient onReceivedSslError Android中使用WebView加载html,在html中使用ajax请求Https服务,如何进行证书的安全校验呢?本文主要讲解WebView实现Https……
<<上一篇
下一篇>>
文章目录
关闭
目 录