Https请求
一、访问HTTPS站点
两种方法来模拟发送HTTP请求,访问HTTP站点。一种方式是通过java.net自带的HttpURLConnection,另一种方式是通过Apache的HttpClient,这两种方式各有各的优势。这里也使用这两种方式来访问HTTPS站点,从下面的代码可以看到,和前面访问HTTP站点几乎完全一样。
1.1使用HttpURLConnection
@Test
public void basicHttpsGet() throws
Exception {
String url = "https://www.baidu.com";
URL obj = new
URL(url);
HttpsURLConnection
con = (HttpsURLConnection) obj.openConnection();
con.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
con.setRequestProperty("Accept-Language",
"en-US,en;q=0.5");
con.setRequestMethod("GET");
String
responseBody = readResponseBody(con.getInputStream());
System.out.println(responseBody);
}
1.2使用HttpClient
@Test
public void basicHttpsGet() throws
Exception {
String url = "https://www.baidu.com";
HttpGet request =
new HttpGet(url);
request.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
CloseableHttpClient
httpclient = HttpClients.createDefault();
CloseableHttpResponse
response = httpclient.execute(request);
String
responseBody = readResponseBody(response);
System.out.println(responseBody);
}
具体的代码解释参见第一篇博客,这里不再赘述。一般情况下,访问HTTPS站点就和访问HTTP站点一样简单,无论是HttpURLConnection还是HttpClient,都将底层的实现细节封装了起来,给我们提供了一致的对外接口,所以我们不用关心HTTPS的实现原理。
二、Java里的证书
上面所介绍的是浏览器对证书进行验证的过程,浏览器保存了一个常用的CA证书列表,在验证证书链的有效性时,直接使用保存的证书里的公钥进行校验,如果在证书列表中没有找到或者找到了但是校验不通过,那么浏览器会警告用户,由用户决定是否继续。与此类似的,操作系统也一样保存有一份可信的证书列表,譬如在Windows系统下,你可以运行certmgr.msc打开证书管理器查看,这些证书实际上是存储在Windows的注册表中,一般情况下位于:\SOFTWARE\Microsoft\SystemCertificates\路径下。那么在Java程序中是如何验证证书的呢?
和浏览器操作系统类似,Java在JRE的安装目录下也保存了一份默认可信的证书列表,这个列表一般是保存在$JRE/lib/security/cacerts文件中。要查看这个文件,可以使用类似KeyStore Explorer这样的软件,当然也可以使用JRE自带的keytool工具(后面再介绍),cacerts文件的默认密码为changeit(但是我保证,大多数人都不会change it)。
我们知道,证书有很多种不同的存储格式,譬如CA在发布证书时,常常使用PEM格式,这种格式的好处是纯文本,内容是BASE64编码的,证书中使用"-----BEGIN CERTIFICATE-----"和"-----END CERTIFICATE-----"来标识。另外还有比较常用的二进制DER格式,在Windows平台上较常使用的PKCS#12格式等等。当然,不同格式的证书之间是可以相互转换的,我们可以使用openssl这个命令行工具来转换,参考SSL
Converter,另外,想了解更多证书格式的,可以参考这里:Various
SSL/TLS Certificate File Types/Extensions。
在Java平台下,证书常常被存储在KeyStore文件中,上面说的cacerts文件就是一个KeyStore文件,KeyStore不仅可以存储数字证书,还可以存储密钥,存储在KeyStore文件中的对象有三种类型:Certificate、PrivateKey和SecretKey。Certificate就是证书,PrivateKey是非对称加密中的私钥,SecretKey用于对称加密,是对称加密中的密钥。KeyStore文件根据用途,也有很多种不同的格式:JKS、JCEKS、PKCS12、DKS等等,PixelsTech上有一系列文章对KeyStore有深入的介绍,可以学习下:Different
types of keystore in Java。
到目前为止,我们所说的KeyStore其实只是一种文件格式而已,实际上在Java的世界里KeyStore文件分成两种:KeyStore和TrustStore,这是两个比较容易混淆的概念,不过这两个东西从文件格式来看其实是一样的。KeyStore保存私钥,用来加解密或者为别人做签名;TrustStore保存一些可信任的证书,访问HTTPS时对被访问者进行认证,以确保它是可信任的。所以准确来说,上面的cacerts文件应该叫做TrustStore而不是KeyStore,只是它的文件格式是KeyStore文件格式罢了。
除了KeyStore和TrustStore,Java里还有两个类KeyManager和TrustManager与此息息相关。JSSE的参考手册中有一张示意图,说明了各个类之间的关系:
可以看出如果要进行SSL会话,必须得新建一个SSLSocket对象,而SSLSocket对象是通过SSLSocketFactory来管理的,SSLSocketFactory对象则依赖于SSLContext,SSLContext对象又依赖于keyManager、TrustManager和SecureRandom。我们这里最关心的是TrustManager对象,另外两个暂且忽略,因为正是TrustManager负责证书的校验,对网站进行认证,要想在访问HTTPS时通过认证,不报sun.security.validator.ValidatorException异常,必须从这里开刀。
三、Java客户端访问https时证书验证处理规则
客户端的TrustStore文件中保存着被客户端所信任的服务器的证书信息。客户端在进行SSL连接时,JSSE将根据这个文件中的证书决定是否信任服务器端的证书。在SunJSSE中,有一个信任管理器类负责决定是否信任远端的证书,这个类有如下的处理规则:
1)
若系统属性javax.net.sll.trustStore指定了TrustStore文件,那么信任管理器就去jre安装路径下的lib/security/目录中寻找并使用这个文件来检查证书。
2)
若该系统属性没有指定TrustStore文件,它就会去jre安装路径下寻找默认的TrustStore文件,这个文件的相对路径为:lib/security/jssecacerts。
3)
若jssecacerts不存在,但是cacerts存在(它随J2SDK一起发行,含有数量有限的可信任的基本证书),那么这个默认的TrustStore文件就是lib/security/cacerts。
四、自定义TrustManager绕过证书检查进行https访问
我们知道了TrustManager是专门负责校验证书的,那么最容易想到的方法应该就是改写TrustManager类,让它不要对证书做校验,这种方法虽然粗暴,但是却相当有效,而且Java中的TrustManager也确实可以被重写,下面是示例代码:
@Test
public void
basicHttpsGetIgnoreCertificateValidation() throws Exception {
String url = "https://kyfw.12306.cn/otn/";
// Create a trust
manager that does not validate certificate chains
TrustManager[]
trustAllCerts = new TrustManager[] {
new
X509TrustManager() {
public
X509Certificate[] getAcceptedIssuers() {
return
null;
}
public
void checkClientTrusted(X509Certificate[] certs, String authType) {
//
don't check
}
public
void checkServerTrusted(X509Certificate[] certs, String authType) {
//
don't check
}
}
};
SSLContext ctx =
SSLContext.getInstance("TLS");
ctx.init(null,
trustAllCerts, null);
LayeredConnectionSocketFactory
sslSocketFactory = new SSLConnectionSocketFactory(ctx);
CloseableHttpClient
httpclient = HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
.build();
HttpGet request =
new HttpGet(url);
request.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
CloseableHttpResponse
response = httpclient.execute(request);
String responseBody
= readResponseBody(response);
System.out.println(responseBody);
}
我们新建了一个匿名类,继承自X509TrustManager接口,这个接口提供了三个方法用于验证证书的有效性:getAcceptedIssuers、checkClientTrusted、checkServerTrusted,我们在验证的函数中直接返回,不做任何校验,这样在访问HTTPS站点时,就算是证书不可信,也不会抛出异常,可以继续执行下去。
这种方法虽然简单,但是却有一个最严重的问题,就是不安全。因为不对证书做任何合法性校验,而且这种处理是全局性的,不管青红皂白,所有的证书都不会做验证,所以就算遇到不信任的证书,代码依然会继续与之通信,至于通信的数据安全不安全就不能保证了。所以如果你只是想在测试环境做个实验,那没问题,但是如果你要将代码发布到生产环境,请慎重。
五、使用证书进行https访问
对于有些证书,我们基本上确定是可以信任的,但是这些证书又不在Java的cacerts文件中,譬如12306网站,或者使用了Let's Encrypt证书的一些网站,对于这些网站,我们可以将其添加到信任列表中,而不是使用上面的方法统统都相信,这样程序的安全性仍然可以得到保障。
5.1使用keytool导入证书
简单的做法是将这些网站的证书导入到cacerts文件中,这样Java程序在校验证书的时候就可以从cacerts文件中找到并成功校验这个证书了。上面我们介绍过JRE自带的keytool这个工具,这个工具小巧而强悍,拥有很多功能。首先我们可以使用它查看KeyStore文件,使用下面的命令可以列出KeyStore文件中的所有内容(包括证书、私钥等):
$ keytool -list -keystore cacerts
然后通过下面的命令,将证书导入到cacerts文件中:
$ keytool -import -alias 12306 -keystore cacerts
-file 12306.cer
要想将网站的证书导入cacerts文件中,首先要获取网站的证书,譬如上面命令中的12306.cer文件,它是使用浏览器的证书导出向导保存的。如下图所示:
关于keytool的更多用法,可以参考keytool的官网手册,SSLShopper上也有一篇文章列出了常用的keytool命令。
5.2使用KeyStore动态加载证书
使用keytool导入证书,这种方法不仅简单,而且保证了代码的安全性,最关键的是代码不用做任何修改。所以我比较推荐这种方法。但是这种方法有一个致命的缺陷,那就是你需要修改JRE目录下的文件,如果你的程序只是在自己的电脑上运行,那倒没什么,可如果你的程序要部署在其他人的电脑上或者公司的服务器上,而你没有权限修改JRE目录下的文件,这该怎么办?如果你的程序是一个分布式的程序要部署在成百上千台机器上,难道还得修改每台机器的JRE文件吗?好在我们还有另一种通过编程的手段来实现的思路,在代码中动态的加载KeyStore文件来完成证书的校验,抱着知其然知其所以然的态度,我们在最后也实践下这种方法。通过编写代码可以更深刻的了解KeyStore、TrustManagerFactory、SSLContext以及SSLSocketFactory这几个类之间的关系。
@Test
public void
basicHttpsGetUsingSslSocketFactory() throws Exception {
String
keyStoreFile = "D:\\code\\ttt.ks";
String password =
"poiuyt";
KeyStore ks =
KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream
in = new FileInputStream(keyStoreFile);
ks.load(in,
password.toCharArray());
System.out.println(KeyStore.getDefaultType().toString());
System.out.println(TrustManagerFactory.getDefaultAlgorithm().toString());
TrustManagerFactory
tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx =
SSLContext.getInstance("TLS");
ctx.init(null,
tmf.getTrustManagers(), null);
LayeredConnectionSocketFactory
sslSocketFactory = new SSLConnectionSocketFactory(ctx);
String url = "https://ttt.aneasystone.com";
/**
* Return
the page with content:
* 401
Authorization Required
*/
CloseableHttpClient
httpclient = HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
.build();
HttpGet request =
new HttpGet(url);
request.setHeader("User-Agent",
"Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
CloseableHttpResponse
response = httpclient.execute(request);
String
responseBody = readResponseBody(response);
System.out.println(responseBody);
}
上面的代码使用了HttpClient,如果是使用HttpsURLConnection只需要改动下面两行即可:
HttpsURLConnection con =
(HttpsURLConnection) obj.openConnection();
con.setSSLSocketFactory(ctx.getSocketFactory());
最后的最后,我们还可以通过下面的属性来指定trustStore,这样也不需要编写像上面那样大量繁琐的代码,另外,参考我前面的博客,这些属性还可以通过JVM的参数来设置。
System.setProperty("javax.net.ssl.trustStore",
"D:\\code\\ttt.ks");
System.setProperty("javax.net.ssl.trustStorePassword",
"poiuyt");
若没有通过设置JVM参数来指定要加载的证书库文件,则使用jdk默认的jre\\lib\\cacerts证书库文件来验证请求站点的证书是否合法。
5.3 Java请求https服务(真伪查询实例)
publicstaticJSONObject SetSystsHttpsSSLPost(Stringurl, Stringargs, Stringappkey, Stringiv)throwsException {
JSONObjectresult=newJSONObject();
Stringargs= SSLPostTest.encryption(querydata);
Stringiv=newBase64().encodeToString(IV.getBytes());
StringkeyStoreFile="C:\\Users\\Gufung\\nubiacsm.keystore";
/***方式一:使用keystore加载证书库文件****/
/*String keyStoreFile = System.getProperty("java.home")+ "\\lib\\security\\cacerts";
Stringpassword = "changeit";
KeyStoreks= KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStreamin = new FileInputStream(keyStoreFile);
ks.load(in,password.toCharArray());
TrustManagerFactorytmf= TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContextctx= SSLContext.getInstance("TLS");
ctx.init(null,tmf.getTrustManagers(), null);
URL myURL = new URL(url);
HttpsURLConnectioncon= (HttpsURLConnection) myURL.openConnection();
con.setSSLSocketFactory(ctx.getSocketFactory());*/
/***方式一:使用keystore加载证书库文件****/
/***方式二:通过设置JVM参数来指定要加载的证书库文件****/
System.setProperty("javax.net.ssl.trustStore",keyStoreFile);
System.setProperty("javax.net.ssl.trustStorePassword","changeit");
URLmyURL=newURL(url);
HttpsURLConnectioncon= (HttpsURLConnection)myURL.openConnection();
/***方式二:通过设置JVM参数来指定要加载的证书库文件****/
con.setDoOutput(true);
con.setDoInput(true);
con.setRequestMethod("POST");
con.setUseCaches(false);
con.connect();
DataOutputStreamout=newDataOutputStream(con.getOutputStream());
Stringcontent="args="+ URLEncoder.encode(args,"UTF-8");
content+="&appkey="+ URLEncoder.encode(appkey,"UTF-8");
content+="&iv="+ URLEncoder.encode(iv,"UTF-8");
out.writeBytes(content);
out.flush();
out.close();
intresultCode=con.getResponseCode();
System.out.println(resultCode);
if(HttpURLConnection.HTTP_OK==resultCode) {
StringreadLine=newString();
BufferedReaderresponseReader=newBufferedReader(
newInputStreamReader(con.getInputStream(),"UTF-8"));
while((readLine=responseReader.readLine()) !=null) {
result=newJSONObject(readLine);
}
responseReader.close();
}
con.disconnect();
System.out.println(result.toString());
returnresult;
}
参考
Different types of keystore in Java -- Overview
Different types of keystore in Java -- JKS
Java中用HttpsURLConnection访问Https链接的问题
Where is the certificate folder in Windows 7?
Difference between trustStore and keyStore in Java - SSL
Java Secure Socket Extension (JSSE) Reference Guide
Disable Certificate Validation in Java SSL Connections
javax.net.ssl.SSLHandshakeException:
sun.security.validator.ValidatorException: PKIX path building failed
How to solve javax.net.ssl.SSLHandshakeException?
The Most Common Java Keytool Keystore Commands
keytool - Key and Certificate Management Tool
原文链接:http://www.aneasystone.com/archives/2016/04/java-and-https.html
参考资料:Java安全通信:HTTPS与SSL
http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html
http://www.aneasystone.com/archives/2016/04/java-and-https.html
http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html
https://my.oschina.net/zhlmmc/blog/42111
https://www.sslshopper.com/article-most-common-java-keytool-keystore-commands.html
http://blog.csdn.net/csdnbenbenchong/article/details/7388260
http://blog.csdn.net/u011042133/article/details/51671801
http://snowolf.iteye.com/blog/397693
http://www.iamlbk.com/blog/20160731/tomcat-https/?utm_source=tuicool&utm_medium=referral
http://blog.chenxiaosheng.com/posts/2013-12-26/java-use-self_signed_certificate.html
http://www.zhixing123.cn/jsp/49937.html
http://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html
https://publib.boulder.ibm.com/tividd/td/TRM/SC23-4822-00/zh_CN/HTML/user276.htm
http://op.baidu.com/2015/04/https-s01a01/
http://lukejin.iteye.com/blog/605634
http://ln-ydc.iteye.com/blog/1335213
https://blog.cnbluebox.com/blog/2014/03/24/shu-zi-zheng-shu/
http://blog.csdn.net/sfdev/article/details/2957240
https://segmentfault.com/a/1190000002554673#articleHeader0
http://www.cnblogs.com/devinzhang/archive/2012/02/28/2371631.html