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

时间:2018-10-25 11:54:15

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

有时需要允许不安全的HTTPS连接,例如在某些适用于任何网站的网络抓取应用程序中。我将one such solution与旧的HttpsURLConnection 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()

但是在发送时,我有一个异常(使用自签名证书在localhost上尝试):

java.io.IOException: No name matching localhost found

使用IP地址而不是localhost会出现“不存在使用者替代名称”的情况。

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

2 个答案:

答案 0 :(得分:5)

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

答案 1 :(得分:4)

使用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

可以是set programmatically,如下所示:-

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