HttpClient实现HTTPS客户端编程---可信证书与自签名证书

xiaoxiao2021-03-01  15

问题描述:使用HttpClient请求https连接(连接不是被信任的,自签名证书),报如下错误:

访问代码如下:

public class TestHttps12306 { public static String getHtmlStringFromHttp(String url) { String str = ""; HttpClient httpclient = new DefaultHttpClient(); HttpGet httpget = new HttpGet(url); try { HttpResponse response = httpclient.execute(httpget); HttpEntity entity = response.getEntity(); str = EntityUtils.toString(entity); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } httpget.abort(); httpclient.getConnectionManager().shutdown(); return str; } public static void main(String[] args) { String str12306 = getHtmlStringFromHttp("https://61.190.252.126:7012/SpringJMS/SendJMSMessage/sendSQLMessage.do?strJSON =1"); System.out.println(str12306); } }

原因分析:通过以上代码可以访问https://www.baidu.com和http://www.baidu.com。那么在访问百度时,SSL握手过程中百度网站的证书是如何被认为是可信证书的?很多人会不假思索的回答:“因为百度网站使用了可信CA签发的证书”。通过Chrome的开发者工具:F12->security可以看出来,百度的证书被浏览器认定为可信的。但我们要知道,浏览https网站的场景是由客户端校验服务器是否可信的(这里指一般的场景,当然还有双向认证),单从服务器如何如何并不能解释客户端的行为,真正的答案是windows系统预制了一批可信根证书,从Internet选项->内容->证书里面可以看到,如下图,有兴趣的可以找找是否有baidu的根证书: 

类比浏览器,HttpClient既然能访问百度成功,其必然也加载了可信根证书作为判断依据,那么这些根证书在哪,由谁去加载的?通过debug代码,可以发现,java程序在默认的SSLSocketFactory中加载了104个证书(不同版本jdk证书数目可能有差别): 

进一步在%JAVA_HOME%/jre/lib/security的目录下找到了疑似的keystore文件。 

keytool -list -keystore cacerts -storepass changeit看一下,里面确实是这104个根证书。 

那么服务器就是要用自签名证书,如何解决?

解决方法:

在维持整个校验过程不变的前提下,keystore中导入这个证书,将其认为可信即可。  第一步:将证书保存到本地:  Chrome浏览器F12-Security-View certificate打开证书信息窗口-详细信息-复制到文件将其保存为X.509格式。  证书窗口中我们可以看到证书链,保存链里面的任一个证书都可以。 

第二步:将证书导入keystore(导入jdk中的caserts文件或者生成一个新的keystore文件)  一般不建议随意修改jdk中的文件,咱们生成一个新的keystore,并修改代码加载它。

keytool -import -keystore my.keystore -storepass 123456 -file 12306root.cer -alias 12306root  

上文的代码也需要一些修改,给CloseableHttpClient对象设置自定义的SSLConnectionSocketFactory:

// 加载自定义的keystore SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(new File("D:/java/IdeaProjects/test/src/main/resources/certs/my.keystore"), "123456".toCharArray()).build(); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext); CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build(); HttpGet httpGet = new HttpGet("https://61.190.252.126:7012/SpringJMS/SendJMSMessage/sendSQLMessage.do?strJSON =1"); System.out.println("Executing request " + httpGet.getRequestLine()); CloseableHttpResponse response = httpClient.execute(httpGet); System.out.println("----------------------------------------"); System.out.println(response.getStatusLine()); System.out.println(EntityUtils.toString(response.getEntity()));

与问题一中的结果对比,异常变了,从未找到合法证书变为未匹配到subject alternative names(可选名称) 

为什么会有匹配不到AlternativeName的错误?

我们来看看源码中这段校验逻辑:服务器证书信息中有AlternativeName就用它和访问的地址比较,没有就用CN和访问的地址比较。 

这俩东东分别对应证书(以百度的证书为例)中这两段信息: 

而我们要访问的网址的证书没有可选名称,只有CN,且CN值为ahswj,咱们访问的是61.190.252.126:7012,自然匹配不到了。 

这有点尴尬了,ahswj的证书除了是自签名以外,证书颁给的域名还不对,怎么办呢,我们可以在构造SSLConnectionSocketFactory时重写域名校验逻辑,简单起见就直接校验通过返回true了(注意这是不得已而为之的办法,违反了证书的安全机制)

SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() { public boolean verify(String s, SSLSession sslSession) { // 我们可以重写域名校验逻辑 return true; } });

为什么我设置了ConnectionManager后又不行了呢

一般代码中,我们会给HttpClient设置连接池:

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(20); connectionManager.setDefaultMaxPerRoute(20); httpClient = HttpClients.custom() .setSSLSocketFactory(sslConnectionSocketFactory) .setConnectionManager(connectionManager) .build();

结果辛苦调通的网址又访问不了了,回到了最初的问题:unable to find valid certification path to requested target。  阅读一番源码后,直接将原因告诉大家:  如上图,HttpClient在connect()时,获取到的SSLSocketFactory,是new PoolingHttpClientConnectionManager()时默认构造的,并不是我们.setSSLSocketFactory(sslConnectionSocketFactory)设置的那一个。除非我们在new PoolingHttpClientConnectionManager就注册好传给它的构造函数。

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslConnectionSocketFactory) .build());

总结

https(SSL)证书校验常见的几点

证书是否为可信CA签发证书中的AlternativeNames或CN是否与我们访问的地址相同证书是否过期/是否已被撤销(见CRL)

最终的完整代码

package org.fst.network; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import java.io.File; public class HttpGetTest { public static void main(String[] args) { CloseableHttpClient httpClient = null; try { // 加载自定义的keystore SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(new File("D:/java/IdeaProjects/test/src/main/resources/certs/my.keystore"), "123456".toCharArray()).build(); // 默认的域名校验类为DefaultHostnameVerifier,比对服务器证书的AlternativeName和CN两个属性。 // 如果服务器证书这两者不合法而我们又必须让其校验通过,则可以自己实现HostnameVerifier。 SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new HostnameVerifier() { public boolean verify(String s, SSLSession sslSession) { // 我们可以重写域名校验逻辑 return true; } }); // 一个httpClient对象对于https仅会选用一个SSLConnectionSocketFactory // 至少在4.5.3和4.5.4中,如果给HttpClient对象设置ConnectionManager,我们必须在PoolingHttpClientConnectionManager的构造方法中传入Registry, // 并将https对应的工厂设置为我们自己的SSLConnectionSocketFactory对象,因为在DefaultHttpClientConnectionOperator.connect()中,逻辑是从这里找SSLConnectionSocketFactory的。 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslConnectionSocketFactory) .build()); connectionManager.setMaxTotal(20); connectionManager.setDefaultMaxPerRoute(20); httpClient = HttpClients.custom() // 不在connectionManager中注册,仅在这里设置SSLConnectionSocketFactory是无效的,详见build()内部逻辑,在connectionManager不为null时,不会使用里的SSLConnectionSocketFactory .setSSLSocketFactory(sslConnectionSocketFactory) .setConnectionManager(connectionManager) .build(); HttpGet httpGet = new HttpGet("https://www.12306.cn"); System.out.println("Executing request " + httpGet.getRequestLine()); CloseableHttpResponse response = httpClient.execute(httpGet); System.out.println("----------------------------------------"); System.out.println(response.getStatusLine()); System.out.println(EntityUtils.toString(response.getEntity())); } catch (Exception e) { e.printStackTrace(); } finally { try { if (null != httpClient) { httpClient.close(); } } catch (Exception e) { e.printStackTrace(); } } } }

一个朋友新做的公众号,帮忙宣传一下,会不定时推送一些开发中碰到的问题的解决方法,以及会分享一些开发视频。资料等。请大家关注一下谢谢:

转载请注明原文地址: https://www.6miu.com/read-4045608.html

最新回复(0)