HTTPS 和 Java 的融合問題
HTTPS 協議是一套完善的標準,它能確保網絡連接的安全。要理解這套協議如何運作并非難事,而對應的 RFC 文檔 早在 2000 年就有了。
盡管 HTTPS 的應用已經如此廣泛,你仍然可以遇到一些軟件并不對這套協議進行處理,因為它們覺著沒必要。不幸的是我曾在使用一種語言實現 認證系統融合 的過程中碰到過這種問題,它本不應該讓我感到如此驚訝的。它就是Java 。
HTTPS 是如何工作的 ?
在對我遇到的問題進行描述之前,讓我先講講融合的認證系統是如何運作的。HTTPS 協議使用了 TLS/SSL 協議來確保連接的安全。TLS/SSL 協議對認證的握手環節進行了定義,這個環節能讓服務器以一種安全的方式連接到客戶端。在 握手 期間會執行如下幾個步驟:
-
客戶端發送消息開始進行連接。
-
服務端將證書發送給客戶端。
-
客戶端使用由受信任的機構簽發的證書來對服務端發送的證書進行驗證。
-
服務端發送請求,要求客戶端提供證書。
-
客戶端將它的證書發送給服務端。
-
服務端對客戶端發送過來的證書進行驗證。
-
服務端和客戶端交換主密鑰,它會在數據的加密過程中被用到。
-
連接建立。
我與團隊伙伴一道嘗試用 Java 實現 HTTPS 客戶端。結合我們關于 TLS/SSL 握手的知識以及使用 curl 進行手工測試的經驗,我們認為要實現這個客戶端只需要三個文件: 一個客戶端證書,一個客戶端私鑰 以及 一個能對服務端證書進行驗證的受信任的證書。
好吧,其實我們想錯了。
Java: 問題,解決方案,以及為什么如此麻煩
因為每天都使用融合認證這樣的做法并不通用,因此我們向這個世界上最好的資源尋求幫助。初看起來 Google 大叔提供的結果并沒有顯露這個實現背后的復雜性,而且每次查看的搜索結果都在將我們引向越來越令人心煩意亂的解決方案(有些可以追溯到上個世紀90年代)。更糟糕的是我們還的用到 Apache 的 HttpComponents 來實現連接,但是大多數建議使用的方案都是基于純 Java 原生庫的。
來自互聯網的知識讓我們確信了如下幾點:
-
Java不能直接對任何證書或者私鑰進行使用(像 curl 那樣)。
-
Java需要使用單獨的文件 ( Java Keystore 文件 ) ,它可以包含原來的證書和密鑰。
-
我們需要一個受信任的 keystore 文件 ,里面有服務端對每次 HTTS 連接進行證書驗證時需要的證書。
-
我們需要一個密鑰的 keysotre 文件,里面有客戶端的證書以及用于融合認證的客戶端私鑰。
首選,我們的創建受信任的 keystore 文件。我們使用 keytool 命令來創建帶有證書的 keystore 文件:
$ keytool -import -alias trusted_certificate -keystore trusted.jks -file trusted.crt
我們在 keystore 文件中將 trusted.jks 以及 證書 trusted.crt 存儲在 trusted_certificate 別名下。 在命令的執行過程中,會要求我們輸入這個keystore文件的密碼,稍后我們會使用這個密碼來訪問這個 keystore 文件。
要創建一個 keystore,還需要幾個額外的步驟。在大多數情況下,你可能會要從公司收到兩個從客戶端證書發出的文件。第一個文件將會是 pem 格式的客戶端證書。這個證書將會被發送給服務端。第二個文件是客戶端的私鑰(也是 pem 格式的),它被用來在握手過程成確認你是否是客戶端證書的擁有者。
不幸的是, Java 只支持 PKCS12 格式,因此我們就得將我們的證書和私鑰翻譯成 PKCS12 格式的。我們可以使用 OpenSSL來做這件事情。
$ openssl pkcs12 -export \
-in client.crt \
-inkey client.key \
-out key.p12 \
-name client
我們從 client.crt 和 client.key 文件生成了 key.p12 文件。這里需要再次輸入密碼。這個密碼是要被用來保護私鑰的。
從 PKCS12 格式的文件我們可以通過將 PKCS12 引入到新的 keystore 中,來生成另外一個 keystore 文件:
$ keytool -importkeystore \
-destkeystore key.jks \
-deststorepass <<keystore_password>> \
-destkeypass <<key_password_in_keystore>> \
-alias client \
-srckeystore key.p12 \
-srcstoretype PKCS12 \
-srcstorepass <<original_password_of_PKCS12_file>>
這個命令看起來更加復雜一點,不過解密過程是一樣簡單的。在命令的開頭我們對新的叫做的 key.jks 的 keystore 的參數進行了聲明。我們定義了 keystore 的密碼以及私鑰的密碼,它們都會被 keystore 用到。我們也將私鑰分配給了keystore中的一些別名 (在本例中就是 isclient)。接下來,我們對源文件 (key.p12) 進行了指定, 還有文件的格式以及原來的密碼。
有了 trusted.jks 和 key.jks ,我們就可以開始寫代碼了。第一步我們得描述一下如何去使用 keystore:
File trustedKeystoreFile = new File("trusted.jks");
File keystoreFile = new File("key.jks");
SSLContext sslcontext = SSLContexts.custom()
.loadTrustMaterial(trustedKeystoreFile,
"<<trusted_keystore_password>>".toCharArray())
.loadKeyMaterial(keystoreFile,
"<<keystore_password>>".toCharArray(),
"<<original_password_of_PKCS12_file>>".toCharArray())
.build();
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslcontext,
new String[]{"TLSv1.2"},
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
我們使用了 keystore 文件并且構建了一個 SSL 上下文。接下來,我們創建了 socket 文件,它能為我們的請求提供合適的 HTTPS 連接。
最后我們就可以用 Java 來調用我們的端點了:
try (CloseableHttpClient httpclient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.build()) {
HttpGet httpGet = new HttpGet("https://ourserver.com/our/endpoint");
try (CloseableHttpResponse response = httpclient.execute(httGet)) {
HttpEntity entity = response.getEntity();
System.out.println(response.getStatusLine());
EntityUtils.consume(entity);
}
}
OK。在創建兩個等于于我們原來的證書和私鑰的文件(keystore)之后,我們用 Java 實現了融合認證。也許用 Java 實現的 HTTPS 連接有一定的理由,但現在它只會令人頭疼。
來自:https://www.oschina.net/translate/mutual-problems