為什麼RestTemplate 呼叫HTTPS 總報「主機名稱不符」? CertificateException 根源解析
                    前言
在使用RestTemplate呼叫 API的過程中,我們可能會遇到java.security.cert.CertificateException: No name matching這樣的錯誤。
原因解析
java.security.cert.CertificateException: No name matching錯誤本質上是SSL憑證驗證失敗的一種表現。當RestTemplate透過HTTPS協定呼叫API時,會對伺服器傳回的SSL憑證進行驗證。其中,憑證中的主機名稱(Common Name,簡稱 CN)或主題備用名稱(Subject Alternative Name,簡稱 SAN)需要與我們實際呼叫的API的主機名稱相符。如果不匹配,就會觸發該錯誤,這是Java的SSL/TLS機制為了保障通訊安全而採取的措施,防止中間人攻擊等安全風險。
解決方法
方法一:忽略SSL 憑證驗證(僅適用於開發環境)
建立一個信任所有憑證的SSLContext,並將其套用到RestTemplate。具體代碼如下:
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() throws Exception {
        // 创建信任所有证书的SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    }
                    @Override
                    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    }
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }
                }
        };
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
        // 创建HostnameVerifier,信任所有主机名
        HostnameVerifier hostnameVerifier = (s, sslSession) -> true;
        // 配置RestTemplate
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
        requestFactory.setConnectTimeout(30000);
        requestFactory.setReadTimeout(30000);
        return new RestTemplate(requestFactory);
    }
}- 1.
 - 2.
 - 3.
 - 4.
 - 5.
 - 6.
 - 7.
 - 8.
 - 9.
 - 10.
 - 11.
 - 12.
 - 13.
 - 14.
 - 15.
 - 16.
 - 17.
 - 18.
 - 19.
 - 20.
 - 21.
 - 22.
 - 23.
 - 24.
 - 25.
 - 26.
 - 27.
 - 28.
 - 29.
 - 30.
 - 31.
 - 32.
 - 33.
 - 34.
 - 35.
 - 36.
 - 37.
 - 38.
 
方法二:設定正確的SSL 憑證(適用於生產環境)
- 取得正確的SSL證書:從API服務提供者取得包含正確主機名稱(CN或SAN)的 SSL 證書,通常為.cer或.pem格式。
 - 匯入憑證到信任庫:使用Java的keytool工具將憑證匯入Java的信任庫。命令如下:
 
keytool -import -alias apiCert -file /path/to/certificate.cer -keystore $JAVA_HOME/jre/lib/security/cacerts- 1.
 
❝
執行此指令時,需要輸入信任庫的預設密碼changeit
- 配置RestTemplate使用信任庫:在建立RestTemplate時,指定使用包含正確憑證的信任庫。程式碼如下:
 
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() throws Exception {
        // 加载信任库
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        FileInputStream fis = new FileInputStream("/path/to/truststore/apiCert.jks");
        trustStore.load(fis, "truststorePassword".toCharArray());
        fis.close();
        // 初始化TrustManagerFactory
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);
        // 创建SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());
        // 配置RestTemplate
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        requestFactory.setConnectTimeout(30000);
        requestFactory.setReadTimeout(30000);
        return new RestTemplate(requestFactory);
    }
}- 1.
 - 2.
 - 3.
 - 4.
 - 5.
 - 6.
 - 7.
 - 8.
 - 9.
 - 10.
 - 11.
 - 12.
 - 13.
 - 14.
 - 15.
 - 16.
 - 17.
 - 18.
 - 19.
 - 20.
 - 21.
 - 22.
 - 23.
 - 24.
 - 25.
 - 26.
 - 27.
 - 28.
 
總結
RestTemplate能同時相容於HTTP和HTTPS協議,是因為RestTemplate底層會依據請求的URL協議(http或https)自動選擇對應的處理邏輯。當請求為HTTP時,不會觸發SSL憑證驗證相關的流程,直接依照HTTP的通訊方式進行資料傳輸;當請求為HTTPS時,才會運用我們設定的SSLContext等相關參數進行憑證驗證和加密通訊。
在生產環境中,為了更靈活地相容於兩種協議,我們可以對RestTemplate的配置進行進一步最佳化,使用HttpComponentsClientHttpRequestFactory取代SimpleClientHttpRequestFactory,它對HTTP和HTTPS的支援更為完善。
public class RestTemplateConfig {
    public RestTemplate restTemplate() throws Exception {
        // 加载信任库
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new FileInputStream("path/to/truststore"), "truststorePassword".toCharArray());
        // 构建SSLContext
        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial(trustStore, null)
                .build();
        // 创建SSL连接套接字工厂
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
        // 创建HttpClient
        HttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(sslSocketFactory)
                .build();
        // 配置请求工厂
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        requestFactory.setConnectTimeout(30000);
        requestFactory.setReadTimeout(30000);
        return new RestTemplate(requestFactory);
    }
}- 1.
 - 2.
 - 3.
 - 4.
 - 5.
 - 6.
 - 7.
 - 8.
 - 9.
 - 10.
 - 11.
 - 12.
 - 13.
 - 14.
 - 15.
 - 16.
 - 17.
 - 18.
 - 19.
 - 20.
 - 21.
 - 22.
 - 23.
 - 24.
 - 25.
 - 26.
 - 27.
 - 28.
 
也可以SimpleClientHttpRequestFactory實作協定自適應處理,具體步驟如下:
public class DualProtocolRequestFactory extends SimpleClientHttpRequestFactory {
    @Override
    protected void prepareConnection(HttpURLConnection connection, String httpMethod) {
        try {
            // HTTP请求直接处理
            if (!(connection instanceof HttpsURLConnection)) {
                super.prepareConnection(connection, httpMethod);
                return;
            }
            // HTTPS请求跳过证书验证(仅测试环境)
            HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{new BlindTrustManager()}, null);
            httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
            httpsConnection.setHostnameVerifier((hostname, session) -> true); // 禁用主机名验证
            super.prepareConnection(httpsConnection, httpMethod);
        } catch (Exception e) {
            throw new RuntimeException("HTTPS配置失败", e);
        }
    }
    private static class BlindTrustManager implements X509TrustManager {
        public X509Certificate[] getAcceptedIssuers() { return null; }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }
}