zoukankan      html  css  js  c++  java
  • OkHttp配置HTTPS访问+服务器部署

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

  • 相关阅读:
    PAT甲级1091Acute Stroke
    PAT甲级1076Forwards on Weibo
    PAT甲级1131Subway Map
    PAT甲级1130Infix Expression
    PAT甲级1103Integer Factorization
    PAT甲级1034Head of a Gang
    Blender删除历史材质球未用材质球
    王者荣耀 花木兰 水晶猎龙者 同人3D壁纸 木兰小哥哥也有拧不开瓶盖的时候,嘿嘿嘿 家居服 减布料
    王者荣耀 嫦娥 同人 3D渲染 壁纸 家居服 减布料
    联考6
  • 原文地址:https://www.cnblogs.com/6b7b5fc3/p/12716291.html
Copyright © 2011-2022 走看看