為什麼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) {}
}
}