1. 概述

在这个教程中,我们将学习如何使用Java HttpClient连接HTTPS URL,并处理那些没有有效SSL证书的URL。在早期的Java版本中,我们更倾向于使用Apache HTTPClient或OkHttp库。然而,在Java 11中,JDK增加了改进的HttpClient库。

让我们一起探索如何使用它来通过SSL调用服务。

2. 使用Java HttpClient调用HTTPS URL

我们将通过示例代码运行客户端。为了测试目的,我们将使用一个已存在的HTTPS运行的URL。

首先编写代码设置客户端并调用服务:

HttpClient httpClient = HttpClient.newHttpClient();
    
HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("https://www.google.com/"))
  .build();

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

这里,我们首先使用HttpClient.newHttpClient()方法创建了一个客户端,然后创建了一个请求并设置了我们将要访问的服务URL。最后,我们使用HttpClient.send()方法发送请求,并收集响应——一个包含响应体的HttpResponse对象。

当我们在测试用例中放置上述代码并执行以下断言时,我们会看到它通过了:

assertEquals(200, response.statusCode());

3. 调用无效的HTTPS URL

现在,我们将URL更改为一个没有有效SSL证书的另一个URL。我们可以通过更改请求对象来实现:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://wrong.host.badssl.com/"))
  .build();

当我们再次运行测试时,会得到以下错误:

Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching wrong.host.badssl.com found.
  at java.base/sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:212)
  at java.base/sun.security.util.HostnameChecker.match(HostnameChecker.java:103)

这是因为URL没有有效的SSL证书。

4. 避免SSL证书验证

4.1. 使用模拟信任管理器

为了跳过HttpClient自动执行的验证,我们可以扩展默认的 X509ExtendedTrustManager 对象,它是负责检查SSL证书有效性的类,通过重写默认的业务逻辑,使其成为空方法:

private static final TrustManager MOCK_TRUST_MANAGER = new X509ExtendedTrustManager() {
   @Override
   public java.security.cert.X509Certificate[] getAcceptedIssuers() {
       return new java.security.cert.X509Certificate[0];
   }

   @Override
   public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
       // empty method
   }
   // ... Other void methods
}

接着,我们需要将新的信任管理器实现提供给我们的HttpClient实例。为此,我们首先创建一个SSLContextHttpClient实例的新实例,然后初始化对象,传入一个空的KeyManager列表,一个新创建的信任管理器数组,以及SecureRandom类的一个新实例(随机数生成器):

SSLContext sslContext = SSLContext.getInstance("SSL"); // OR TLS
sslContext.init(null, new TrustManager[]{DUMMY_TRUST_MANAGER}, new SecureRandom());

现在,我们可以使用这个更新后的SSLContext构建我们的HttpClient:

HttpClient httpClient = HttpClient.newBuilder().sslContext(sslContext).build();

现在我们可以调用任何没有有效证书的URL,仍然可以得到成功响应:

HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI("https://wrong.host.badssl.com/"))
            .build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(200, response.statusCode())

在下一段,我们将看到另一种方法,通过JVM标志来实现相同的结果,这将禁用每个HttpClient请求的证书验证。

4.2. 使用JVM禁用主机验证标志

作为解决上述问题的最后一种方法,我们将探讨一种绕过SSL证书验证的解决方案。

在Apache HttpClient中,我们可以修改客户端以绕过证书验证。然而,对于Java HttpClient,我们无法做到这一点。我们必须依赖于对JVM进行更改,以禁用主机验证。

一种方法是将网站的证书导入到Java Keystore。如果内部信任的网站数量较少,这是一个常见的做法。

但如果需要管理的网站数量众多或环境众多,这可能会变得繁琐。在这种情况下,我们可以使用属性jdk.internal.httpclient.disableHostnameVerification来禁用主机验证

我们可以在运行应用程序时通过命令行参数设置这个属性:

java -Djdk.internal.httpclient.disableHostnameVerification=true -jar target/java-httpclient-ssl-0.0.1-SNAPSHOT.jar

另一种选择是 在创建客户端之前,程序性地设置这个属性

Properties props = System.getProperties();
props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());

HttpClient httpClient = HttpClient.newHttpClient();

现在,当我们运行测试时,会看到它通过了。

需要注意的是,改变这个属性意味着所有请求的证书验证都将被禁用。 这可能在生产环境中并不理想。但在非生产环境中,这是常见的做法。

5. 我们能使用Java HttpClient与Spring一起吗?

Spring提供了两个流行的HTTP请求接口:

  • RestTemplate用于同步请求
  • WebClient用于同步和异步请求

它们都可以与Apache HttpClient、OkHttp和旧的HttpURLConnection等流行HTTP客户端一起使用。然而,我们不能将Java HttpClient集成到这两个接口中它更像是它们的替代方案。

我们可以直接使用Java HttpClient进行同步和异步请求,转换请求和响应,添加超时等。因此,不需要Spring的接口即可使用。

6. 总结

在这篇文章中,我们探讨了如何使用Java HTTP Client连接需要SSL的服务器,并处理没有有效证书的URL。代码示例始终可在GitHub上找到。