在使用 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 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;public class ClientCustomSSL { public final static void main (String[] args) throws Exception { SSLContext sslcontext = SSLContexts.custom() .loadTrustMaterial(new File ("my.keystore" ), "nopassword" .toCharArray(), new TrustSelfSignedStrategy ()) .build(); 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;public class TestClientCustomSSL { public final static void main (String[] args) { 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(); } 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" }
注:这里访问的并不是一个不受信任的站点,但是实际测试不受信任的站点也可以正常访问。