Android Https的安全使用

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wangsf1112/article/details/73650750
一.Https介绍
HTTP协议是没有加密的明文传输协议,如果APP采用HTTP传输数据,则会泄露传输内容,可能被中间人劫持,修改传输的内容。
HTTPS是HTTP over SSL,HTTP是应用层协议,TCP是传输层协议,在应用层和传输层之间,增加了一个安全套接层SSL。安全套接字层 (SSL)现在技术上称为传输层安全协议(TLS)。SSL/TLS层负责客户端和服务器之间的加解密算法协商、密钥交换、通信连接的建立。

在典型的 HTTPS 使用场景中,会使用一个包含公钥及与其匹配的私钥的证书配置服务器。作为 SSL 客户端与服务器握手的一部分,服务器将通过使用公钥加密签署其证书来证明自己具有私钥。

不过,任何人都可以生成他们自己的证书和私钥,因此,一个简单的握手只能说明服务器知道与证书公钥匹配的私钥,除此之外什么都证明不了。解决此问题的一个方法是让客户端拥有其信任的一个或多个证书集。如果证书不在此集合中,则不会信任服务器。

但这个简单的方法有几个缺点。服务器应能够随时间的推移升级到更强的密钥(“密钥旋转”),使用新的公钥替换证书中的公钥。遗憾的是,客户端应用现在必须根据服务器配置发生的变化进行更新。如果服务器不在应用开发者的控制下(例如,如果服务器是一个第三方网络服务),则很容易出现问题。如果应用必须与网络浏览器或电子邮件应用等任意服务器通信,那么,此方法也会带来问题。

为弥补这些缺点,通常使用来自知名颁发者证书颁发机构(CA)发放的证书配置服务器。主机平台一般包含其信任的知名 CA 的列表。从 Android 4.2 开始,Android 目前包含在每个版本中更新的 100 多个 CA。CA 具有一个证书和一个私钥,这点与服务器相似。为服务器发放证书时,CA 使用其私钥签署服务器证书。然后,客户端可以验证该服务器是否具有平台已知的 CA 发放的证书。

不过,在解决一些问题的同时,使用 CA 也会引发其他问题。因为 CA 为许多服务器发放证书,因此,您仍需要某种方式来确保您与您需要的服务器通信。为解决这个问题,CA 发放的证书通过类似 gmail.com 等具体名称或 *.google.com 等通配型主机集识别服务器。

以下示例会让这些概念更具体。下面的代码段来自命令行,openssl 工具的 s_client 命令将查看维基百科( Wikipedia) 的服务器证书信息。它指定端口 443,因为此端口是 HTTPS的默认端口。此命令将 openssl s_client 的输出发送到 openssl x509,后者将根据 X.509 标准 格式化与证书有关的信息。具体而言,此命令获取subject和issuer信息,分别包含服务器名称信息 和 可认证 CA 的颁发结构。如下:

$ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer
subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org
issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA

你会看到证书是由 RapidSSL CA 为与 *.wikipedia.org 匹配的服务器发放的。

 HTTPS通信所用到的证书由CA提供,需要在服务器中进行相应的设置才能生效。另外在我们的客户端设备中,只要访问的HTTPS的网站所用的证书是知名CA根证书签发的,如果这些CA又在浏览器或者操作系统的根信任列表中,就可以直接访问。而如12306.cn网站,它的证书是非可信CA提供的,是自己签发的,所以在用谷歌浏览器打开时,会提示“您的连接不是私密连接”,证书是非可信CA颁发的:

 所以在12306.cn的网站首页会提示“为保障您顺畅购票,请下载安装根证书”,操作系统安装后,就不会再有上图的提示了。
 
二. Https使用

1. 访问知名CA发放证书的Https服务器

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
假设要访问一个由知名 CA 发放证书的网络服务器,那么,可以使用上述简单代码发起安全的请求,因为Android平台已经包含了该CA。
而使用私有CA签发的证书的服务器使用上述方式就无法访问。

2.访问使用未知CA或私有CA签发证书的服务器
如果要使用私有CA或未知CA签发的证书,因为Android系统还不信任该CA的证书,会出现异常javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found。 

对服务器证书进行校验必须重写校验证书链TrustManager中的方法checkServerTrusted()
    private void requestFromServer(final Context context, String https_url) throws NoSuchAlgorithmException, KeyManagementException, IOException {
        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        //校验服务器证书
                        if (chain == null) {
                            throw new IllegalArgumentException("Check Server X509Certificates is null");
                        }

                        if (chain.length < 0) {
                            throw new IllegalArgumentException("Check Server X509Certificates is empty");
                        }

                        for (X509Certificate cert : chain) {
                            cert.checkValidity();

                            try {
                                String certName = "abc.crt"; //一般将下载的证书放到项目中的assets目录下
                                InputStream certInput = new BufferedInputStream(context.getAssets().open(certName));
                                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                                X509Certificate serverCert = (X509Certificate) certificateFactory.generateCertificate(certInput);

                                cert.verify(serverCert.getPublicKey());
                            } catch (IOException e) {
                                e.printStackTrace();
                            } catch (NoSuchAlgorithmException e) {
                                e.printStackTrace();
                            } catch (InvalidKeyException e) {
                                e.printStackTrace();
                            } catch (NoSuchProviderException e) {
                                e.printStackTrace();
                            } catch (SignatureException e) {
                                e.printStackTrace();
                            }
                        }
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        };


        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAllCerts, null);

        HostnameVerifier hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                //校验服务器证书的域名是否相符(若不校验服务器证书域名则直接return true)
                HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
                Boolean result = hv.verify("*.xxx.com", session);
                return result;
            }
        };

        URL url = new URL(https_url);
        HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
        httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
        httpsURLConnection.setHostnameVerifier(hostnameVerifier);
//        httpsURLConnection.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //信任所有主机(注意:此处SSLSocketFactory与sslContext.getSocketFactory()返回的是不同的类)

        InputStream in = httpsURLConnection.getInputStream();

        //网络返回的数据处理...

    }

另一种实现方式
通过保存在assets路径下的证书文件获取特定的证书,用该 CA 创建 KeyStore,然后用后者创建和初始化 TrustManagerTrustManager 是系统用于从服务器验证证书的工具,可以使用一个或多个 CA 从KeyStore 创建,而创建的 TrustManager 将仅信任这些 CA。如果是新的 TrustManager ,此示例将初始化一个新的 SSLContext,后者可以提供一个 SSLSocketFactory,您可以通过 HttpsURLConnection 用它来替换默认的 SSLSocketFactory。这样一来,网络请求时将使用您的 CA 验证证书,系统能够验证您的服务器证书是否来自值得信任的颁发者。
    /**
     * 获取校验服务器端证书的SocketFactory
     *
     * @param context
     * @param certName 保存在assets路径下的服务器端证书文件名,如abc.crt
     * @return
     */
    public static javax.net.ssl.SSLSocketFactory getSocketFactory(Context context, String certName) throws IOException, CertificateException,
            KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        InputStream certInput = new BufferedInputStream(context.getAssets().open(certName));

        //以X.509格式获取证书
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Certificate cert = certificateFactory.generateCertificate(certInput);

        //生成一个包含服务器端证书的KeyStore
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        keyStore.setCertificateEntry("cert", cert);

        //用包含服务器端证书的KeyStore生成一个TrustManager
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm);
        trustManagerFactory.init(keyStore);

        //生成一个使用我们TrustManager的SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

        return sslContext.getSocketFactory();
    }

    public static void test(final Context context) {
        new Thread() {
            @Override
            public void run() {
                super.run();
                String https_url = "https://www.xxx.com";
                try {
                    URL url = new URL(https_url);
                    HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
                    httpsURLConnection.setSSLSocketFactory(getSocketFactory(context, "abc.cer"));

                    InputStream in = httpsURLConnection.getInputStream();

                    ...
                    
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (CertificateException e) {
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (KeyStoreException e) {
                    e.printStackTrace();
                } catch (KeyManagementException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

3.使用OKHttp访问https服务器
private void requestFromServer(final Context context, String https_url) throws NoSuchAlgorithmException, KeyManagementException, IOException {
        X509TrustManager[] trustAllCerts = new X509TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        //不校验客户端证书
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//不校验服务器证书
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, null);

HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
//不校验服务器证书的域名
return true;
}
};

OkHttpClient okHttpClient = new OkHttpClient.Builder().hostnameVerifier(hostnameVerifier)
.sslSocketFactory(sslContext.getSocketFactory(), trustAllCerts[0]).build();

Request request = new Request.Builder().url(https_url).build();

Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {

}
});

}
上述方法中未校验服务器证书和域名,可以参考前面的方法进行具体校验。

4.WebView访问https服务器
webview加载H5页面,在webView.setWebViewClient(webviewClient)时重载WebViewClient的onReceivedSslError(),如果出现证书错误会回调该方法,在其中直接调用handler.proceed()会忽略错误继续加载证书有问题的页面,如果调用handler.cancel()可以终止加载证书有问题的页面,证书出现问题了,可以提示用户风险,让用户选择加载与否。
    public class MyWebViewClient extends WebViewClient {

        ...

        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            handler.proceed();
        }
    }

参考
展开阅读全文

没有更多推荐了,返回首页