調用 HTTPS 服務時,若服務端證書非公信 CA 簽發(自簽名/內網 CA),JDK 默認 TrustStore 不信任,會拋出 PKIX path building failed。本文詳述 InstallCert 獲取證書、導入 TrustStore、代碼配置雙向認證的完整步驟。
源碼來自 Oracle 官方範例,核心邏輯:建立自定義 TrustManager 捕獲握手時的證書鏈,交互式選擇導入 jssecacerts。
public class InstallCert {
public static void main(String[] args) throws Exception {
// 解析 host:port、passphrase
// 載入或創建 jssecacerts
// 初始化 SSLContext + SavingTrustManager
// 建立 SSLSocket 執行握手
// 打印證書鏈信息(Subject、Issuer、SHA1、MD5)
// 用戶選擇導入哪張證書,寫入 jssecacerts
}
// SavingTrustManager 內部類捕獲 server certificate chain
}
使用步驟:
InstallCert.java。api.example.com:443 changeit(host:port + keystore 密碼)。SSLException 正常,輸入 1 回車選擇第一張證書。jssecacerts 文件。推薦複製到 JDK 目錄:
cp jssecacerts $JAVA_HOME/jre/lib/security/ # Linux/Mac
copy jssecacerts %JAVA_HOME%\jre\lib\security\ # Windows
或項目內部路徑,啟動參數指定:-Djavax.net.ssl.trustStore=./jssecacerts。
static {
// 1. 域名驗證(生產環境請嚴格校驗 CN/SAN)
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) ->
"api.example.com".equals(hostname) || "10.0.0.5".equals(hostname)
);
// 2. 指定 TrustStore
System.setProperty("javax.net.ssl.trustStore", "C:/Java/jdk1.8/jre/lib/security/jssecacerts");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
}
若服務端要求客戶端證書:
static {
// TrustStore(信任服務端)
System.setProperty("javax.net.ssl.trustStore", "/path/jssecacerts");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
// KeyStore(提供客戶端證書)
System.setProperty("javax.net.ssl.keyStore", "/path/client.p12"); // PKCS12 格式
System.setProperty("javax.net.ssl.keyStorePassword", "keystorePwd");
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
}
或程序化構建 SSLContext(推薦,線程安全、可熱更新):
SSLContext ctx = SSLContext.getInstance("TLS");
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream("client.p12"), "keystorePwd".toCharArray());
kmf.init(ks, "keypwd".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore ts = KeyStore.getInstance("JKS");
ts.load(new FileInputStream("jssecacerts"), "changeit".toCharArray());
tmf.init(ts);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory());
| 錯誤 | 原因 | 解決 |
|---|---|---|
| PKIX path building failed | TrustStore 缺少服務端根證書 | 重新運行 InstallCert 導入正確證書 |
| hostname in certificate didn't match | 證書 CN/SAN 不匹配請求域名/IP | 修正 HostnameVerifier 或獲取正確域名證書 |
| Received fatal alert: handshake_failure | 協議/加密套件不匹配、雙向認證缺客戶端證書 | 啟用 `-Djavax.net.debug=ssl:handshake` 分析 ClientHello/ServerHello |
| keystore password was incorrect | trustStorePassword/keyStorePassword 錯誤 | 核對 InstallCert 運行時輸入的 passphrase |