【问题标题】:Allow insecure HTTPS connection for Java JDK 11 HttpClient允许 Java JDK 11 HttpClient 的不安全 HTTPS 连接
【发布时间】:2018-10-25 11:54:14
【问题描述】:

有时需要允许不安全的 HTTPS 连接,例如在一些应该适用于任何网站的网络爬虫应用程序中。我将 one such solution 与旧的 HttpsURLConnection API 一起使用,该 API 最近被 JDK 11 中的 new HttpClient API 取代。使用这个新 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 地址而不是 localhost 会出现“不存在主题替代名称”异常。

经过JDK的一些调试,我发现sslParams在抛出异常的地方确实被忽略了,并且使用了一些本地创建的实例。进一步调试显示,影响主机名验证算法的唯一方法是将jdk.internal.httpclient.disableHostnameVerification 系统属性设置为 true。这似乎是一个解决方案。上面代码中的SSLParameters 无效,因此可以丢弃这部分。使其仅在全局范围内可配置看起来像是新 HttpClient API 中的严重设计缺陷。

【问题讨论】:

  • 你需要实例化一个SSLContext,它会忽略坏证书,然后用它来建立连接。

标签: java java-11 java-http-client


【解决方案1】:

正如已经建议的那样,您需要一个忽略不良证书的 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 服务器提供的证书。

【讨论】:

    【解决方案2】:

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

    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
    

    从cmets更新,禁用主机名验证,目前可以使用系统属性:

    -Djdk.internal.httpclient.disableHostnameVerification
    

    可以是set programmatically,如下:-

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

    【讨论】:

    • 我找到了解决方案并更新了问题。你的回答大部分是有效的,所以我接受了。但还要注意jdk.internal.httpclient.disableHostnameVerification 系统属性必须设置以摆脱主机名验证异常,遗憾的是 SSLParameters 不会影响此行为,这要么是错误,要么是一些奇怪的设计决定。
    • 在实例化httpclient之前以编程方式禁用主机名验证// PREVENTS HOST VALIDATIONfinal Properties props = System.getProperties();props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());
    • jdk.internal.httpclient.disableHostnameVerification 是系统范围的属性 - 如果您在同一个 JVM 下有 两个 客户端:一个需要禁用该客户端,一个需要启用该客户端...跨度>
    【解决方案3】:

    提供不进行证书验证的 X509TrustManager(如上面的答案)不足以禁用主机名验证,因为 SSLContext 的实现“包装”提供的 X509TrustManager 和 X509ExtendedTrustManager(如果它不是 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 范围的)。

    【讨论】:

    • 这对我有用,谢谢!
    猜你喜欢
    • 2018-01-14
    • 1970-01-01
    • 2020-02-12
    • 2017-02-12
    • 1970-01-01
    • 1970-01-01
    • 2019-12-03
    • 2017-11-27
    • 1970-01-01
    相关资源
    最近更新 更多