Apache HttpClient 4.5.x 忽略证书验证

在使用 Apache HttpComponents 时,需要访问一个第三方接口进行数据传输,但是由于对方使用的是 Let’s Encrypt 的证书,虽然浏览器是信任的,但是在调试的时候发现程序并不信任,所以采用自定义 SSLContext 的方式解决该问题。

查找官方解决方案

作为一只程序猿,翻官方文档肯定是首选方案。官方提供了一种 Custom SSL context 的实现。

http://hc.apache.org/httpcomponents-client-4.5.x/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.http.examples.client;

import java.io.File;

import javax.net.ssl.SSLContext;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

/**
* This example demonstrates how to create secure connections with a custom SSL
* context.
*/
public class ClientCustomSSL {

public final static void main(String[] args) throws Exception {
// Trust own CA and all self-signed certs
SSLContext sslcontext = SSLContexts.custom()
.loadTrustMaterial(new File("my.keystore"), "nopassword".toCharArray(),
new TrustSelfSignedStrategy())
.build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
new String[] { "TLSv1" },
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.build();
try {

HttpGet httpget = new HttpGet("https://httpbin.org/");

System.out.println("Executing request " + httpget.getRequestLine());

CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();

System.out.println("----------------------------------------");
System.out.println(response.getStatusLine());
EntityUtils.consume(entity);
} finally {
response.close();
}
} finally {
httpclient.close();
}
}

}

由于考虑到该接口为第三方接口,对方可能进行证书的更换,再加上由于所传输信息是可以公开的、非重要数据的数据,所以并不采用官方给出的导入证书而使用不进行检查直接信任的方式处理。

定义自己的 SSLContext

所以问题的关键在于自定义 SSLContext,查看其 loadTrustMaterial 方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public SSLContextBuilder loadTrustMaterial(
final KeyStore truststore,
final TrustStrategy trustStrategy) throws NoSuchAlgorithmException, KeyStoreException {
final TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(truststore);
final TrustManager[] tms = tmfactory.getTrustManagers();
if (tms != null) {
if (trustStrategy != null) {
for (int i = 0; i < tms.length; i++) {
final TrustManager tm = tms[i];
if (tm instanceof X509TrustManager) {
tms[i] = new TrustManagerDelegate(
(X509TrustManager) tm, trustStrategy);
}
}
}
for (final TrustManager tm : tms) {
this.trustmanagers.add(tm);
}
}
return this;
}

可见这边是使用实现了 TrustStrategy 接口的 TrustSelfSignedStrategy 对 X509 证书进行管理,所以我们需要自定义一个 TrustStrategy 的实例。

先查看 TrustStrategy 接口的源代码,发现除了继承了 org.apache.http.ssl.TrustStrategy 之外没有添加任何内容。由于该类在 http-core 包中,直接反编译查看得:

1
2
3
public interface TrustStrategy {
boolean isTrusted(X509Certificate[] var1, String var2) throws CertificateException;
}

所以这里需要重写 isTrusted 方法,不进行验证,直接返回 true。由于不需要导入 Keystore,所以直接给空值,代码如下。

1
2
3
4
5
6
7
8
SSLContext sslcontext = SSLContexts.custom()
.loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
})
.build();

测试

修改后的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
* Created by timer on 2017/4/23.
*/
public class TestClientCustomSSL {

public final static void main(String[] args) {

// 自定义 SSLContext 使信任所有证书。
SSLContext sslcontext = null;
try {
sslcontext = SSLContexts.custom()
.loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
})
.build();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
}

// 仅允许 TLSv1 协议
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
new String[] { "TLSv1" },
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());

try(CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build()) {

HttpGet httpget = new HttpGet("https://httpbin.org/get");

try(CloseableHttpResponse response = httpclient.execute(httpget)) {
HttpEntity entity = response.getEntity();
System.out.println(EntityUtils.toString(entity));
EntityUtils.consume(entity);
} catch (IOException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

成功访问,输出如下:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept-Encoding": "gzip,deflate",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "Apache-HttpClient/4.5.3 (Java/1.8.0_121)"
},
"origin": "60.219.211.8",
"url": "https://httpbin.org/get"
}

注:这里访问的并不是一个不受信任的站点,但是实际测试不受信任的站点也可以正常访问。