OkHttp配置HTTPS访问+服务器部署

941 阅读8分钟

1 概述

OkHttp配置HTTPS访问,核心为以下三个部分:

  • sslSocketFactory
  • HostnameVerifier
  • X509TrustManager

第一个是套接字工厂,第二个用来验证主机名,第三个是证书信任器管理类。通过OkHttp实现HTTPS访问需要自己实现以上三部分,另外还简单提及了服务器端的部署,用的是Tomcat9,最后是一些常见问题的可能解决方案。

2 OkHttp介绍

OkHttp是一款开源的处理网络请求的轻量级框架,有Square公司贡献,用于替代HttpUrlConnectionApache HttpClient,优点有:

  • 共享SocketHTTP/2支持所有连接到同一个主机的请求共享Socket
  • 连接池可以减少请求延迟
  • 缓存响应数据减少重复的网络请求
  • 自动处理gzip压缩

3 准备工作

  • 一台服务器
  • 一个域名
  • 一个证书

4 OkHttp部分

4.1 暴力方案

public static String test() {
	OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(createSSLSocketFactory(), new TrustAllCerts())
        .hostnameVerifier(new TrustAllHostnameVerifier()).build();

	String url = "https://xxxxxxx";   //修改成自己的url
    Request request = new Request.Builder().url(url).build();
    Call call = build.newCall(request);
    Response response = call.execute();
    if(response.body() != null)
    {
        String result = response.body().string();
        //处理result
    }
}

private static class TrustAllCerts implements X509TrustManager {
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
    public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}
}

private static class TrustAllHostnameVerifier implements HostnameVerifier {
    public boolean verify(String hostname, SSLSession session) { return true; }
}

private static SSLSocketFactory createSSLSocketFactory() {
    SSLSocketFactory ssfFactory = null;
    try {
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null, new TrustManager[]{new TrustAllCerts()}, new SecureRandom());
        ssfFactory = sc.getSocketFactory();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ssfFactory;
}

这是一种暴力的方案,看类名就知道了,信任所有的证书与主机:

public boolean verify(String hostname, SSLSession session) { return true; }

这个方法直接返回true,也就是信任所有的主机。

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

这里两个check函数没有做任何的工作,表示接受任意的客户端与服务端的证书。这样写的话相当于是使用了一个没用的TrustManager,这样还不如不加密,不推荐使用。

4.2 推荐方案

从两方面入手修改,一是从X509TrustManager入手,二是从HostnameVerifier入手。

4.2.1 HostnameVerifier

先说个简单的,这里主要是验证主机名,简单的话,可以如下实现:

HostnameVerifier hnv = new HostnameVerifier() {
	@Override
	public boolean verify(String hostname, SSLSession session) {
	    if("www.test.com".equals(hostname)){  
			return true;  
	    } 
	    else {  
			HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
			return hv.verify(hostname, session);
		}
	}
};

这里验证主机名是www.test.com就返回true(也可以使用服务器IP进行验证),实现得比较简单,业务复杂的话可以结合配置中心,黑/白名单等动态校验。

4.2.2 X509TrustManager

接着是X509TrustManager的处理,这里其实有两种方式,一种是以流的方式添加信任证书(来源):

private static X509TrustManager trustManagerForCertificates(InputStream in)
        throws GeneralSecurityException
{
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
    if (certificates.isEmpty()) {
        throw new IllegalArgumentException("expected non-empty set of trusted certificates");
    }

    char[] password = "password".toCharArray(); // 这里可以使用任意密码
    KeyStore keyStore = newEmptyKeyStore(password);
    int index = 0;
    for (Certificate certificate : certificates) {
        String certificateAlias = Integer.toString(index++);
        keyStore.setCertificateEntry(certificateAlias, certificate);
    }

    // Use it to build an X509 trust manager.
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, password);
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(keyStore);
    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
    if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager))
    {
        throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
    }
    return (X509TrustManager) trustManagers[0];
}

返回一个信任由输入流读取的证书的信任管理器,若证书没有被签名则抛出SSLHandsakeException,证书建议使用第三方签名的而不是自签名的(比如使用OpenSSL生成),特别是在生产环境中,例子的注释也提到:

在这里插入图片描述

完整代码见文末。这里把工具类的方法实现成了静态,调用时可以直接:

OKHTTP.send("https://xxxxx");

另一种方式是直接自定义一个TrustManager,重写里面的三个方法:

SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{
    new X509TrustManager() {
        @Override
        public void checkClientTrusted(X509Certificate[] chain,String authType) throws CertificateException {}
        
        @Override
        public void checkServerTrusted(X509Certificate[] chain,String authType) throws CertificateException {
            for (X509Certificate cert : chain) {
                // Make sure that it hasn't expired.
                cert.checkValidity();
                // Verify the certificate's public key chain.
                try {
                    cert.verify(((X509Certificate) ca).getPublicKey());
                } 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];
        }
    }
}, null);

第一个方法为

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

该方法检查客户端的证书,由于不需要对客户端进行认证,默认即可。

第二个方法为

@Override
public void checkServerTrusted(X509Certificate[] chain,String authType)

该方法检查服务器的证书,若不信任该证书则抛出异常,通过自己实现该方法可以信任任何自己指定的证书,不做任何处理的话,不会抛出任何异常,相当于信任所有证书。这里检查了证书是否过期以及证书的签名是否匹配。

第三个方法为

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

返回受信任的X509证书数组。

这种方法笔者没有试过,仅供参考。

5 服务器部署

服务器用的是Tomcat,简单介绍一下部署。

5.1 上传工程

后端处理用的Spring Boot的工程,就不演示了,使用打包后上传到webapps下即可。

5.2 Tomcat配置

重点说一下Tomcat的配置,首先需要一个域名,修改conf/server.xml文件,找到默认的名叫localhostHost

在这里插入图片描述

然后直接复制Host标签,把name修改成自己的域名即可。

在这里插入图片描述

然后是证书的配置,笔者的证书在某某云上购买的,这里提供了几种格式的证书下载:

在这里插入图片描述

Tomcat的是两个文件,一个是pfx文件,一个是密码文件,把pfx文件上传到服务器的Tomcat后,继续修改server.xml,搜索8443找到如下位置(Tomcat 9.0.33):

在这里插入图片描述

一些Tomcat8的高版本提供了HTTP/2的实现,默认使用apr实现的,这里使用的是HTTP/1.1,使用HTTP/2需要额外安装AprApr-util以及Tomcat-native,因此这里采用HTTP/1.1实现。

修改如下:

在这里插入图片描述

添加了schemesecurekeystoreFilekeystoreTypekeystorePassclientAuthsslProtocol配置,同时去掉里面的<SSLHostConfig>keystoreFile是刚才的pfx文件,采用绝对路径,keystorePass是密码。

另外默认的端口为8443,这里修改成了8123

如果想要更安全的话可以手动指定TLS的版本:

<Connector ...
sslProtocol="TLS" sslEnabledProtocols="TLSv1.3"
>

重启Tomcat后输入

https://www.test.com:port

进行测试

在这里插入图片描述

这样就成功了。

6 验证与源码

这个因为没有完整的Demo很难做验证,具体来说前端用的OkHttp核心都介绍了,后端的话服务器Tomcat也介绍了,用Spring Boot做个Demo应该不难。

这里只给出了工具类OKHTTP的源码:

package xxx.xxx;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

import javax.net.ssl.*;
import java.io.*;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.TimeUnit;

public class OKHTTP {
    private static OkHttpClient client;
    private static X509TrustManager trustManager;
    static
    {
        try
        {
            //这里是服务器的证书文件,笔者查看了其他的教程,使用的是getAssets().open(),那是AS的工程
            //这里是Maven工程,证书文件放在了src/main/resources下
            //可以以pem或crt结尾,具体可以向购买证书的服务商查询.
            trustManager = trustManagerForCertificates(new FileInputStream("src/main/resources/server.crt"));
            client = new OkHttpClient.Builder()
                .sslSocketFactory(createSSLSocketFactory(), trustManager)
                .hostnameVerifier((hostname, sslSession) -> {
                    //验证主机名
                    if("www.test.com".equals(hostname))
                    {
                        return true;
                    }
                    else
                    {
                        HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
                            return verifier.verify(hostname,sslSession);
                    }
                }).build();
        }
        catch (GeneralSecurityException | FileNotFoundException e)
        {
            e.printStackTrace();
        }
    }

    public static String send(String url)
    {
        Request request = new Request.Builder().url(url).build();
        //如果想要加上get/post请求的话再.build前添加即可   
        try (Response response = client.newCall(request).execute())
        {
            ResponseBody body = response.body();
            return body == null ? null : body.string();
        }
        catch (IOException e)
        {
            e.printStackTrace();
            return null;
        }
    }
    
    //以下代码为别人的轮子
    private static X509TrustManager trustManagerForCertificates(InputStream in)
            throws GeneralSecurityException
    {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
        if (certificates.isEmpty()) {
            throw new IllegalArgumentException("expected non-empty set of trusted certificates");
        }

        char[] password = "password".toCharArray(); // 这里可以使用任意密码
        KeyStore keyStore = newEmptyKeyStore(password);
        int index = 0;
        for (Certificate certificate : certificates) {
            String certificateAlias = Integer.toString(index++);
            keyStore.setCertificateEntry(certificateAlias, certificate);
        }

        // Use it to build an X509 trust manager.
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
                KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, password);
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager))
        {
            throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
        }
        return (X509TrustManager) trustManagers[0];
    }

    private static KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); // 这里添加自定义的密码,默认
            InputStream in = null; // By convention, 'null' creates an empty key store.
            keyStore.load(in, password);
            return keyStore;
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }

    private static SSLSocketFactory createSSLSocketFactory() {
        SSLSocketFactory ssfFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, new TrustManager[]{trustManager}, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ssfFactory;
    }
}

7 常见问题

7.1 Tomcat HTTPS无法访问

  • 证书文件错误,不过这个可能性比较少
  • 配置错误,请检查配置文件是否正确,可以ps -ef | grep tomcat查看Tomcat是否开启以及查看logs/catalina.out日志
  • 端口错误,访问的端口需要与<Connector>中的端口对应
  • 安全组/防火墙问题,云服务器的话需要在安全组配置中开启相应端口,同时应查看有没有把某个IP列入黑名单导致无法访问。防火墙的话这里主要指iptables,如果没有开启的话不需要理会,如果开启的话需要开放对应端口

7.2 OkHttp HTTPS无法访问

  • 无法读取证书文件:需要把证书文件放在工程对应路径下读取,比如Android Studio中放在assets下然后使用getAssets().open("xxx.xxx")获取,Maven工程的话放在resources下直接使用FileInputStream获取
  • singed fields invalid

在这里插入图片描述

证书文件格式错误,使用.crt/.pem等证书

  • Signature does not match:这个有可能是使用OpenSSL自生成证书在验证的时候出现的异常,可能的解决办法是转换证书的格式,如果不行就重新生成一次证书

8 参考