實作能夠存取 mTLS Rest API 的 Java HTTP Client
本文介紹兩種實作 mTLS Java HTTP Client 的方式,分為以下兩種:
1. 透過載入金鑰庫(Keystore)
2. 透過載入客戶端憑證
我們使用 Gradle 建立一個 Java 專案
plugins {
id 'java'
}
group = 'com.example.mtlsclient'
version = '1.0-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
test {
useJUnitPlatform()
}
以下為本專案的結構,由於 JDK 只認得 keystore 這個格式,因此請務必將前面生成的 PKCS#12 格式的 keystore file 名為 keystore.p12 放入本專案 resources 目錄底下:
mtls-client-demo/
├── build/
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── mtls-client-demo/
│ │ │ ├── MTLsHttpClientAuthViaKeystore.java
│ │ │ └── ...
│ │ └── resources/
│ │ └── keystore.p12
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── myproject/
│ │ ├── MTLsHttpClientAuthViaKeystoreTest.java
│ │ └── ...
│ └── resources/
│ ├── keystore.p12
│ └── ...
├── .gitignore
├── build.gradle
├── gradlew
├── gradlew.bat
├── README.md
└── settings.gradle
我們在 MTLsHttpClientAuthViaKeystore.java 中加入以下的程式碼:
package com.example.mtlsclient;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream;
import java.security.KeyStore;
public class MTLsHttpClientAuthViaKeystore {
private static final String KEYSTORE_TYPE = "PKCS12";
private static final String KEYSTORE_FILE = "keystore.p12";
private static final String KEYSTORE_PASSWORD = "password";
private static final String KEY_PASSWORD = "password";
private static final String API_ENDPOINT = "https://localhost:8080/api/v1/mtls/connect";
public static void main(String[] args) throws Exception {
// 載入 MTLS keystore
KeyStore mtlsKeyStore = KeyStore.getInstance(KEYSTORE_TYPE);
InputStream mtlsKeyStoreFile = MTLsHttpClientAuthViaKeystore.class.getClassLoader().getResourceAsStream(KEYSTORE_FILE);
mtlsKeyStore.load(mtlsKeyStoreFile, KEYSTORE_PASSWORD.toCharArray());
// 建立 MTLS SSL 環境
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(mtlsKeyStore, KEY_PASSWORD.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(mtlsKeyStore);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(mtlsKeyStore, null)
.loadKeyMaterial(mtlsKeyStore, KEY_PASSWORD.toCharArray())
.build();
// 將已建立的 MTLS SSL 環境載進 HTTP client
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setSSLSocketFactory(sslsf)
.build();
// 發送 Post 請求
HttpPost httpPost = new HttpPost(API_ENDPOINT);
HttpResponse httpResponse = httpClient.execute(httpPost);
// 處理伺服器的回應
String response = EntityUtils.toString(httpResponse.getEntity());
System.out.println(response);
}
}
執行這支簡易的 Java Client 呼叫我們預先準備好的 mTLS API 出現以下的結果,代表大功告成!
Profiling started
{"message":"Connect Succeed!"}
Process finished with exit code 0
-
註冊 BouncyCastle 作為 Java Security Provider。
-
載入 MTLS 信任憑證 (TrustStore) 和客戶端證書。
-
載入客戶端私鑰,支援 PEMEncryptedKeyPair 和 PKCS8EncryptedPrivateKeyInfo 兩種格式。
-
將客戶端私鑰添加到 KeyStore。
-
初始化 KeyManagerFactory 和 SSLContext。
-
建立 MTLS SSL 連線,並發送 POST 請求。
-
處理伺服器的回應。
首先我們添加以下的依賴庫到 Gradle file 中
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78'
我們在本專案 resources 目錄加入 rootCA.pem、client.pem、client.key 三個檔案:
mtls-client-demo/
├── build/
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── mtls-client-demo/
│ │ │ ├── MTLsHttpClientAuthViaKeystore.java
│ │ │ ├── MTLsHttpClientAuthViaCertificateAndKey.java
│ │ │ └── ...
│ │ └── resources/
│ ├── keystore.p12
│ ├── rootCA.pem
│ ├── client.pem
│ ├── client.key
│ └── ...
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── myproject/
│ │ ├── MTLsHttpClientAuthViaKeystoreTest.java
│ │ ├── MTLsHttpClientAuthViaCertificateAndKeyTest.java
│ │ └── ...
│ └── resources/
│ ├── keystore.p12
│ ├── rootCA.pem
│ ├── client.pem
│ ├── client.key
│ └── ...
├── .gitignore
├── build.gradle
├── gradlew
├── gradlew.bat
├── README.md
└── settings.gradle
我們在 MTLsHttpClientAuthViaCertificateAndKey.java 中加入以下的程式碼:
package com.example.mtlsclient;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class MTLsHttpClientAuthViaCertificateAndKey {
private static final String ROOT_CA_FILE = "rootCA.pem";
private static final String CLIENT_PEM_FILE = "client.pem";
private static final String CLIENT_KEY_FILE = "client.key";
private static final String KEY_PASSWORD = "password";
private static final String API_ENDPOINT = "https://localhost:8080/api/v1/mtls/connect";
static {
// 在類別載入時註冊 BouncyCastle 為 Java Security provider
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) throws Exception {
// 載入 MTLS 信任憑證 (TrustStore)
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
InputStream trustStoreInputStream = MTLsHttpClientAuthViaCertificateAndKey.class.getClassLoader().getResourceAsStream(ROOT_CA_FILE);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate caCert = (X509Certificate) cf.generateCertificate(trustStoreInputStream);
trustStore.setCertificateEntry("ca", caCert);
// 載入客戶端證書
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream clientCertStream = MTLsHttpClientAuthViaCertificateAndKey.class.getClassLoader().getResourceAsStream(CLIENT_PEM_FILE);
InputStream clientKeyStream = MTLsHttpClientAuthViaCertificateAndKey.class.getClassLoader().getResourceAsStream(CLIENT_KEY_FILE);
clientKeyStore.load(null, null);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
X509Certificate clientCert = (X509Certificate) certFactory.generateCertificate(clientCertStream);
clientKeyStore.setCertificateEntry("client", clientCert);
// 載入客戶端私鑰
PEMParser pemParser = new PEMParser(new InputStreamReader(clientKeyStream));
Object keyObject = pemParser.readObject();
JcePEMDecryptorProviderBuilder decryptorProviderBuilder = new JcePEMDecryptorProviderBuilder();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
KeyPair keyPair;
if (keyObject instanceof PEMEncryptedKeyPair) {
// 如果私鑰是 PEMEncryptedKeyPair 格式
PEMDecryptorProvider decProv = decryptorProviderBuilder.build(KEY_PASSWORD.toCharArray());
keyPair = converter.getKeyPair(((PEMEncryptedKeyPair) keyObject).decryptKeyPair(decProv));
} else {
// 如果私鑰是 PKCS8EncryptedPrivateKeyInfo 格式
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) keyObject;
InputDecryptorProvider inputDecProv = new JceOpenSSLPKCS8DecryptorProviderBuilder()
.setProvider("BC")
.build(KEY_PASSWORD.toCharArray());
PrivateKeyInfo privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(inputDecProv);
keyPair = new KeyPair(null, converter.getPrivateKey(privateKeyInfo));
}
pemParser.close();
// 將客戶端私鑰添加到 KeyStore
clientKeyStore.setKeyEntry("client", keyPair.getPrivate(), KEY_PASSWORD.toCharArray(), new Certificate[]{clientCert});
// 初始化 KeyManagerFactory
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, KEY_PASSWORD.toCharArray());
// 建立 MTLS SSL 環境
SSLContext sslContext = SSLContexts.custom()
.loadKeyMaterial(clientKeyStore, KEY_PASSWORD.toCharArray())
.loadTrustMaterial(trustStore, null)
.build();
// 建立 MTLS SSL 連線
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLContext(sslContext)
.build();
// 發送 POST 請求
HttpPost httpPost = new HttpPost(API_ENDPOINT);
HttpResponse httpResponse = httpClient.execute(httpPost);
// 處理伺服器的回應
String response = EntityUtils.toString(httpResponse.getEntity());
System.out.println(response);
}
}
執行本 Java Client 呼叫我們預先準備好的 mTLS API 出現以下結果,大功告成!
Profiling started
{"message":"Connect Succeed!"}
Process finished with exit code 0
效能提升
我們會發現,執行上述 Client 程式時,註冊 BouncyCastle 的過程會佔去15%的時間!明顯影響執行效能。而每一次與伺服器建立 mTLS 連線都要重新註冊也不現實。因此我們可以在正式環境中設定 jre 預先載入,讓執行環境隨著 OS 啟動時一次性的完成這件事。
-
將 BouncyCastle 的 jar 放入 {JAVA_HOME}/jre/lib/ext
-
編輯 {JAVA_HOME}/jre/lib/security/java.security 加入以下設定:
security.provider.N = org.bouncycastle.jce.provider.BouncyCastleProvider
如此一來,在 jre 隨著 OS 啟動時,就會預先載入 BouncyCastle。至於程式的部分也可以將以下程式碼移除,避免重複註冊。
// 由於已在Java環境啟動階段將BouncyCastle設為預設的安全性提供者,為了避免重複註冊,故移除以下程式碼
static {
Security.addProvider(new BouncyCastleProvider());
}
參考來源:
1. https://www.baeldung.com/java-ssl-debug-logging
2. refactorizando-web/Mutual-TLS: Mutual TLS example with Spring Boot and WebClient (github.com)
3. Introduction to BouncyCastle with Java | Baeldung
4. mTLS: When certificate authentication is done wrong - The GitHub Blog