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

Posted OkidoGreen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OkHttp配置HTTPS访问+服务器部署相关的知识,希望对你有一定的参考价值。

1 概述

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

  • sslSocketFactory
  • HostnameVerifier
  • X509TrustManager

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

OkHttp介绍

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

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

3 准备工作

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

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"
>
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

重启Tomcat后输入

https://www.test.com:port
  • 1
  • 1

进行测试

这样就成功了。

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 参考

以上是关于OkHttp配置HTTPS访问+服务器部署的主要内容,如果未能解决你的问题,请参考以下文章

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

Retrofit+OKHttp忽略https证书验证

Android使用OkHttp请求自签名的https网站

Https系列之四:https的SSL证书在Android端基于okhttp,Retrofit的使用

Https系列之四:https的SSL证书在Android端基于okhttp,Retrofit的使用

Android5.0系统访问https要怎么做