允许 Java JDK 11 HttpClient 的不安全 HTTPS 连接

Allow insecure HTTPS connection for Java JDK 11 HttpClient

有时需要允许不安全的 HTTPS 连接,例如在一些应该适用于任何站点的网络爬虫应用程序中。我在 JDK 中使用了 one such solution with old HttpsURLConnection API which was recently superseded by the new HttpClient API 11. 使用这个新 API 允许不安全的 HTTPS 连接(自签名或过期证书)的方法是什么?

UPD:我试过的代码(在 Kotlin 中但直接映射到 Java):

    val trustAllCerts = arrayOf<TrustManager>(object: X509TrustManager {
        override fun getAcceptedIssuers(): Array<X509Certificate>? = null
        override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
        override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
    })

    val sslContext = SSLContext.getInstance("SSL")
    sslContext.init(null, trustAllCerts, SecureRandom())

    val sslParams = SSLParameters()
    // This should prevent host validation
    sslParams.endpointIdentificationAlgorithm = ""

    httpClient = HttpClient.newBuilder()
        .sslContext(sslContext)
        .sslParameters(sslParams)
        .build()

但是在发送时出现异常(尝试使用自签名证书在本地主机上):

java.io.IOException: No name matching localhost found

使用 IP 地址而不是本地主机给出 "No subject alternative names present" 异常。

在对 JDK 进行一些调试后,我发现 sslParams 在抛出异常的地方确实被忽略了,并且使用了一些本地创建的实例。进一步的调试显示影响主机名验证算法的唯一方法是将 jdk.internal.httpclient.disableHostnameVerification system 属性 设置为 true。这似乎是一个解决方案。上面代码中的SSLParameters没有任何作用所以这部分可以舍弃。使其仅在全局范围内可配置看起来像是新 HttpClient API.

中的严重设计缺陷

使用 Java 11,您也可以使用 HttpClient 构建的 as mentioned in the selected answer in the link shared 做类似的工作:

HttpClient httpClient = HttpClient.newBuilder()
        .connectTimeout(Duration.ofMillis(<timeoutInSeconds> * 1000))
        .sslContext(sc) // SSL context 'sc' initialised as earlier
        .sslParameters(parameters) // ssl parameters if overriden
        .build();

带有样品请求

HttpRequest requestBuilder = HttpRequest.newBuilder()
            .uri(URI.create("https://www.example.com/getSomething"))
            .GET()
            .build();

可以执行为:

httpClient.send(requestBuilder, HttpResponse.BodyHandlers.ofString()); // sends the request

根据评论更新,禁用主机名验证,目前可以使用系统属性:

-Djdk.internal.httpclient.disableHostnameVerification

可以是 如下:-

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

正如已经建议的那样,您需要一个忽略错误证书的 SSLContext。在问题的一个链接中获取 SSLContext 的确切代码应该通过基本上创建一个不查看证书的空 TrustManager 来工作:

private static TrustManager[] trustAllCerts = new TrustManager[]{
    new X509TrustManager() {
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }
        public void checkClientTrusted(
            java.security.cert.X509Certificate[] certs, String authType) {
        }
        public void checkServerTrusted(
            java.security.cert.X509Certificate[] certs, String authType) {
        }
    }
};

public static  void main (String[] args) throws Exception {
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, trustAllCerts, new SecureRandom());

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

上述问题显然是所有站点都完全禁用了服务器身份验证。如果只有一个坏证书,那么您可以将其导入密钥库:

keytool -importcert -keystore keystorename -storepass pass -alias cert -file certfile

然后使用读取密钥库的 InputStream 初始化 SSLContext,如下所示:

char[] passphrase = ..
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(i, passphrase); // i is an InputStream reading the keystore

KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, passphrase);

TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ks);

sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

上述任一解决方案都适用于自签名证书。第三种选择是在服务器提供有效的非自签名证书但主机不匹配它提供的证书中的任何名称的情况下,然后系统 属性 "jdk.internal.httpclient.disableHostnameVerification"可以设置为 "true",这将强制证书以与之前使用 HostnameVerifier API 相同的方式被接受。请注意,在正常部署中,预计不会使用任何这些机制,因为应该可以自动验证任何正确配置的 HTTPS 服务器提供的证书。

提供不进行证书验证的 X509TrustManager(如上面的答案)不足以禁用主机名验证,因为 SSLContext 的实现使用 X509ExtendedTrustManager“包装”提供的 X509TrustManager(如果它不是 X509ExtendedTrustManager)。包装“扩展”X509ExtendedTrustManager 执行主机名验证。

因此,避免主机名验证的一种方法是提供不执行主机名验证的 X509ExtendedTrustManager。这可以按如下方式完成:

SSLContext context = SSLContext.getInstance("TLS");
context.init(
    null,
    new TrustManager[]
    {
        new X509ExtendedTrustManager()
        {
            public X509Certificate[] getAcceptedIssuers()
            {
                return null;
            }

            public void checkClientTrusted(
                final X509Certificate[] a_certificates,
                final String a_auth_type)
            {
            }

            public void checkServerTrusted(
                final X509Certificate[] a_certificates,
                final String a_auth_type)
            {
            }
            public void checkClientTrusted(
                final X509Certificate[] a_certificates,
                final String a_auth_type,
                final Socket a_socket)
            {
            }
            public void checkServerTrusted(
                final X509Certificate[] a_certificates,
                final String a_auth_type,
                final Socket a_socket)
            {
            }
            public void checkClientTrusted(
                final X509Certificate[] a_certificates,
                final String a_auth_type,
                final SSLEngine a_engine)
            {
            }
            public void checkServerTrusted(
                final X509Certificate[] a_certificates,
                final String a_auth_type,
                final SSLEngine a_engine)
            {
            }
        }
    },
    null);

以通常的方式将此 SSLContext 提供给 HTTPClient.Builder(如前面的答案所示)。

此方法的好处是在每个 HttpClient 基础上应用(即不是 JVM 范围)。