一、准备工作

1、客户端、服务端HTTPS证书

正常情况下,如果服务端应用需要在互联网上发布HTTPS服务,需要提供可信赖的证书,证书可到专门的证书服务商购买,也可以到专业的开放网站申请免费的证书,例如阿里云、腾讯云、FreeSSL网站等。如果仅仅是个人测试用,或者服务端仅仅是通过HTTPS私有服务,可以利用JDK自带的工具生成证书库、从证书库中导出证书、将证书导入另一证书库。

参考链接:https://blog.csdn.net/dwyane__wade/article/details/80350548

JDK证书工具在JDK的bin目录下,找到keytools程序,相关操作如下:

(1)创建服务端证书库

参考如下命令行:

keytool -genkey -alias tomcat -keypass 123456 -keyalg RSA -keysize 1024 -validity 365 -keystore D:/keys/tomcat.keystore -storepass 123456

参数说明如下:

-alias tomcat(别名,证书的唯一标识,用于区别于其他证书)
-keypass 123456(别名密码)
-keyalg RSA(算法)
-keysize 1024(密钥长度)
-validity 365(有效期,天单位)
-keystore D:/keys/tomcat.keystore(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)

执行后,出现命令提示,依次输入主机名等信息完成证书创建,过程如下:

您的名字与姓氏是什么?  [Unknown]:  localhost   (为发布服务的主机域名或IP)
您的组织单位名称是什么?  
[Unknown]:  myCompay
您的组织名称是什么?  
[Unknown]:  myOrg
您所在的城市或区域名称是什么?  
[Unknown]:  myCity
您所在的省/市/自治区名称是什么?  
[Unknown]:  myProvince
该单位的双字母国家/地区代码是什么?  
[Unknown]:  cn
CN=localhost, OU=myCompany, O=myOrg, L=myCity, ST=myProvince, C=zh是否正确?  [否]:  y (然后回车)

(2)从证书库导出证书

参考如下命令行:

keytool -export -alias tomcat -keystore D:/keys/tomcat.keystore -keypass 123456 -file D:/tomcat.cer

注:上述命令将导出别名tomcat的证书记录到D:/tomcat.cer这个文件,另外如果keystore类型是PKCS12,需通过参数指定keystore类型,参考命令如下:

keytool -export -alias client -keystore D:/keys/client.p12 -storetype PKCS12 -keypass 123456 -file D:/keys/client.cer

(3)将一个证书导入到另一个证书库

参考如下命令行:

keytool -import -v -file D:/keys/client.cer -keystore D:/keys/tomcat2.keystore -storepass 123456

(4)查看库中已有证书

参考如下命令行:

keytool -list -v -keystore D:/keys/tomcat.keystore

(5)删除库中已有证书

参考如下命令行:

keytool   -delete     -alias      "tomcat"      -keystore           D:/keys/tomcat2.keystore       -storepass   123456

(6)创建客户端证书库

浏览器通常使用PCKS12格式的证书(否则参考(1)),参考如下命令行:

keytool -genkey -alias client -keypass 123456 -keyalg RSA -keysize 1024 -validity 365 -storetype PKCS12 -keystore D:/keys/client.p12 -storepass 123456


2、HTTPS握手认证过程

(1)客户端证书及分发

适用于服务端对客户端也进行认证的场景,利用上述1中(6)创建证书库,再利用(2)将证书导出,利用(3)将证书导入服务端证书库;

最后通过(4)可查看服务端中保存的证书,其中应包含服务端自己的证书(PrivateKeyEntry)、信任的客户端证书(TrustedKeyEntry);

(2)服务端证书及分发

利用上述1中(1)创建证书库,再利用(2)将证书导出,利用(3)将证书导入客户端证书库,客户端是浏览器时需要使用浏览器的证书导入功能;

最后通过(4)可查看客户端中保存的证书,其中应包含客户端自己的证书(PrivateKeyEntry)、信任的服务端证书(TrustedKeyEntry);

(3)HTTPS握手及认证过程

大致过程如下:

  •  浏览器将自己支持的一套加密规则发送给网站。

  • 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。

  • 浏览器获得网站证书之后浏览器要做以下工作:
    •验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示
    •如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
    •使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。

  • 网站接收浏览器发来的数据之后要做以下的操作:
    •使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
    •使用密码加密一段握手消息,发送给浏览器。

  • 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

二、服务端HTTPS

1、Tomcat的HTTPS Connector配置

对于Tomcat8.0及低于8.0的版本,server.xml中的配置示例:


    <Connector connectionTimeout="20000" port="80" protocol="HTTP/1.1" redirectPort="443"/>
    <Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"  
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"  clientAuth="false" sslProtocol="TLS" keystoreFile="/conf/test.keystore" keystorePass="123456"/>

对于Tomcat9.0,server.xml中的配置示例为:

               maxThreads="150" SSLEnabled="true"
      scheme="https" secure="true">
       
                            certificateKeystorePassword="tomcat" clientAuth="false"

                         type="RSA" />    
       
   

注:上述参数中clientAuth="false"表示提供HTTPS的Tomcat服务不对访问的客户端进行证书认证;如为true,则Tomcat只允许Tomcat证书库中匹配的客户端访问;


2、Web服务器强制使用HTTPS

在 tomcat/conf/web.xml 中的 后面加上如下内容:

   
       
    CLIENT-CERT   
    Client Cert Users-only Area   
   
   
       
       
        SSL   
        /*   
   
   
       
        CONFIDENTIAL   
   
   


三、支持HTTPS的链接池

请参考如下几个步骤:

1、创建SSLContext对象生成Registry

有多种方式

(1)方式一:使用系统默认的方式,不作证书检查,支持连接公开网站如(https://www.baidu.com,https://www.sina.com.cn),例如:

public static Registry createRegistry4Sys() {
    SSLContext sslContext = SSLContexts.createSystemDefault();
    SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
        @Override
        public boolean verify(String s, SSLSession sslSession) {
            return true;
        }
    });
    Registry registry = RegistryBuilder.create()
            .register("http", PlainConnectionSocketFactory.getSocketFactory())
            .register("https", sslFactory)
            .build();
    return registry;
}

注:上述SSLContexts.createSystemDefault()用的是系统默认支持的方式,另外也可以用SSLContexts.createDefault(),区别在于前者用了JVM虚拟机传入的配置参数;

(2)方式二:关联到客户端环境的证书库,支持连接公开证书网站及自有证书库中的网址,例如:

public static Registry createRegistry4SysOrCer(String trustStoreType, String trustStorePath,
        String password) throws GeneralSecurityException, IOException {
    try (InputStream stream = new FileInputStream(trustStorePath)) {
        KeyStore trustStore = KeyStore.getInstance(trustStoreType);
        trustStore.load(stream, password.toCharArray());
        SSLContext sslContext = SSLContexts.custom()
                                           .loadTrustMaterial(null, new TrustSelfSignedStrategy())
                                           .loadKeyMaterial(trustStore, password.toCharArray())
                                           .build();
        SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        Registry registry = RegistryBuilder.create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslFactory)
                .build();
        return registry;
    }
}

注:上述方式实际是对服务端不作检查(即代码中的loadTrustMaterial(null, new TrustSelfSignedStrategy())),另外服务端需要对客户端认证时,能够提供客户端的证书(即代码中的loadKeyMaterial(trustStore, password.toCharArray()))

(3)方式三:信任自签发策略的方式,只信任证书库中的网址,例如:

public static Registry createRegistry4Cer(String trustStoreType, String trustStorePath,
        String password) throws GeneralSecurityException, IOException {
    try (InputStream stream = new FileInputStream(trustStorePath)) {
        KeyStore trustStore = KeyStore.getInstance(trustStoreType);
        trustStore.load(stream, password.toCharArray());
        SSLContext sslContext = SSLContexts.custom()
                                           .loadTrustMaterial(trustStore, new TrustSelfSignedStrategy())
                                           .build();
        SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext);
        Registry registry = RegistryBuilder.create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslFactory)
                .build();
        return registry;
    }
}
2、创建连接池
/**
     * @param registry       Connection socket factory
     * @param maxTotal       Max total connections in the pool
     * @param maxPerRoute    Max connections per route
     * @param tcpNoDelay     If true, data will be sent immediately without using socket buffer
     * @param soReuseAddress If true, when socket closed by current process, its port can be reused by other process
     *                       even not released
     * @param socketTimeout  Timeout for waiting data received
     * @param soLinger       If true, when closing socket, all data will be sent or wait this timeout (seconds)
     * @param soKeepAlive    If true, client will send idle packet to check server alive
     * @return Connection pool
     */
    private static PoolingHttpClientConnectionManager createPool(Registry registry, int maxTotal, int maxPerRoute,
            boolean tcpNoDelay, boolean soReuseAddress, int socketTimeout, int soLinger, boolean soKeepAlive) {
        PoolingHttpClientConnectionManager manager = registry == null ? new PoolingHttpClientConnectionManager()
                : new PoolingHttpClientConnectionManager(registry);
        manager.setMaxTotal(maxTotal);
        manager.setDefaultMaxPerRoute(maxPerRoute);
        // For some route using specified amount to override default, use: manager.setMaxPerRoute(route,max);
        SocketConfig config = SocketConfig.custom()
                                          .setTcpNoDelay(tcpNoDelay)
                                          .setSoReuseAddress(soReuseAddress)
                                          .setSoTimeout(socketTimeout)
                                          .setSoLinger(soLinger)
                                          .setSoKeepAlive(soKeepAlive)
                                          .build();
        manager.setDefaultSocketConfig(config);
        // For some host using specified configure, use: manager.setConnectionConfig(host,connectionConfig);
        return manager;
    }

4、从连接池中获取HttpClient


    private static RequestConfig createRequestConfig() {
        return RequestConfig.custom()
                            .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT)
                            .setConnectTimeout(CONNECTION_TIMEOUT)
                            .setSocketTimeout(SOCKET_TIMEOUT).build();
    }
    
    public static HttpClient createHttpClient(PoolingHttpClientConnectionManager manager){
        return HttpClients.custom()
                          .setConnectionManager(manager)
                          .setDefaultRequestConfig(createRequestConfig())
                          .build();
    }

后续通过HttpClient就可以进行一系列发送HTTP/HTTPS消息的操作了。