1. 概述

本文将展示如何为 Apache HttpClient 4 & 5 配置“全接受”SSL 支持。目标很简单:消费没有有效证书的 HTTPS URL。

如果想深入了解 HttpClient 的其他高级用法,可以参考 **HttpClient 完整指南**。

2. SSLPeerUnverifiedException 异常

未配置 SSL 的 HttpClient 在访问 HTTPS URL 时会失败:

@Test
void whenHttpsUrlIsConsumed_thenException() {
    String urlOverHttps = "https://localhost:8082/httpclient-simple";
    HttpGet getMethod = new HttpGet(urlOverHttps);

    assertThrows(SSLPeerUnverifiedException.class, () -> {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpResponse response = httpClient.execute(getMethod, new CustomHttpClientResponseHandler());
        assertThat(response.getCode(), equalTo(200));
    });
}

具体报错如下:

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
    at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:126)
    ...

当无法为 URL 建立有效的信任链时,就会抛出 SSLPeerUnverifiedException

3. 配置 SSL 全接受模式(HttpClient 5)

现在配置 HttpClient 信任所有证书链,无论其有效性如何:

@Test
void givenAcceptingAllCertificates_whenHttpsUrlIsConsumed_thenOk() throws GeneralSecurityException, IOException {

    final HttpGet getMethod = new HttpGet(HOST_WITH_SSL);

    final TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
    final SSLContext sslContext = SSLContexts.custom()
        .loadTrustMaterial(null, acceptingTrustStrategy)
        .build();
    final SSLConnectionSocketFactory sslsf = 
        new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    final Registry<ConnectionSocketFactory> socketFactoryRegistry = 
        RegistryBuilder.<ConnectionSocketFactory> create()
        .register("https", sslsf)
        .register("http", new PlainConnectionSocketFactory())
        .build();

    final BasicHttpClientConnectionManager connectionManager =
        new BasicHttpClientConnectionManager(socketFactoryRegistry);

    try( CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(connectionManager)
        .build();

        CloseableHttpResponse response = (CloseableHttpResponse) httpClient
            .execute(getMethod, new CustomHttpClientResponseHandler())) {

            final int statusCode = response.getCode();
            assertThat(statusCode, equalTo(HttpStatus.SC_OK));
    }
}

通过新的 TrustStrategy 覆盖标准证书验证流程(原本应检查配置的信任管理器),测试现在通过,客户端可以成功消费 HTTPS URL

4. 配置 SSL 全接受模式(HttpClient 4.5)

@Test
public final void givenAcceptingAllCertificates_whenHttpsUrlIsConsumed_thenOk()
  throws GeneralSecurityException {
    TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
    SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, 
      NoopHostnameVerifier.INSTANCE);
    
    Registry<ConnectionSocketFactory> socketFactoryRegistry = 
      RegistryBuilder.<ConnectionSocketFactory> create()
      .register("https", sslsf)
      .register("http", new PlainConnectionSocketFactory())
      .build();

    BasicHttpClientConnectionManager connectionManager = 
      new BasicHttpClientConnectionManager(socketFactoryRegistry);
    CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
      .setConnectionManager(connectionManager).build();

    HttpComponentsClientHttpRequestFactory requestFactory = 
      new HttpComponentsClientHttpRequestFactory(httpClient);
    ResponseEntity<String> response = new RestTemplate(requestFactory)
      .exchange(urlOverHttps, HttpMethod.GET, null, String.class);
    assertThat(response.getStatusCode().value(), equalTo(200));
}

5. Spring RestTemplate 集成 SSL(HttpClient 5)

了解了原生 HttpClient 的 SSL 配置后,我们来看更高级的客户端——Spring RestTemplate

未配置 SSL 时,测试会按预期失败:

@Test
void whenHttpsUrlIsConsumed_thenException() {
    final String urlOverHttps = "https://localhost:8443/httpclient-simple/api/bars/1";

    assertThrows(ResourceAccessException.class, () -> {
        final ResponseEntity<String> response = new RestTemplate()
            .exchange(urlOverHttps, HttpMethod.GET, null, String.class);
        assertThat(response.getStatusCode().value(), equalTo(200));
    });
}

现在配置 SSL:

@Test
void givenAcceptingAllCertificates_whenHttpsUrlIsConsumed_thenOk() throws GeneralSecurityException {

    final TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
    final SSLContext sslContext = SSLContexts.custom()
        .loadTrustMaterial(null, acceptingTrustStrategy)
        .build();
    final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> create()
        .register("https", sslsf)
        .register("http", new PlainConnectionSocketFactory())
        .build();

    final BasicHttpClientConnectionManager connectionManager = 
        new BasicHttpClientConnectionManager(socketFactoryRegistry);
    final CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(connectionManager)
        .build();

    final HttpComponentsClientHttpRequestFactory requestFactory =
        new HttpComponentsClientHttpRequestFactory(httpClient);
    final ResponseEntity<String> response = new RestTemplate(requestFactory)
        .exchange(urlOverHttps, HttpMethod.GET, null, String.class);
    assertThat(response.getStatusCode()
        .value(), equalTo(200));
}

可以看到,这与原生 HttpClient 的 SSL 配置非常相似——我们为请求工厂配置 SSL 支持,然后通过这个预配置的工厂实例化 RestTemplate

6. Spring RestTemplate 集成 SSL(HttpClient 4.5)

@Test
void givenAcceptingAllCertificates_whenUsingRestTemplate_thenCorrect() {
    final CloseableHttpClient httpClient = HttpClients.custom()
        .setSSLHostnameVerifier(new NoopHostnameVerifier())
        .build();
    final HttpComponentsClientHttpRequestFactory requestFactory
        = new HttpComponentsClientHttpRequestFactory();
    requestFactory.setHttpClient(httpClient);

    final ResponseEntity<String> response = new RestTemplate(requestFactory).exchange(urlOverHttps, HttpMethod.GET, null, String.class);
    assertThat(response.getStatusCode().value(), equalTo(200));
}

7. 总结

本文介绍了如何为 Apache HttpClient 配置 SSL,使其能够消费任何 HTTPS URL(无论证书有效性)。同时也展示了 Spring RestTemplate 的相同配置方案。

⚠️ 重要提醒:这种策略完全跳过了证书检查,存在安全风险,仅适用于明确需要忽略证书验证的场景。

完整示例代码可在 GitHub 项目 中找到。这是一个基于 Eclipse 的项目,可以直接导入运行。HttpClient 4 的示例位于 Apache HttpClient 4 模块

关键要点

✅ 通过 TrustStrategy 覆盖默认证书验证
✅ 使用 NoopHostnameVerifier 禁用主机名验证
✅ HttpClient 4 和 5 的配置略有差异
❌ 生产环境慎用全接受模式(存在安全风险)


原始标题:Apache HttpClient with SSL