解决Java中IP地址访问HTTPS接口的SSL证书验证问题

问题描述

在使用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证书验证,主要包括两个方面:

  1. 证书有效性验证:检查证书是否过期、是否被吊销等
  2. 主机名验证:检查证书中的域名或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);
    // 处理响应...
}

注意事项

⚠️ 安全警告

  1. 仅用于开发/测试环境:跳过SSL验证会带来安全风险,生产环境不应该使用
  2. 中间人攻击风险:攻击者可以伪造证书,你的应用无法识别
  3. 数据泄露风险:无法保证连接的安全性

生产环境建议

如果必须在生产环境使用,建议:

  1. 使用正确的证书:让服务器提供包含IP地址的证书
  2. 配置证书信任:只信任特定的证书颁发机构(CA)
  3. 使用域名访问:尽量使用域名而不是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,说明配置生效了。

总结

  1. 问题根源:Java的SSL验证严格,证书中没有IP地址时会拒绝连接
  2. 解决方案 :配置RestTemplate跳过SSL证书验证
  3. 关键代码 :自定义X509TrustManager + NoopHostnameVerifier
  4. 安全提醒:仅用于开发/测试环境,生产环境应使用正确的证书

参考资源

相关推荐
日日大王学编程5 小时前
阿里巴巴开源 Data Agent(Text2SQL)项目全维度详解
java·ai·开源
Wang's Blog5 小时前
Java 项目实战:交友项目 - 交友左滑右滑
java·交友
测试盐5 小时前
spark作业日志定位过程
java·hadoop·spark
ly76895 小时前
从 OpenAPI 到多语言 SDK 生成:Java 服务端契约漂移与生成器版本治理
java·开发语言·openapi·java 21·版本兼容·契约漂移·sdk 生成
lqbzlll5 小时前
CVE-2026-91403 排查指南:Java JSSE 信任链漏洞的根因、影响范围和修复步骤
java·开发语言·信任链
CarIise5 小时前
AOP 概念的引入
java·开发语言
重生之我是Java开发战士5 小时前
【Redis】Java & JavaSpring 客户端
java·redis·bootstrap
tedcloud1236 小时前
Docker Compose 部署 Uptime Kuma:配置 HTTPS、QQ 邮箱告警与数据备份
服务器·网络协议·ui·docker·容器·https·开源
嵌入式学习菌6 小时前
HTTP/HTTPS、MQTT/MQTTS 区别
网络协议·http·https
郭小李6 小时前
mybatis的 Mapper.xml语法
xml·java·mybatis