问题描述:使用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就用它和访问的地址比较,没有就用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; } });一般代码中,我们会给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());一个朋友新做的公众号,帮忙宣传一下,会不定时推送一些开发中碰到的问题的解决方法,以及会分享一些开发视频。资料等。请大家关注一下谢谢: