首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JSSE 實戰:信任自簽/未驗證 HTTPS 憑證的兩種方案與安全權衡

     編輯:Java 相關知識

問題背景

開發/測試環境常使用自簽憑證或內部 CA 簽發憑證,預設 JSSE `TrustManager` 僅信任 `cacerts` 內的公信 CA,導致 `javax.net.ssl.SSLHandshakeException: PKIX path building failed`。

方案一:將憑證匯入 TrustStore (推薦用於測試/受控環境)

步驟

  1. 取得伺服器憑證 (`.cer` / `.pem` / `.der`)
    
    # 使用 openssl 取得
    openssl s_client -connect example.com:443 -servername example.com  server.pem
    
  2. 匯入 JDK TrustStore
    
    keytool -importcert -alias myserver -keystore $JAVA_HOME/lib/security/cacerts -file server.pem -storepass changeit -noprompt
    
  3. 或建立專用 TrustStore (不污染系統)
    
    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 安全機制完整保留憑證輪換需重新匯入、部署步驟增加

方案二:自訂 X509TrustManager (僅限開發/測試,嚴禁生產)

完整實作 (支援選擇性信任)


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;
    }
}

套用至 HttpURLConnection


SSLContext ctx = DevTrustManager.createInsecureContext();
HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory());
// 亦可單獨連線設定
HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
conn.setSSLSocketFactory(ctx.getSocketFactory());

套用至 HttpClient (Java 11+)


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 檢視系統信任鏈
  • 使用 SSL Labs Scanner 驗證伺服器配置

結語

「信任所有憑證」是技術債,而非解決方案。建立專用 TrustStore、自動化憑證輪換流程、生產上線憑證釘選,才是專業團隊的標準作業程序。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved