使用 Java 執行 mTLS 呼叫
1.概述
安全性是任何軟體應用程式最重要的方面之一,不容置疑。相互傳輸層安全性 (mTLS) 是人們用來保護應用程式安全的方法之一。在本文中,我們將學習如何在啟用 mTLS 的情況下向伺服器發出 HTTP 呼叫。
我們將首先介紹 mTLS,然後學習如何使用 mTLS 設定 Nginx 伺服器。最後,我們將使用HttpsURLConnection
和HttpClient
等 Java 用戶端來呼叫伺服器。
2.什麼是 mTLS?
相互 TLS (mTLS) 是一種安全協議,它擴展了標準 TLS,要求客戶端和伺服器同時提供並驗證 X.509 證書,從而實現雙向身份驗證。與僅由伺服器證明其身分的常規 TLS 不同,mTLS 確保用戶端也經過驗證,因此非常適合高安全性環境。
它通常用於保護服務之間的內部通訊、無需密碼即可驗證使用者或設備,以及在微服務、API 和企業系統中實施零信任安全模型。
3. 使用 mTLS 設定 Nginx
要使用 mTLS 設定 Nginx,我們將為伺服器和客戶端產生所需的憑證。憑證準備好後,我們將使用這些憑證來設定 Nginx 以啟用 mTLS。
3.1. 產生證書
對於 mTLS,我們需要為伺服器和客戶端產生憑證。為了簡單起見,我們將使用自簽名憑證。為此,我們需要自己的憑證授權單位 (CA)。
那麼,讓我們設定證書頒發機構:
openssl genrsa -des3 -out ca.key 4096
openssl rsa -in ca.key -out ca.key
openssl req -new -x509 -days 3650 -key ca.key -subj "/CN=*.server.hostname" -out ca.crt
上述指令產生一個自簽名 SSL 憑證。首先,建立一個 4096 位元 RSA 私鑰,即ca.key
,並使用 DES3 演算法的密碼進行加密。然後,從金鑰中移除密碼,輸出該金鑰的未加密版本。最後,使用未加密的私密金鑰產生一個自簽署 X.509 憑證ca.crt
,有效期限為 10 年,主體通用名稱設定為*.server.hostname.
現在,我們的憑證授權單位 (CA) 已經準備好了。我們可以使用此 CA 建立伺服器和用戶端憑證。首先,讓我們建立伺服器私鑰和憑證。
第一步是產生私鑰:
printf test > server_passphrase.txt
openssl genrsa -des3 -passout file:server_passphrase.txt -out server.key 1024
與先前的指令一樣,上述指令產生伺服器私鑰,即server.key.
現在,我們可以為伺服器產生憑證簽章請求(CSR):
openssl req -new -passin file:server_passphrase.txt -key server.key -subj "/CN=*.server.hostname" -out server.csr
在這裡,我們將使用先前建立的憑證授權單位簽署 CSR:
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
現在,伺服器的證書,即server.crt
已經準備好了。讓我們為客戶端建立證書。
我們也將對客戶端使用相同的 CA:
printf test > client_passphrase.txt
openssl genrsa -des3 -passout file:client_passphrase.txt -out client.key 2048
openssl rsa -passin file:client_passphrase.txt -in client.key -out client.key
openssl req -new -key client.key -subj "/CN=*.client.hostname" -out client.csr
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
與伺服器一樣,我們也對客戶端使用了client.key
的命令來產生金鑰和憑證。 client.key 和client.crt
分別是私鑰和憑證。
3.2. 向 Nginx 新增 mTLS
在這裡,我們將使用以下配置來設定 Nginx 以處理 mTLS 請求:
http {
server {
listen 443 ssl;
server_name test.server.hostname;
ssl_password_file /etc/nginx/certs/server_passphrase.txt;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client on;
ssl_verify_depth 3;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
location /ping {
proxy_pass http://localhost:9091;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
此 Nginx 設定在連接埠 443 上設定了一個啟用 mTLS 的 HTTPS 伺服器,要求用戶端提供由受信任 CA 簽署的有效憑證。它使用server.crt
和server.key
作為伺服器的憑證,從passphrase.txt
中讀取私鑰密碼,並根據ca.crt
驗證客戶端證書,驗證深度為 3。 /ping /ping
將請求代理到連接埠9091
上的本機服務,並轉送原始主機和用戶端 IP 標頭以取得上下文。
4. 將 mTLS 新增至 Java 用戶端
在使用 mTLS 請求呼叫上面建立的 Nginx 伺服器之前,我們首先需要為 Java 用戶端建立 SSL 配置。那麼,讓我們學習如何建構SSLContext
。
4.1. 建置SSLContext
讓我們逐步學習如何建立SSLContext
.
首先,我們將客戶端私鑰從 PEM 格式轉換為 DER:
openssl pkcs8 -topk8 -inform PEM -outform PEM -in client.key -out client.key.pkcs8 -nocrypt
透過在此範例中使用本機 Nginx 服務,我們需要停用主機名稱驗證:
final Properties props = System.getProperties();
props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());
下一步是將客戶端金鑰載入到 Java 程式碼中並建立KeyManagerFactory
:
String privateKeyPath = "/etc/crts/client.key.pkcs8";
String publicKeyPath = "/etc/crts/client.crt";
final byte[] publicData = Files.readAllBytes(Path.of(publicKeyPath));
final byte[] privateData = Files.readAllBytes(Path.of(privateKeyPath));
String privateString = new String(privateData, Charset.defaultCharset())
.replace("-----BEGIN PRIVATE KEY-----", "")
.replaceAll(System.lineSeparator(), "")
.replace("-----END PRIVATE KEY-----", "");
byte[] encoded = Base64.getDecoder().decode(privateString);
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
final Collection<? extends Certificate> chain = certificateFactory.generateCertificates(new ByteArrayInputStream(publicData));
Key key = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(encoded));
KeyStore clientKeyStore = KeyStore.getInstance("jks");
final char[] pwdChars = "test".toCharArray();
clientKeyStore.load(null, null);
clientKeyStore.setKeyEntry("test", key, pwdChars, chain.toArray(new Certificate[0]));
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(clientKeyStore, pwdChars);
在上述程式碼中,我們從憑證和金鑰檔案中讀取字節,並根據公鑰和私鑰實例建立憑證鏈。這些位元組用於建立包含憑證和私鑰的 Java KeyStore
。最後,使用KeyStore
初始化KeyManagerFactory
,以啟用安全的 SSL/TLS 通訊。
由於使用了自簽名證書,我們需要使用TrustManager
來接受它們。以下TrustManager
將接受伺服器提供的所有憑證:
TrustManager[] acceptAllTrustManager = { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(
X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(
X509Certificate[] certs, String authType) {
}
}};
現在,我們可以使用上面建立的KeyManagerFactory
初始化SSLContext
:
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), acceptAllTrustManager, new java.security.SecureRandom());
4.2. 使用 Java 用戶端
到目前為止,我們已經設定了 Nginx 伺服器並開啟了 mTLS,並且知道如何建置SSLContext.
接下來,讓我們逐一測試一下。首先,我們將編寫一個單元測試來驗證SSLContext
:
@Test
public void whenPrivateAndPublicKeysAreGiven_thenAnSSLContextShouldBeCreated(){
SSLContext sslContext = SslContextBuilder.buildSslContext();
Assertions.assertThat(sslContext).isNotNull();
}
由於我們的SSLContext
已準備就緒並且可以正常工作,我們現在可以使用 Java 客戶端對 Nginx 進行 mTLS 呼叫。
在使用 Java 用戶端測試之前,我們需要確保 Nginx 已啟動並執行。此外,我們還需要在 9091 連接埠運行一個伺服器,用於處理/ping
請求。我們可以按照本文輕鬆創建。為了通過這些測試,這兩個組件都是必要的。
我們首先使用HttpClient
進行測試:
@Test
public void whenWeExecuteMutualTLSCallToNginxServerWithHttpClient_thenItShouldReturnStatusOK() {
SSLContext sslContext = SslContextBuilder.buildSslContext();
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.build();
HttpRequest exactRequest = HttpRequest.newBuilder()
.uri(URI.create("https://localhost/ping"))
.GET()
.build();
HttpResponse<String> response = client.sendAsync(exactRequest, HttpResponse.BodyHandlers.ofString())
.join();
Assertions.assertThat(response).isNotNull();
Assertions.assertThat(response.statusCode()).isEqualTo(200);
}
上述測試將呼叫 Nginx,並將/ping
路由到在連接埠 9091 上執行的伺服器。我們將收到 200 代碼,這表示我們的請求已成功建立 mTLS 連線。
現在,我們將使用HttpsURLConnection:
@Test
public void whenWeExecuteMutualTLSCallToNginxServerWithHttpURLConnection_thenItShouldReturnNonNullResponse() {
SSLContext sslContext = SslContextBuilder.buildSslContext();
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL("https://127.0.0.1/ping")
.openConnection();
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
httpsURLConnection.setHostnameVerifier(HostNameVerifierBuilder.getAllHostsValid());
InputStream inputStream = httpsURLConnection.getInputStream();
String response = new String(inputStream.readAllBytes(), Charset.defaultCharset());
Assertions.assertThat(response).isNotNull();
}
就像上面的測試一樣,這將執行相同的流程。
5. 結論
在本文中,我們透過設定憑證授權單位並為伺服器和用戶端產生憑證來了解 mTLS。
然後,我們設定了一個 Nginx 伺服器,使用 mTLS 進行安全通訊。在客戶端,我們透過建立KeyManagerFactory
和TrustManager
並初始化SSLContext
,將憑證載入到 Java 應用程式中。
最後,我們測試了SSLConext
並使用 Java 用戶端(例如HttpClient
和HttpURLConnection.
本文使用的所有程式碼範例均可在 GitHub 上找到。