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