Springboot 3 中 httpclient5 如何修改 ClientHttpRequestFactory 使之支持SSL

0 阅读5分钟

在 Spring Boot 3 中,如果你想使用 Apache HttpClient 5 作为 RestTemplate 的底层实现,并需要自定义 SSL 配置(例如,信任自签名证书、使用特定的信任库/密钥库),你需要创建一个自定义的 ClientHttpRequestFactory

推荐的方式是使用 RestTemplateBuilder 来配置 RestTemplate,并为其提供一个配置了自定义 SSLContext 的 HttpClient 实例。

以下是详细步骤和示例代码:

1. 添加依赖

首先,确保你的 pom.xml (Maven) 或 build.gradle (Gradle) 文件中包含了 HttpClient 5 的依赖。Spring Boot 的 spring-boot-starter-web 默认不包含它,你需要显式添加:

Maven (pom.xml):

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <!-- Spring Boot 会管理版本,如果需要可以指定 -->
    <!-- <version>5.x.x</version> -->
</dependency>

Gradle (build.gradle):

implementation 'org.apache.httpcomponents.client5:httpclient5'

2. 创建自定义 SSLContext

你需要根据你的具体需求(信任所有证书、信任特定证书、使用客户端证书等)来创建 SSLContext

  • 信任所有证书 (仅用于测试,不安全!)
  • 加载自定义信任库 (TrustStore)
  • 加载自定义密钥库 (KeyStore,用于客户端认证)

示例:加载自定义 TrustStore (例如 mytruststore.jks)

假设你有一个名为 mytruststore.jks 的 JKS 格式的信任库文件,密码是 password,并且它位于 classpath 下。

import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; // 如果信任自签名
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy; // 用于信任所有等策略

import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

// ... other imports

private SSLContext createCustomSslContext() {
    try {
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); // 或者 "JKS", "PKCS12" 等

        // --- 从 Classpath 加载 TrustStore ---
        // 推荐使用 ResourceLoader 来加载资源
        // @Autowired private ResourceLoader resourceLoader;
        // Resource trustStoreResource = resourceLoader.getResource("classpath:mytruststore.jks");
        // try (InputStream trustStoreStream = trustStoreResource.getInputStream()) {
        //    trustStore.load(trustStoreStream, "password".toCharArray());
        // }

        // --- 或者直接使用 ClassLoader (简化示例) ---
        try (InputStream trustStoreStream = getClass().getClassLoader().getResourceAsStream("mytruststore.jks")) {
            if (trustStoreStream == null) {
                throw new RuntimeException("Truststore 'mytruststore.jks' not found in classpath");
            }
            trustStore.load(trustStoreStream, "password".toCharArray());
        }


        // --- 创建 SSLContext ---
        // 1. 信任加载的 TrustStore 中的证书
        SSLContext sslContext = SSLContexts.custom()
                .loadTrustMaterial(trustStore, null) // 第二个参数是 TrustStrategy,null 表示使用标准信任链验证
                .build();

        // 2. 或者:信任自签名证书 (如果 TrustStore 包含自签名 CA)
        // SSLContext sslContext = SSLContexts.custom()
        //        .loadTrustMaterial(trustStore, new TrustSelfSignedStrategy())
        //        .build();

        // 3. 或者:信任所有证书 (非常不安全,仅用于测试)
        // TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
        // SSLContext sslContext = SSLContexts.custom()
        //        .loadTrustMaterial(null, acceptingTrustStrategy)
        //        .build();


        // --- 如果需要加载 KeyStore (客户端证书) ---
        // KeyStore keyStore = KeyStore.getInstance("PKCS12"); // 或 JKS
        // try (InputStream keyStoreStream = getClass().getClassLoader().getResourceAsStream("mykeystore.p12")) {
        //    keyStore.load(keyStoreStream, "keyPassword".toCharArray());
        // }
        // sslContext = SSLContexts.custom()
        //        .loadTrustMaterial(trustStore, null) // 信任库
        //        .loadKeyMaterial(keyStore, "keyStorePassword".toCharArray()) // 密钥库
        //        .build();


        return sslContext;

    } catch (Exception e) {
        // 处理异常,例如记录日志或抛出 RuntimeException
        throw new RuntimeException("Failed to create custom SSL context", e);
    }
}

3. 创建配置了 SSL 的 HttpClient

使用上面创建的 SSLContext 来配置 SSLConnectionSocketFactory,然后构建 HttpClient

import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; // 如果需要禁用主机名验证(不安全)
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.apache.hc.core5.reactor.ssl.TlsDetails;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.Timeout;
// ... 其他 imports

private CloseableHttpClient createHttpClientWithCustomSsl() {
    try {
        SSLContext sslContext = createCustomSslContext(); // 调用上面创建 SSLContext 的方法

        // --- 配置 SSLConnectionSocketFactory ---
        SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create()
                .setSslContext(sslContext)
                // 可选:禁用主机名验证 (仅用于测试,不安全)
                // .setHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                .build();

        // --- 创建连接管理器 ---
        HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
                .setSSLSocketFactory(sslSocketFactory) // 应用 SSL Socket Factory
                // 可选:其他连接池配置
                // .setMaxConnTotal(100)
                // .setMaxConnPerRoute(20)
                // .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(Timeout.ofSeconds(30)).build())
                .build();


        // --- 构建 HttpClient ---
        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                // 可选:设置请求配置 (超时等)
                // .setDefaultRequestConfig(RequestConfig.custom()
                //      .setConnectTimeout(Timeout.ofSeconds(5))
                //      .setResponseTimeout(Timeout.ofSeconds(30))
                //      .build())
                // 可选: 禁用重试
                // .disableAutomaticRetries()
                // 可选: 禁用内容压缩
                // .disableContentCompression()
                .build();

        return httpClient;

    } catch (Exception e) {
        throw new RuntimeException("Failed to create HttpClient with custom SSL", e);
    }
}

4. 配置 RestTemplate Bean

在你的 Spring Boot 配置类 (@Configuration) 中,定义一个 RestTemplate Bean,并使用 RestTemplateBuilder 来注入上面创建的自定义 HttpClient

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;

// ... 其他 SSL 和 HttpClient 相关 imports

@Configuration
public class RestTemplateConfig {

    // 如果需要从 classpath 加载资源,注入 ResourceLoader
    // @Autowired
    // private ResourceLoader resourceLoader;

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        // 1. 创建自定义的 HttpClient
        CloseableHttpClient httpClient = createHttpClientWithCustomSsl();

        // 2. 创建 HttpComponentsClientHttpRequestFactory
        // 这个工厂会使用我们自定义的 HttpClient
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(httpClient);

        // 可选: 设置连接超时和读取超时 (也可以在 HttpClient 中配置)
        // requestFactory.setConnectTimeout(5000); // 5 seconds
        // requestFactory.setReadTimeout(30000);    // 30 seconds

        // 3. 使用 RestTemplateBuilder 应用自定义的 RequestFactory
        return builder
                .requestFactory(() -> requestFactory) // 提供自定义工厂
                .build();
    }

    // --- 将上面创建 SSLContext 和 HttpClient 的方法放在这里 ---
    private SSLContext createCustomSslContext() {
        // ... (实现如上)
        try {
            // 示例: 信任所有证书 (仅测试)
            TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
            return SSLContexts.custom()
                    .loadTrustMaterial(null, acceptingTrustStrategy)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException("Failed to create custom SSL context", e);
        }
    }

    private CloseableHttpClient createHttpClientWithCustomSsl() {
        // ... (实现如上)
        try {
            SSLContext sslContext = createCustomSslContext();
            SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create()
                    .setSslContext(sslContext)
                    .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) // 仅测试
                    .build();
            HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
                    .setSSLSocketFactory(sslSocketFactory)
                    .build();
            return HttpClients.custom()
                    .setConnectionManager(connectionManager)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException("Failed to create HttpClient with custom SSL", e);
        }
    }

    // --- 定义 TrustStrategy ---
    private static class TrustAllStrategy implements TrustStrategy {
        @Override
        public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            return true;
        }
    }
}

解释:

  1. createCustomSslContext(): 负责根据你的策略(信任库、密钥库、信任所有等)构建 SSLContext。根据实际情况修改这部分代码。
  2. createHttpClientWithCustomSsl(): 使用上面创建的 SSLContext 初始化 SSLConnectionSocketFactory,然后用这个工厂配置 HttpClientConnectionManager (推荐使用连接池 PoolingHttpClientConnectionManagerBuilder),最后构建 CloseableHttpClient
  3. restTemplate(RestTemplateBuilder builder): 这是 Spring Boot 配置 RestTemplate 的标准方式。
    • 我们先调用 createHttpClientWithCustomSsl() 创建一个带有自定义 SSL 配置的 HttpClient 实例。
    • 然后创建一个 HttpComponentsClientHttpRequestFactory 实例,并将自定义的 HttpClient 设置给它。HttpComponentsClientHttpRequestFactory 是 Spring 提供的适配器,用于将 Apache HttpClient 集成到 Spring 的 ClientHttpRequestFactory 体系中。
    • 最后,通过 builder.requestFactory(() -> requestFactory) 将这个自定义的工厂提供给 RestTemplateBuilder。注意这里使用 lambda 表达式提供了一个 Supplier<ClientHttpRequestFactory>
    • builder.build() 返回最终配置好的 RestTemplate 实例。

现在,通过 @Autowired 注入这个 RestTemplate Bean 的任何地方,它都会使用配置了自定义 SSL 的 HttpClient 5 来发起 HTTPS 请求。

重要安全提示:

  • 避免信任所有证书 (TrustAllStrategy) 和禁用主机名验证 (NoopHostnameVerifier) 在生产环境中使用! 这会使你的应用程序容易受到中间人攻击。只应在严格控制的测试环境中使用。
  • 安全地管理密码: 不要将信任库/密钥库的密码硬编码在代码中。使用 Spring Boot 的配置属性 (application.propertiesapplication.yml),并通过 @Value 或配置类注入,或者使用更安全的秘密管理工具。
  • 使用正确的信任库: 确保你的信任库只包含你确实信任的证书颁发机构 (CA) 或服务器证书。

选择最适合你场景的 SSLContext 创建方式,并确保资源文件(如 .jks, .p12 文件)能够被正确加载(通常放在 src/main/resources 下)。