Android Https相關完全解析 當OkHttp遇到Https
一、概述
其實這篇文章理論上不限於okhttp去訪問自簽名的網站,不過接上篇博文了,就叫這個了。首先要了解的事,okhttp預設情況下是支援https協議的網站的,比如https://www.baidu.com
,https://github.com/hongyangAndroid/okhttp-utils
等,你可以直接通過okhttp請求試試。不過要注意的是,支援的https的網站基本都是CA機構頒發的證書,預設情況下是可以信任的。
當然我們今天要說的是自簽名的網站,什麼叫自簽名呢?就是自己通過keytool
去生成一個證書,然後使用,並不是CA機構去頒發的。使用自簽名證書的網站,大家在使用瀏覽器訪問的時候,一般都是報風險警告,好在有個大名鼎鼎的網站就是這麼幹的,https://kyfw.12306.cn/otn/
如下介面:
大家可以嘗試拿okhttp訪問下:
OkHttpClientManager.getAsyn ("https://kyfw.12306.cn/otn/", callack);
- 1
- 2
會爆出如下錯誤
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
- 1
- 2
- 3
好了,本篇博文當然不是去說如何去訪問12306,而是以12306為例子來說明如何去訪問自簽名證書的網站。因為部分開發者app與自己服務端互動的時候可能也會遇到自簽名證書的。甚至在開發安全級別很高的app時,需要用到雙向證書的驗證。
那麼本篇博文的基本內容包含:
- https一些相關的知識
- okhttp訪問自簽名https網站
- 如何構建一個支援https的伺服器(這裡主要為了測試多個證書的時候,如何去載入)
- 如何進行雙向證書驗證
二、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(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
為了程式碼可讀性,我把異常捕獲的部分簡化了,可以看到我們提供了一個方法傳入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(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
然後嘗試以下程式碼訪問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); }});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
ok,到這就可以看到使用Okhttp可以很方便的應對自簽名的網站的訪問,只需要拿到包含公鑰的證書即可。
(二)、使用字串替代證書
下面繼續,有些人可能覺得把證書copy到assets下還是覺得不舒服,其實我們還可以將證書中的內容提取出來,寫成字串常量,這樣就不需要證書根據著app去打包了。
zhydeMacBook-Pro:temp zhy$ keytool -printcert -rfc -file srca.cer-----BEGIN CERTIFICATE-----MIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4XDTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb29bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHletne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDovLzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Ptx1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9ee///VOhzR6Tebg9QfyPSohkBrhXQenvQog555S+C3eJAAVeNCTeMS3N/M5hzBRJAoffn3qoYdAO1Q8bTguOi+2849A==-----END CERTIFICATE-----
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
使用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());}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
注意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> 的金鑰口令 (如果和金鑰庫口令相同, 按回車):
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
使用以上命令即可生成一個證書請求檔案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
- 1
- 2
- 3
- 4
即可生成包含公鑰的證書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" />
- 1
- 2
- 3
- 4
- 5
- 6
注意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(); } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
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" />
- 1
- 2
- 3
- 4
將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
- 1
- 2
接下里修改server.xml為:
<Connector 其他屬性與前面一致 clientAuth="true" truststoreFile="/Users/zhy/temp/zhy_client_for_sever.jks" />
- 1
- 2
- 3
- 4
此時啟動即可。
此時再拿瀏覽器已經無法訪問到我們的服務了,會顯示基於證書的身份驗證失敗
。
我們將目標來到客戶端,即我們的Android端,我們的Android端,如何設定kjs檔案呢。
(二)配置app端
目前我們app端依靠的應該是zhy_client.kjs
。
ok,大家還記得,我們在支援https的時候呼叫了這麼倆行程式碼:
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());mOkHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
- 1
- 2
- 3
注意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 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(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
核心程式碼其實就是:
//初始化keystoreKeyStore 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());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
然而此時啟動會報錯:java.io.IOException: Wrong version of key store.
為什麼呢?
因為:Java平臺預設識別jks格式的證書檔案,但是android平臺只識別bks格式的證書檔案。
這麼就糾結了,我們需要將我們的jks檔案轉化為bks檔案,怎麼轉化呢?
這裡的方式可能比較多,大家可以百度,我推薦一種方式:
–
解壓後,裡面包含bcprov.jar
檔案,使用jave -jar bcprov.jar即可開啟GUI介面。
按照上圖即可將zhy_client.jks
轉化為zhy_client.bks
。
然後將zhy_client.bks
拷貝到assets目錄下,修改程式碼為:
//初始化keystoreKeyStore 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());
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
再次執行即可。然後就成功的做到了雙向的驗證,關於雙向這塊大家瞭解下即可。
ok,到此本篇博文就結束了,文章相當的長~~ 關於okhttp在https協議下的使用,應該沒什麼問題。
ps:如果大家對okhttp-utils有任何建議,非常歡迎提出,最近根據大家的需求修改相當頻繁~~
群號:463081660,歡迎入群
微信公眾號:hongyangAndroid (歡迎關注,第一時間推送博文資訊)