Android Https相關完全解析 當OkHttp遇到Https

jopen 8年前發布 | 34K 次閱讀 Android開發 移動開發

轉載請標明出處:
http://blog.csdn.net/lmj623565791/article/details/48129405
本文出自:【張鴻洋的博客】

一、概述

其實這篇文章理論上不限于okhttp去訪問自簽名的網站,不過接上篇博文了,就叫這個了。首先要了解的事,okhttp默認情況下是支持https協議的網站的,比如https://www.baidu.comhttps://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一些相關的知識
  • okhttp訪問自簽名https網站
  • 如何構建一個支持https的服務器(這里主要為了測試多個證書的時候,如何去加載)
  • 如何進行雙向證書驗證

二、Https相關知識

關于特別理論的東西大家可以百度下自己去了解下,這里就簡單說一下,HTTPS相當于HTTP的安全版本了,為什么安全呢?

因為它在HTTP的之下加入了SSL (Secure Socket Layer),安全的基礎就靠這個SSL了。SSL位于TCP/IP和HTTP協議之間,那么它到底能干嘛呢?

它能夠:

  • 認證用戶和服務器,確保數據發送到正確的客戶機和服務器;(驗證證書)
  • 加密數據以防止數據中途被竊取;(加密)
  • 維護數據的完整性,確保數據在傳輸過程中不被改變。(摘要算法)

以上3條來自百度

下面我們簡單描述下HTTPS的工作原理,大家就能對應的看到上面3條作用的身影了:

HTTPS在傳輸數據之前需要客戶端(瀏覽器)與服務端(網站)之間進行一次握手,在握手過程中將確立雙方加密傳輸數據的密碼信息。握手過程的簡單描述如下:

  1. 瀏覽器將自己支持的一套加密算法、HASH算法發送給網站。
  2. 網站從中選出一組加密算法與HASH算法,并將自己的身份信息以證書的形式發回給瀏覽器。證書里面包含了網站地址,加密公鑰,以及證書的頒發機構等信息。
  3. 瀏覽器獲得網站證書之后,開始驗證證書的合法性,如果證書信任,則生成一串隨機數字作為通訊過程中對稱加密的秘鑰。然后取出證書中的公鑰,將這串數字以及HASH的結果進行加密,然后發給網站。
  4. 網站接收瀏覽器發來的數據之后,通過私鑰進行解密,然后HASH校驗,如果一致,則使用瀏覽器發來的數字串使加密一段握手消息發給瀏覽器。
  5. 瀏覽器解密,并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.cer
-----BEGIN CERTIFICATE-----
MIICmjCCAgOgAwIBAgIIbyZr5/jKH6QwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAn
BgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4X
DTA5MDUyNTA2NTYwMFoXDTI5MDUyMDA2NTYwMFowRzELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNp
bm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDMpbNeb34p0GvLkZ6t72/OOba4mX2K/eZRWFfnuk8e5jKDH+9BgCb2
9bSotqPqTbxXWPxIOz8EjyUO3bfR5pQ8ovNTOlks2rS5BdMhoi4sUjCKi5ELiqtyww/XgY5iFqv6
D4Pw9QvOUcdRVSbPWo1DwMmH75It6pk/rARIFHEjWwIDAQABo4GOMIGLMB8GA1UdIwQYMBaAFHle
tne34lKDQ+3HUYhMY4UsAENYMAwGA1UdEwQFMAMBAf8wLgYDVR0fBCcwJTAjoCGgH4YdaHR0cDov
LzE5Mi4xNjguOS4xNDkvY3JsMS5jcmwwCwYDVR0PBAQDAgH+MB0GA1UdDgQWBBR5XrZ3t+JSg0Pt
x1GITGOFLABDWDANBgkqhkiG9w0BAQUFAAOBgQDGrAm2U/of1LbOnG2bnnQtgcVaBXiVJF8LKPaV
23XQ96HU8xfgSZMJS6U00WHAI7zp0q208RSUft9wDq9ee///VOhzR6Tebg9QfyPSohkBrhXQenvQ
og555S+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]:  cn
CN=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.kjszhy_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
        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
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
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有任何建議,非常歡迎提出,最近根據大家的需求修改相當頻繁~~


歡迎關注我的微博:
http://weibo.com/u/3165018720


群號:463081660,歡迎入群

微信公眾號:hongyangAndroid
(歡迎關注,第一時間推送博文信息)

參考

來自: http://blog.csdn.net//lmj623565791/article/details/48129405

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!