问题描述
在使用Java的RestTemplate访问HTTPS接口时,如果使用IP地址而不是域名,经常会遇到以下错误:
java.security.cert.CertificateException: No subject alternative names matching IP address 121.63.81.79 found
nested exception is javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names matching IP address 121.63.81.79 found
奇怪的是:使用Postman、curl等工具可以正常访问,但Java代码却报错。
问题原因
SSL证书验证机制
HTTPS协议在建立连接时,会进行SSL证书验证,主要包括两个方面:
- 证书有效性验证:检查证书是否过期、是否被吊销等
- 主机名验证:检查证书中的域名或IP地址是否与访问的地址匹配
为什么Postman可以,Java不行?
- Postman:默认情况下,Postman可能会跳过某些SSL验证(特别是开发环境)
- Java :Java的SSL验证非常严格,会检查证书的
Subject Alternative Names (SAN)字段 - 问题根源:当使用IP地址访问时,如果证书的SAN中没有包含该IP地址,Java就会拒绝连接
证书的Subject Alternative Names
SSL证书中有一个字段叫Subject Alternative Names (SAN),它列出了该证书可以用于哪些域名或IP地址。例如:
Subject Alternative Names:
DNS: example.com
DNS: www.example.com
IP Address: 192.168.1.1
如果证书中没有包含你访问的IP地址(如121.63.81.79),Java就会抛出上述异常。
解决方案
方案概述
我们需要配置RestTemplate,让它跳过SSL证书验证。注意:这种方法只适用于开发/测试环境,生产环境应该使用正确的证书。
完整代码实现
创建一个RestTemplateConfig配置类:
java
package com.example.zbx.config;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
* RestTemplate配置类
* 配置忽略SSL证书验证,支持使用IP地址访问HTTPS接口
* @author lxy
*/
@Configuration
public class RestTemplateConfig {
/**
* 创建RestTemplate Bean
* 配置忽略SSL证书验证,支持使用IP地址访问HTTPS接口
* @return RestTemplate实例
*/
@Bean
public RestTemplate restTemplate() {
try {
// 创建完全信任所有证书的TrustManager
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
// 不进行任何检查,信任所有客户端证书
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// 不进行任何检查,信任所有服务器证书
}
}
};
// 创建SSL上下文,使用自定义的TrustManager
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// 创建SSL连接工厂,忽略主机名验证(包括IP地址验证)
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE
);
// 创建请求配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(10000) // 连接超时10秒
.setConnectionRequestTimeout(10000) // 请求超时10秒
.setSocketTimeout(30000) // 读取超时30秒
.build();
// 创建HttpClient
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.evictIdleConnections(30, java.util.concurrent.TimeUnit.SECONDS)
.build();
// 创建请求工厂
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
factory.setConnectTimeout(10000);
factory.setConnectionRequestTimeout(10000);
factory.setReadTimeout(30000);
// 创建RestTemplate
return new RestTemplate(factory);
} catch (Exception e) {
throw new RuntimeException("创建RestTemplate失败", e);
}
}
}
关键点说明
1. 自定义TrustManager
java
TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// 不进行任何检查,信任所有服务器证书
}
// ... 其他方法
}
};
这个X509TrustManager会跳过所有证书验证,包括:
- 证书是否过期
- 证书是否被吊销
- 证书的域名/IP是否匹配
2. 创建SSL上下文
java
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
使用自定义的TrustManager初始化SSL上下文。
3. 忽略主机名验证
java
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE // 不验证主机名
);
NoopHostnameVerifier.INSTANCE会跳过主机名验证,这样即使证书中没有对应的IP地址也能通过验证。
Maven依赖
确保你的pom.xml中包含以下依赖:
xml
<!-- HttpClient4 (for RestTemplate SSL configuration) -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
使用示例
配置完成后,直接使用RestTemplate即可,无需额外代码:
java
@Autowired
private RestTemplate restTemplate;
public void callApi() {
String url = "https://121.63.81.79:10091/api/endpoint";
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
// 处理响应...
}
注意事项
⚠️ 安全警告
- 仅用于开发/测试环境:跳过SSL验证会带来安全风险,生产环境不应该使用
- 中间人攻击风险:攻击者可以伪造证书,你的应用无法识别
- 数据泄露风险:无法保证连接的安全性
生产环境建议
如果必须在生产环境使用,建议:
- 使用正确的证书:让服务器提供包含IP地址的证书
- 配置证书信任:只信任特定的证书颁发机构(CA)
- 使用域名访问:尽量使用域名而不是IP地址
示例:生产环境的正确做法
java
// 只信任特定的证书
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("truststore.jks"), "password".toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
常见问题
Q1: 为什么之前可以,现在不行了?
可能的原因:
- Java版本更新,SSL验证更严格了
- 服务器证书更新了,新证书没有包含IP地址
- 网络环境变化,使用了不同的IP地址
Q2: 有没有更简单的办法?
对于开发环境,可以设置JVM参数(不推荐):
bash
-Dcom.sun.net.ssl.checkRevocation=false
但这种方法不够灵活,建议使用配置类的方式。
Q3: 会影响其他HTTP请求吗?
不会。这个配置只影响使用RestTemplate的HTTPS请求,HTTP请求不受影响。
Q4: 如何验证配置是否生效?
在代码中添加日志,观察是否还有证书验证错误:
java
log.info("RestTemplate配置完成,SSL验证已禁用");
如果不再出现CertificateException,说明配置生效了。
总结
- 问题根源:Java的SSL验证严格,证书中没有IP地址时会拒绝连接
- 解决方案 :配置
RestTemplate跳过SSL证书验证 - 关键代码 :自定义
X509TrustManager+NoopHostnameVerifier - 安全提醒:仅用于开发/测试环境,生产环境应使用正确的证书