為什麼RestTemplate 呼叫HTTPS 總報「主機名稱不符」? CertificateException 根源解析

2025.08.26

前言

在使用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) {}
    }
}