android 网络通信(二):使用https

什么是https?

超文本传输安全协议(英语:Hypertext Transfer Protocol Secure缩写HTTPS,常称为HTTP over TLSHTTP over SSLHTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。
使用https 进行网络通信可以保护通信通道的安全,保护用户的信息安全,减少应用通信过程中出现流量劫持的风险

CA & 数字证书

在 https 的通信过程中经常会涉及到 CA 和 证书这两个名词,CA 的全称是Certificate Authority (电子商务认证机构),CA为每个使用公开密钥的用户发放一个数字证书,数字证书的作用是证明证书中列出的用户合法拥有证书中列出的公开密钥。

在 android 应用开发中使用https

在上篇文章中介绍了使用 HttpUrlConnectiion 通过 http 协议与后台交互,在这里也可以直接使用 HttpUrlConnetion 访问 https 开头的地址

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

但是如果直接使用上述的方法去访问所有的 https 开头的链接地址,则可能会看到访问时出现这个异常:

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

原因是这种方法是直接由 Android 系统校验服务端证书的合法性,只有用可信的 CA 签发的数字证书的服务端才可以正常访问,如果访问的服务端所使用的数字证书是私有 CA 签发的,或者是运行的机器的版本较低没有 CA。在网上看到一种对应该异常的解决方法如下:

private static void trustAllHosts() {
        String TAG = "trustAllHosts";
        TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }

            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }
        }};

        try {
            SSLContext e = SSLContext.getInstance("TLS");
            e.init((KeyManager[])null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(e.getSocketFactory());
        } catch (Exception var3) {
            var3.printStackTrace();
        }

    }

使用上面这种方法后再使用 HttpUrlConnetiion 访问私有签发的证书的服务端时确实没有访问异常了,从代码中可以看出该方法是替换掉 HttpUrlConnetion 默认的证书校验,使用一个没有校验服务端证书的 TrustManager ,这样就跟直接使用 http 协议进行通信一样了,所以该方案不可取,需要在新的 TrustManager 中增加证书校验的方法:

TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }

            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                if (chain == null)
                    throw new IllegalArgumentException("Server X509Certificates is null");
                if (chain.length < 0)
                    throw new IllegalArgumentException("Server X509Certificates is empty");
                //迭代获取证书链中的证书与存放在 app 中的证书做对比
                for (X509Certificate x509Certificate : chain) {
                    x509Certificate.checkValidity();
                    try {
                        x509Certificate.verify(localCert.getPublicKey());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }
        }};

也可以直接用存放在本地的证书来生成一个 TrustManager :

CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = new BufferedInputStream(new FileInputStream("local.crt"));
Certificate ca;
try {
    ca = cf.generateCertificate(caInput);
    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
    caInput.close();
}

// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);

在设置了正确的 TrustManager 之后访问时还有一个常见的问题:

java.io.IOException: Hostname 'xxx.com' was not verified

主机名校验不通过,可以通过设置 HostNameVerifier 解决:

HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify("xx.com", session);
    }
};
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容