为什么 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/cacerts1.

执行该命令时,需要输入信任库的默认密码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) {} } }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.

THE END