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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013310119/article/details/81906571

问题描述:使用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%20=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%20=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();
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/u013310119/article/details/81906571