解决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. 安全提醒:仅用于开发/测试环境,生产环境应使用正确的证书

参考资源

相关推荐
墨着染霜华2 小时前
IntelliJ IDEA 设置导出与导入完整指南(备份 / 迁移 / 团队共享)
java·ide·intellij-idea
浮游本尊2 小时前
Java学习第32天 - 性能优化与架构设计
java
五阿哥永琪2 小时前
Nacos注册/配置中心
java·开发语言
无敌最俊朗@2 小时前
Qt多线程阻塞:为何信号失效?
java·开发语言
__万波__2 小时前
二十三种设计模式(十四)--命令模式
java·设计模式·命令模式
一起养小猫2 小时前
《Java数据结构与算法》第四篇(三)二叉树遍历详解_CSDN文章
java·开发语言·数据结构
少许极端2 小时前
算法奇妙屋(十九)-子序列问题(动态规划)
java·数据结构·算法·动态规划·子序列问题
小满、2 小时前
RabbitMQ:AMQP 原理、Spring AMQP 实战与 Work Queue 模型
java·rabbitmq·java-rabbitmq·spring amqp·amqp 协议·work queue