開發/測試環境常使用自簽憑證或內部 CA 簽發憑證,預設 JSSE `TrustManager` 僅信任 `cacerts` 內的公信 CA,導致 `javax.net.ssl.SSLHandshakeException: PKIX path building failed`。
# 使用 openssl 取得
openssl s_client -connect example.com:443 -servername example.com server.pem
keytool -importcert -alias myserver -keystore $JAVA_HOME/lib/security/cacerts -file server.pem -storepass changeit -noprompt
keytool -importcert -alias myserver -keystore my-truststore.jks -file server.pem -storepass secret -noprompt
# 啟動參數
-Djavax.net.ssl.trustStore=my-truststore.jks -Djavax.net.ssl.trustStorePassword=secret
| 優點 | 缺點 |
|---|---|
| 不修改代碼、JSSE 安全機制完整保留 | 憑證輪換需重新匯入、部署步驟增加 |
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
import java.security.SecureRandom;
public final class DevTrustManager implements X509TrustManager {
private final X509TrustManager defaultTm;
public DevTrustManager() throws Exception {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null); // 使用預設 cacerts
this.defaultTm = (X509TrustManager) tmf.getTrustManagers()[0];
}
@Override public void checkClientTrusted(X509Certificate[] chain, String authType) {
defaultTm.checkClientTrusted(chain, authType); // 維持客戶端驗證
}
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
try {
defaultTm.checkServerTrusted(chain, authType); // 先嘗試預設信任
} catch (CertificateException e) {
// 開發環境:記錄警告但允許通過
System.err.println("[DEV] 忽略憑證驗證: " + chain[0].getSubjectDN());
// 生產環境請直接拋出 e,或僅信任特定指紋/Subject DN
}
}
@Override public X509Certificate[] getAcceptedIssuers() {
return defaultTm.getAcceptedIssuers();
}
// 便利方法:建立全信任 SSLContext
public static SSLContext createInsecureContext() throws Exception {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new DevTrustManager()}, new SecureRandom());
return ctx;
}
}
SSLContext ctx = DevTrustManager.createInsecureContext();
HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory());
// 亦可單獨連線設定
HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
conn.setSSLSocketFactory(ctx.getSocketFactory());
SSLContext ctx = DevTrustManager.createInsecureContext();
HttpClient client = HttpClient.newBuilder()
.sslContext(ctx)
.build();
不信任 CA 體系,而是驗證伺服器憑證公開金鑰指紋 (SHA-256) 或 Subject Public Key Info (SPKI)。
public final class PinningTrustManager implements X509TrustManager {
private static final Set PINNED_SHA256 = Set.of(
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
);
private final X509TrustManager defaultTm;
public PinningTrustManager() throws Exception { /* 同前 */ }
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
defaultTm.checkServerTrusted(chain, authType); // 先通過 CA 驗證
// 再驗證釘選
boolean ok = Arrays.stream(chain).anyMatch(cert -> {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] der = cert.getPublicKey().getEncoded();
String pin = "sha256/" + Base64.getEncoder().encodeToString(md.digest(der));
return PINNED_SHA256.contains(pin);
} catch (Exception e) { return false; }
});
if (!ok) throw new CertificateException("憑證釘選失敗,公鑰指紋不符");
}
// ... 其他方法同前
}
優勢:防禦 CA 被駭/誤發憑證、中間人攻擊。挑戰:憑證輪換需同步更新客戶端、建議雙備份釘選 (當前 + 備援)。
JSSE 預設驗證 CN/SAN。自訂 `HostnameVerifier` (僅開發用):
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); // 危險!
// 或
conn.setHostnameVerifier((h, s) -> h.equals("expected-host"));
| 環境 | 推薦方案 | 禁忌 |
|---|---|---|
| 本地開發 | 方案一 (專用 TrustStore) 或 方案二 (全信任) | 提交含全信任代碼至版控 |
| CI/CD 測試 | 方案一 (注入測試 TrustStore) | 跳過驗證 |
| 預發/生產 | 公信 CA + 方案三 (釘選) | 任何形式的 TrustAll/HostnameVerifier 返回 true |
-Djavax.net.debug=ssl:handshake:trustmanager 觀察握手細節keytool -list -keystore cacerts -storepass changeit 檢視系統信任鏈「信任所有憑證」是技術債,而非解決方案。建立專用 TrustStore、自動化憑證輪換流程、生產上線憑證釘選,才是專業團隊的標準作業程序。