基于HttpClient 4.3的可访问自签名HTTPS站点的新版工具类

本文出处:http://blog.csdn.net/chaijunkun/article/details/40145685,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文。

 

HttpClient在当今Java应用中的位置越来越重要。从该项目的变迁过程我们不难发现,其已经从apache-commons众多的子项目中剥离,一跃成为如今的顶级项目,可见它的分量。然而随着项目的升级和架构的调整,很多以前常用的类和方法都已被打上了@Deprecated注解,作为一个有代码洁癖的程序猿,我们也有必要升级一下工具类,让代码更加整洁。

另外在项目中正好需要访问https协议的接口,而对应的服务器没有购买商业CA颁发的正式受信证书,只是做了个自签名(联想一下12306网站购票时提示的那个警告信息),默认情况下通过HttpClient访问会抛出异常,在本文中也给出了解决办法。

 

在HttpClient 4.x版本中引入了大量的构造器设计模式,很多的配置都不建议直接new出来,而且相关的API也有所改动,例如连接参数,以前是直接new出HttpConnectionParams对象后通过set方法逐一设置属性,现在有了构造器,可以通过如下方式进行构造:

ConnectionConfig.custom().setCharset(Charsets.toCharset(defaultEncoding)).build();

 

SocketConfig.custom().setSoTimeout(100000).build();

 

基本情况就是这样,接下来谈下如何使用新的HttpClient来访问自签名https接口。

 

参阅CSDN博主noodies代码,发现实现访问自签名https的要点就是建立一个自定义的SSLContext对象,该对象要有可以存储信任密钥的容器,还要有判断当前连接是否受信任的策略,以及在SSL连接工厂中取消对所有主机名的验证。他的代码将会在本文最后贴出来,以下代码均针对新HttpClient。

 

首先建立一个信任任何密钥的策略。代码很简单,不去考虑证书链和授权类型,均认为是受信任的:

class AnyTrustStrategy implements TrustStrategy{
	
	@Override
	public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
		return true;
	}
	
}


HttpClient既能处理常规http协议,又能支持https,根源在于在连接管理器中注册了不同的连接创建工厂。当访问url的schema为http时,调用明文连接套节工厂来建立连接;当访问url的schema为https时,调用SSL连接套接字工厂来建立连接。对于http的连接我们不做修改,只针对使用SSL的https连接来进行自定义:

RegistryBuilder registryBuilder = RegistryBuilder.create();
ConnectionSocketFactory plainSF = new PlainConnectionSocketFactory();
registryBuilder.register("http", plainSF);
//指定信任密钥存储对象和连接套接字工厂
try {
	KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
	SSLContext sslContext = SSLContexts.custom().useTLS().loadTrustMaterial(trustStore, new AnyTrustStrategy()).build();
	LayeredConnectionSocketFactory sslSF = new SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
	registryBuilder.register("https", sslSF);
} catch (KeyStoreException e) {
	throw new RuntimeException(e);
} catch (KeyManagementException e) {
	throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
	throw new RuntimeException(e);
}
Registry registry = registryBuilder.build();

在上述代码中可以看到,首先建立了一个密钥存储容器,随后让SSLContext开启TLS,并将密钥存储容器和信任任何主机的策略加载到该上下文中。构造SSL连接工厂时,将自定义的上下文和允许任何主机名通过校验的指令一并传入。最后将这样一个自定义的SSL连接工厂注册到https协议上。

 

前期准备已经完成,接下来我们要获得HttpClient对象:

//设置连接管理器
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(registry);
connManager.setDefaultConnectionConfig(connConfig);
connManager.setDefaultSocketConfig(socketConfig);
//构建客户端
HttpClient client= HttpClientBuilder.create().setConnectionManager(connManager).build();

为了让我们的HttpClient具有多线程处理的能力,连接管理器选用了PoolingHttpClientConnectionManager,将协议注册信息传入连接管理器,最后再次利用构造器的模式创建出我们需要的HttpClient。随后的GET/POST请求发起方法http和https之间没有差异。

2019年4月24日补充:今天突然想起来之前对这个组件进行过优化,原因是:当时用该组件进行高并发访问时,经常会造成访问超时的问题,后来经过研究,发现是没有对线程池的数量进行调优。请在上述代码设置connManager之后增加如下代码:

//以下两个参数对并发影响很大,根据业务场景配置
connManager.setMaxTotal(500);
connManager.setDefaultMaxPerRoute(50);

设置完这两个参数之后再尝试创建client,执行后续的逻辑。特此补充。

2019年10月31日补充:最近Review代码的时候发现了一个问题,当你的项目足够大,QPS非常高的情况下,各个业务共享同一个HttpClient实例,会造成拥堵。主要是因为HttpClient需要有一个线程池大小,当业务A的QPS很高时,可能将HttpClient的线程池占满,此时B业务就会处于等待状态。如果你发现你的业务间莫名其妙的互相影响,可以尝试:

1.控制外界访问流量

2.或者适量增加实例

3.将线程池大小做成可配置。随业务动态调整。

为了验证我们的代码是否成功,可以做下JUnit单元测试:

@Test
public void doTest() throws ClientProtocolException, URISyntaxException, IOException{
	HttpUtil util = HttpUtil.getInstance();
	InputStream in = util.doGet("https://kyfw.12306.cn/otn/leftTicket/init");
	String retVal = HttpUtil.readStream(in, HttpUtil.defaultEncoding);
	System.out.println(retVal);
}

 

执行后可以在控制台看到12306余票查询界面的html代码

 

*2017年2月15日补充:有朋友反馈说提供的工具类中没有直接POST JSON对象的方法,下面我提供一下基础方法,供参考(此代码未包含在下文的共享资源中,请自行补充进去)

 

/**
* 基本Post请求
* @param url 请求url
* @param queryParams 请求头的查询参数
* @param json 直接放入post请求体中的文本(请使用JSON)
* @return
* @throws URISyntaxException 
* @throws UnsupportedEncodingException 
*/
public HttpResponse doPostBasic(String url, Map queryParams, String json) throws URISyntaxException, ClientProtocolException, IOException{
	HttpPost pm = new HttpPost();
	URIBuilder builder = new URIBuilder(url);
	//填入查询参数
	if (MapUtils.isNotEmpty(queryParams)){
		builder.setParameters(HttpUtil.paramsConverter(queryParams));
	}
	pm.setURI(builder.build());
	//填入post json数据
	if (StringUtils.isNotBlank(json)){
		//下面的ContentType完整类名为:org.apache.http.entity.ContentType
		pm.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
	}
	return client.execute(pm);
}


上述只是一个简单的功能实现,更好的形式是将json参数换成Object data,然后在方法内部调用Object转JSON字符串的组件方法进行转换。关于JSON组件的使用,请参阅我的另一篇博文:http://blog.csdn.net/chaijunkun/article/details/8257209

 

 

为了方便大家使用,本人将封装好的代码上传到了CSDN资源共享中,欢迎下载。

下载地址:http://download.csdn.net/detail/chaijunkun/8046331

最后感谢CSDN博主noodies的代码提供了思路:http://blog.csdn.net/noodies/article/details/17240805

本文中的部分代码参阅了HttpClient 4.3.5官方文档的2.7节关于Connection socket factories的内容

你可能感兴趣的:(研究成果,Java笔记)