開發基於連接池的Java客戶端及服務端HTTPS應用

一、準備工作

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中的配置示例:

<!--老版本tomcat的配置,在tomcat8下測試成功-->
    <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中的配置示例爲:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
               maxThreads="150" SSLEnabled="true"
      scheme="https" secure="true">
        <SSLHostConfig>
            <Certificate certificateKeystoreFile="conf/tomcat.keystore"
                certificateKeystorePassword="tomcat" clientAuth="false"

                         type="RSA" />    
        </SSLHostConfig>
    </Connector>

注:上述參數中clientAuth="false"表示提供HTTPS的Tomcat服務不對訪問的客戶端進行證書認證;如爲true,則Tomcat只允許Tomcat證書庫中匹配的客戶端訪問;


2、Web服務器強制使用HTTPS

在 tomcat/conf/web.xml 中的 </welcome-file-list> 後面加上如下內容:

<login-config>   
    <!-- Authorization setting for SSL -->   
    <auth-method>CLIENT-CERT</auth-method>   
    <realm-name>Client Cert Users-only Area</realm-name>   
</login-config>   
<security-constraint>   
    <!-- Authorization setting for SSL -->   
    <web-resource-collection >   
        <web-resource-name >SSL</web-resource-name>   
        <url-pattern>/*</url-pattern>   
    </web-resource-collection>   
    <user-data-constraint>   
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>   
    </user-data-constraint>   
</security-constraint>


三、支持HTTPS的鏈接池

請參考如下幾個步驟:

1、創建SSLContext對象生成Registry<ConnectionSocketFactory>

有多種方式

(1)方式一:使用系統默認的方式,不作證書檢查,支持連接公開網站如(https://www.baidu.com,https://www.sina.com.cn),例如:

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

注:上述SSLContexts.createSystemDefault()用的是系統默認支持的方式,另外也可以用SSLContexts.createDefault(),區別在於前者用了JVM虛擬機傳入的配置參數;

(2)方式二:關聯到客戶端環境的證書庫,支持連接公開證書網站及自有證書庫中的網址,例如:

public static Registry<ConnectionSocketFactory> 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<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslFactory)
                .build();
        return registry;
    }
}

注:上述方式實際是對服務端不作檢查(即代碼中的loadTrustMaterial(null, new TrustSelfSignedStrategy())),另外服務端需要對客戶端認證時,能夠提供客戶端的證書(即代碼中的loadKeyMaterial(trustStore, password.toCharArray()))

(3)方式三:信任自簽發策略的方式,只信任證書庫中的網址,例如:

public static Registry<ConnectionSocketFactory> 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<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>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<ConnectionSocketFactory> 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消息的操作了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章