一、概述
其實這篇文章理論上不限於okhttp去訪問自籤名的網站,不過接上篇博文了,就叫這個了。首先要了解的事,okhttp默認情況下是支持https協議的網站的,比如https://www.baidu.com,https://github.com/hongyangAndroid/okhttp-utils等,你可以直接通過okhttp請求試試。不過要注意的是,支持的https的網站基本都是CA機構頒發的證書,默認情況下是可以信任的。
當然我們今天要說的是自籤名的網站,什麼叫自籤名呢?就是自己通過keytool去生成一個證書,然後使用,並不是CA機構去頒發的。使用自籤名證書的網站,大家在使用瀏覽器訪問的時候,一般都是報風險警告,好在有個大名鼎鼎的網站就是這麼幹的,https://kyfw.12306.cn/otn/,點擊進入12306的購票頁面就能看到了。
如下界面:
大家可以嘗試拿okhttp訪問下:
OkHttpClientManager.getAsyn ("https://kyfw.12306.cn/otn/", callack);會爆出如下錯誤
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.好了,本篇博文當然不是去說如何去訪問12306,而是以12306為例子來說明如何去訪問自籤名證書的網站。因為部分開發者app與自己服務端交互的時候可能也會遇到自籤名證書的。甚至在開發安全級別很高的app時,需要用到雙向證書的驗證。
那麼本篇博文的基本內容包含:
二、Https相關知識關於特別理論的東西大家可以百度下自己去了解下,這裡就簡單說一下,HTTPS相當於HTTP的安全版本了,為什麼安全呢?
因為它在HTTP的之下加入了SSL (Secure Socket Layer),安全的基礎就靠這個SSL了。SSL位於TCP/IP和HTTP協議之間,那麼它到底能幹嘛呢?
它能夠:
以上3條來自百度
下面我們簡單描述下HTTPS的工作原理,大家就能對應的看到上面3條作用的身影了:
HTTPS在傳輸數據之前需要客戶端(瀏覽器)與服務端(網站)之間進行一次握手,在握手過程中將確立雙方加密傳輸數據的密碼信息。握手過程的簡單描述如下:
瀏覽器將自己支持的一套加密算法、HASH算法發送給網站。
網站從中選出一組加密算法與HASH算法,並將自己的身份信息以證書的形式發回給瀏覽器。證書裡面包含了網站地址,加密公鑰,以及證書的頒發機構等信息。
瀏覽器獲得網站證書之後,開始驗證證書的合法性,如果證書信任,則生成一串隨機數字作為通訊過程中對稱加密的秘鑰。然後取出證書中的公鑰,將這串數字以及HASH的結果進行加密,然後發給網站。
網站接收瀏覽器發來的數據之後,通過私鑰進行解密,然後HASH校驗,如果一致,則使用瀏覽器發來的數字串使加密一段握手消息發給瀏覽器。
瀏覽器解密,並HASH校驗,沒有問題,則握手結束。接下來的傳輸過程將由之前瀏覽器生成的隨機密碼並利用對稱加密算法進行加密。
握手過程中如果有任何錯誤,都會使加密連接斷開,從而阻止了隱私信息的傳輸。
ok,以上的流程不一定完全正確,基本就是這樣,當然如果有明顯錯誤歡迎指出。
根據上面的流程,我們可以看到伺服器端會有一個證書,在交互過程中客戶端需要去驗證證書的合法性,對於權威機構頒發的證書當然我們會直接認為合法。對於自己造的證書,那麼我們就需要去校驗合法性了,也就是說我們只需要讓OkhttpClient去信任這個證書就可以暢通的進行通信了。
當然,對於自籤名的網站的訪問,網上的部分的做法是直接設置信任所有的證書,對於這種做法肯定是有風險的,所以這裡我們不去介紹了,有需要自己去查。
下面我們去考慮,如何讓OkHttpClient去信任我們的證書,接下裡的例子就是靠12306這個福利站點了。
首先導出12306的證書,這裡12306提供了下載地址:12306證書點擊下載
下載完成,解壓拿到裡面的srca.cer,一會需要使用。ps:即使沒有提供下載,也可以通過瀏覽器導出的,自行百度。
三、代碼(一)、訪問自籤名的網站首先把我們下載的srca.cer放到assets文件夾下,其實你可以隨便放哪,反正能讀取到就行。
然後在我們的OkHttpClientManager裡面添加如下的方法:
public void setCertificates(InputStream... certificates){ try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); int index = 0; for (InputStream certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); try { if (certificate != null) certificate.close(); } catch (IOException e) { } } SSLContext sslContext = SSLContext.getInstance("TLS"); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); sslContext.init ( null, trustManagerFactory.getTrustManagers(), new SecureRandom() ); mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory()); } catch (Exception e) { e.printStackTrace(); } }為了代碼可讀性,我把異常捕獲的部分簡化了,可以看到我們提供了一個方法傳入InputStream流,InputStream就對應於我們證書的輸入流。
代碼內部,我們:
構造CertificateFactory對象,通過它的generateCertificate(is)方法得到Certificate。
然後講得到的Certificate放入到keyStore中。
接下來利用keyStore去初始化我們的TrustManagerFactory
由trustManagerFactory.getTrustManagers獲得TrustManager[]初始化我們的SSLContext
最後,設置我們mOkHttpClient.setSslSocketFactory即可。
這樣就完成了我們代碼的編寫,其實挺短的,當客戶端進行SSL連接時,就可以根據我們設置的證書去決定是否新人服務端的證書。
記得在Application中進行初始化:
public class MyApplication extends Application{ @Override public void onCreate() { super.onCreate(); try { OkHttpClientManager.getInstance() .setCertificates(getAssets().open("srca.cer")); } catch (IOException e) { e.printStackTrace(); }}然後嘗試以下代碼訪問12306的網站:
OkHttpClientManager.getAsyn("https://kyfw.12306.cn/otn/", new OkHttpClientManager.ResultCallback<String>(){ @Override public void onError(Request request, Exception e) { e.printStackTrace(); } @Override public void onResponse(String u) { mTv.setText(u); }});這樣即可訪問成功。完整代碼已經更新至:https://github.com/hongyangAndroid/okhttp-utils,可以下載裡面的sample進行測試,裡面包含12306的證書。
ok,到這就可以看到使用Okhttp可以很方便的應對自籤名的網站的訪問,只需要拿到包含公鑰的證書即可。
(二)、使用字符串替代證書下面繼續,有些人可能覺得把證書copy到assets下還是覺得不舒服,其實我們還可以將證書中的內容提取出來,寫成字符串常量,這樣就不需要證書根據著app去打包了。
zhydeMacBook-Pro:temp zhy$ keytool -printcert -rfc -file srca.cerBEGIN CERTIFICATEMIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4XDTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb29bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHletne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovLzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Ptx1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9eeog555S+C3eJAAVeNCTeMS3N/M5hzBRJAoffn3qoYdAO1Q8bTguOi+2849A==END CERTIFICATE使用keytool命令,以rfc樣式輸出。keytool命令是JDK裡面自帶的。
有了這個字符串以後,我們就不需要srca.cer這個文件了,直接編寫以下代碼:
public class MyApplication extends Application{ private String CER_12306 = "BEGIN CERTIFICATE\n" + "MIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAn\n" + "BgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4X\n" + "DTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNp\n" + "bm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3\n" + "DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb2\n" + "9bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6\n" + "D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHle\n" + "tne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDov\n" + "LzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Pt\n" + "x1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV\n" + "23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9ee///VOhzR6Tebg9QfyPSohkBrhXQenvQ\n" + "og555S+C3eJAAVeNCTeMS3N/M5hzBRJAoffn3qoYdAO1Q8bTguOi+2849A==\n" + "END CERTIFICATE"; @Override public void onCreate() { super.onCreate(); OkHttpClientManager.getInstance() .setCertificates(new Buffer() .writeUtf8(CER_12306) .inputStream());}注意Buffer是okio包下的,okhttp依賴okio。
ok,這樣就省去將cer文件一起打包進入apk了。
接下來介紹,如何去生成證書以及在tomcat伺服器下使用自籤名證書部署服務。如果大家沒這方面需要可以簡單了解下。
四、tomcat下使用自籤名證書部署服務首先自行下載個tomcat的壓縮包。
既然我們要支持https,那麼肯定需要個證書,如何生成證書呢?使用keytool非常簡單。
(一)生成證書zhydeMacBook-Pro:temp zhy$ keytool -genkey -alias zhy_server -keyalg RSA -keystore zhy_server.jks -validity 3600 -storepass 123456您的名字與姓氏是什麼? [Unknown]: zhang您的組織單位名稱是什麼? [Unknown]: zhang您的組織名稱是什麼? [Unknown]: zhang您所在的城市或區域名稱是什麼? [Unknown]: xian您所在的省/市/自治區名稱是什麼? [Unknown]: shanxi該單位的雙字母國家/地區代碼是什麼? [Unknown]: cnCN=zhang, OU=zhang, O=zhang, L=xian, ST=shanxi, C=cn是否正確? [否]: y輸入 <zhy_server> 的密鑰口令 (如果和密鑰庫口令相同, 按回車):使用以上命令即可生成一個證書請求文件zhy_server.jks,注意密鑰庫口令為:123456.
接下來利用zhy_server.jks來籤發證書:
zhydeMacBook-Pro:temp zhy$ keytool -export -alias zhy_server -file zhy_server.cer -keystore zhy_server.jks -storepass 123456即可生成包含公鑰的證書zhy_server.cer。
(二)、配置Tomcat找到tomcat/conf/sever.xml文件,並以文本形式打開。
在Service標籤中,加入:
<Connector SSLEnabled="true" acceptCount="100" clientAuth="false" disableUploadTimeout="true" enableLookups="true" keystoreFile="" keystorePass="123456" maxSpareThreads="75" maxThreads="200" minSpareThreads="5" port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" scheme="https" secure="true" sslProtocol="TLS" />注意keystoreFile的值為我們剛才生成的jks文件的路徑:/Users/zhy/
temp/zhy_server.jks(填寫你的路徑).keystorePass值為密鑰庫密碼:123456。然後啟動即可,對於命令行啟動,依賴環境變量JAVA_HOME;如果在MyEclispe等IDE下啟動就比較隨意了。
啟動成功以後,打開瀏覽器輸入url:https://localhost:8443/即可看到證書不可信任的警告了。選擇打死也要進入,即可進入tomcat默認的主頁:
如果你在此tomcat中部署了項目,即可按照如下url方式訪問:
https://192.168.1.103:8443/項目名/path,沒有部署也沒關係,直接拿默認的主頁進行測試了,拿它的html字符串。對於訪問,還需要說麼,我們剛才已經生成了zhy_server.cer證書。你可以選擇copy到assets,或者通過命令拿到內部包含的字符串。我們這裡選擇copy。
依然選擇在Application中設置信任證書:
public class MyApplication extends Application{ private String CER_12306 = "省略..."; @Override public void onCreate() { super.onCreate(); try { OkHttpClientManager.getInstance() .setCertificates( new Buffer() .writeUtf8(CER_12306).inputStream(), getAssets().open("zhy_server.cer") ); } catch (IOException e) { e.printStackTrace(); } }}ok,這樣就能正常訪問你部署的https項目中的服務了,沒有部署項目的嘗試拿https://服務端ip:8443/測試即可。
注意:不要使用localhost,真機測試保證手機和伺服器在同一區域網段內。
ok,到此我們介紹完了如果搭建https服務和如何訪問,基本上可以應付極大部分的需求了。當然還是極少數的應用需要雙向證書驗證,比如銀行、金融類app,我們一起來了解下。
五、雙向證書驗證首先對於雙向證書驗證,也就是說,客戶端也會有個「kjs文件」,伺服器那邊會同時有個「cer文件」與之對應。
我們已經生成了zhy_server.kjs和zhy_server.cer文件。
接下來按照生成證書的方式,再生成一對這樣的文件,我們命名為:zhy_client.kjs,zhy_client.cer.
(一)配置服務端首先我們配置服務端:
服務端的配置比較簡單,依然是剛才的Connector標籤,不過需要添加些屬性。
<Connector 其他屬性與前面一致 clientAuth="true" truststoreFile="/Users/zhy/temp/zhy_client.cer" />將clientAuth設置為true,並且多添加一個屬性truststoreFile,理論上值為我們的cer文件。這麼加入以後,嘗試啟動伺服器,會發生錯誤:Invalid keystore format。說keystore的格式不合法。
我們需要對zhy_client.cer執行以下步驟,將證書添加到kjs文件中。
keytool -import -alias zhy_client -file zhy_client.cer -keystore zhy_client_for_sever.jks接下裡修改server.xml為:
<Connector 其他屬性與前面一致 clientAuth="true" truststoreFile="/Users/zhy/temp/zhy_client_for_sever.jks" />此時啟動即可。
此時再拿瀏覽器已經無法訪問到我們的服務了,會顯示基於證書的身份驗證失敗。
我們將目標來到客戶端,即我們的Android端,我們的Android端,如何設置kjs文件呢。
(二)配置app端目前我們app端依靠的應該是zhy_client.kjs。
ok,大家還記得,我們在支持https的時候調用了這麼倆行代碼:
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());注意sslContext.init的第一個參數我們傳入的是null,第一個參數的類型實際上是KeyManager[] km,主要就用於管理我們客戶端的key。
於是代碼可以這麼寫:
public void setCertificates(InputStream... certificates){ try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); int index = 0; for (InputStream certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate)); try { if (certificate != null) certificate.close(); } catch (IOException e) { } } SSLContext sslContext = SSLContext.getInstance("TLS"); TrustManagerFactory trustManagerFactory = TrustManagerFactory. getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); clientKeyStore.load(mContext.getAssets().open("zhy_client.jks"), "123456".toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(clientKeyStore, "123456".toCharArray()); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory()); } catch (Exception e) { e.printStackTrace(); } }核心代碼其實就是:
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());clientKeyStore.load(mContext.getAssets().open("zhy_client.jks"), "123456".toCharArray());KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());keyManagerFactory.init(clientKeyStore, "123456".toCharArray());sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());然而此時啟動會報錯:java.io.IOException: Wrong version of key store.
為什麼呢?
因為:Java平臺默認識別jks格式的證書文件,但是android平臺只識別bks格式的證書文件。
這麼就糾結了,我們需要將我們的jks文件轉化為bks文件,怎麼轉化呢?
這裡的方式可能比較多,大家可以百度,我推薦一種方式:
去Portecle下載Download portecle-1.9.zip (3.4 MB)。
解壓後,裡面包含bcprov.jar文件,使用jave -jar bcprov.jar即可打開GUI界面。
按照上圖即可將zhy_client.jks轉化為zhy_client.bks。
然後將zhy_client.bks拷貝到assets目錄下,修改代碼為:
KeyStore clientKeyStore = KeyStore.getInstance("BKS");clientKeyStore.load(mContext.getAssets().open("zhy_client.bks"), "123456".toCharArray());KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());keyManagerFactory.init(clientKeyStore, "123456".toCharArray());sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());再次運行即可。然後就成功的做到了雙向的驗證,關於雙向這塊大家了解下即可。
源碼都在https://github.com/hongyangAndroid/okhttp-utils之中。
ok,到此本篇博文就結束了,文章相當的長~~ 關於okhttp在https協議下的使用,應該沒什麼問題。
ps:如果大家對okhttp-utils有任何建議,非常歡迎提出,最近根據大家的需求修改相當頻繁~~