Android+Nginx一步步配置https單向/雙向認證請求

  最近想實現一箇舊項目https的防抓包功能,重新學習並且配置了下https相關通信知識,參考了不少文章,有些文章比較舊不全或者有誤,走了不少彎路和坑,所以整理出來方便自己鞏固以及供大家參考,指出不足或者有誤之處互相學習。
  本文的服務端環境:

Debian 9.8
Nginx

原創文章,歡迎轉載,轉載請註明:ifish.site
作者:JaydenZhou

一、需要的前置知識點

  本文需要的前置知識點,這也是我剛接觸時候繞得很暈的問題,自己前後端流程走一遍後,就清晰多了。

網絡相關的知識點:
https通信,CA發證機構,自簽發機構,公鑰,私鑰,數字證書,數字簽名、相關cer,pem,scr,key等關鍵字定義。

Android相關的知識點:
如何發起網絡請求,如何設置單向/雙向認證ssl,如何把一些數字證書轉成Android識別的bks格式證書等。

後端相關知識點:
Liunx基本知識;如何域名解析(或直接ip)訪問;https的CA證書/自簽名證書如何生成;如何配置nginx中的https訪問;

  https的通信,是從非對稱加密(RSA)到對稱加密(AES)的一個過程,其中數字證書扮演着重要作用。 藉助文章:https://juejin.im/post/5c9cbf1df265da60f6731f0a 裏面所講,

數字證書  = 公鑰 + 簽名 + 申請者和頒發者的信息
簽名 = 私鑰 + 信息摘要(hash處理過的不可逆的明文信息)

類比於我們的身份證(數字證書) = 證件號(公鑰) + 公安蓋章(簽名) + 個人姓名/發證公安局(申請者和頒發者的信息)。
私鑰一般以.key結尾,用它才能跟對應的數字證書(公鑰)互相解密。

二、單向/雙向認證的應用場景在哪裏呢?

單向認證: 這裏有個簡單的理解,凡是你可以直接訪問的網站(比如我的域名: https://ifish.site ); 直接請求的https的api等,都是單向認證,因爲它只需要client端能夠解密出server端的數字證書(分CA和自簽發的),操作系統或者瀏覽器一般都內置了一堆相關CA的證書,所以可以直接訪問;若是自簽發的,瀏覽器會提示該證書不受信任。適用場景是站點訪問,非高機密數據傳輸。
雙向認證: 顧名思義,就是在單向基礎上,添加上了服務端要校驗客戶端的公鑰,客戶端要自己保存着自己的私鑰來加密,客戶端發過來的請求要用該私鑰來加密後,才能跟服務器進行完整通信。適用場景:企業間對應機密api接口的數據傳輸。

三、證書的生成

  配置好Linux環境後,首先我們要生成對應的證書,兩種方法如下:

方法一:用CA籤的證書(有收費 or 免費),這樣瀏覽器就不會顯示不信任提示,前提是要有合法的境內實名域名,然後比如在阿里雲服務器管理後臺界面進行證書的申請。

方法二:用openssl在自己服務器上,製作自簽發的證書,瀏覽器也可以訪問,但是會有不信任提示。

方法一按照對應服務商的提示來操作就行,這裏講下openssl來生成的方法,先拋出一個我自己現在也疑惑的問題:
爲何要生成root根CA證書,然後再發布二級server和client證書? 是爲了一個根證書可以直接管理多個二級證書?
本文爲了簡化演示https單向/雙向認證,只需要生成對應的 server 和 client 相關證書就行,避免文件太多導致像我這樣的新手造成的困惑和配置出錯。
1.生成服務端key:

openssl genrsa -out server-key.key 1024

2.生成服務端證書請求文件(這步很關鍵,彈出信息填寫提示時候,“Common Name”一定要填寫你自己的域名,其他的可以直接回車):

openssl req -new -out server-req.csr -key server-key.key

比如我的域名是 ifish.site
name

3.生成服務端證書cer:

openssl x509 -req -in server-req.csr -out server-cert.cer -signkey server-key.key  -CAcreateserial -days 3650

4.生成客戶端key(同上面方法一樣):

openssl genrsa -out client-key.key 1024

5.生成服務端證書請求文件(Common Name最好一致):

openssl req -new -out client-req.csr -key client-key.key

6.生成客戶端證書cer:

openssl x509 -req -in client-req.csr -out client-cert.cer -signkey client-key.key -CAcreateserial -days 3650

7.生成客戶端帶密碼的p12證書(這步很重要,雙向認證的話,瀏覽器訪問時候要導入該證書纔行;Android請求的時候也需要把它轉成bks來請求雙向認證):

openssl pkcs12 -export -clcerts -in client-cert.cer -inkey client-key.key -out client.p12

四、nginx配置

1.將生成的證書,爲了方便管理,建議放到nginx相同目錄下,比如我是放到“/usr/local/nginx/conf/ssl_cust” 裏面;
2.打開對應的 conf 裏面要https訪問的域名,如果之前有配過443端口,那麼只需要指定下對應的證書即可,其中

單向認證是:
ssl_certificate      ssl_cust/server-cert.cer;
ssl_certificate_key  ssl_cust/server-key.key;
雙向認證是:
ssl_certificate      ssl_cust/server-cert.cer;
ssl_certificate_key  ssl_cust/server-key.key;
ssl_client_certificate   ssl_cust/client-cert.cer;
ssl_verify_client    on;

3.若完全沒有配過https的話,可以參考我的配置:

server
    {
        listen       443 ssl;
        server_name  ifish.site www.ifish.site;
        ssl on;
        ssl_certificate      ssl_cust/server-cert.cer;
        ssl_certificate_key  ssl_cust/server-key.key;
# 雙向認證一般不開啓, #是註釋掉
#        ssl_client_certificate   ssl_cust/client-cert.cer;
#        ssl_verify_client    on;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
    }

4.保存關閉後,執行以下命令讓nginx重啓生效:

sudo nginx -s reload

5.如此我們配置就生效了,可以用瀏覽器來驗證下,如果只是單向認證,直接瀏覽器輸入域名來訪問即可,會有不安全提示;
若是雙向認證,直接訪問會出現 400 Bad Requst, 需要我們手動添加證書,這裏是Chrome下的截圖,我們需要添加之前我們生成的 client.p12 文件。
img

五、Android代碼請求

  終於到Android請求的代碼寫法了,爲了簡化Demo的獨立訪問,這裏引入了 xUtils庫 (https://github.com/wyouflf/xUtils3 ),當然你也可以自己手寫或者用比如Retrofit、OkHttp等網絡庫。
單向認證的寫法:
  其實單向認證,用xUtils的話可以不需要設置setSslSocketFactory內容,因爲庫代碼DefaultParamsBuilder.java裏面判斷了如果沒有設置自定義ssl,那麼就直接用操作系統自帶的進行返回,從而來訪問https。
  我們這裏爲了演示下具體代碼,所以自定義一個ssl的構造出來,操作如下:
把服務端server-cert.cer證書放到 assets 目錄下,然後創建一個 SSLHelper.java 的輔助類:

public static SSLSocketFactory getSSLSingleFactory(Context context) {
    try {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null);
        InputStream is = context.getAssets().open("server-cert.cer");
        keyStore.setCertificateEntry("0", certificateFactory.generateCertificate(is));
        if(is != null) {
            is.close();
        }
        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
            return sslContext.getSocketFactory();
    } catch (CertificateException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    }
    return null;
}

對應的Activity請求裏的代碼是:

RequestParams params = new RequestParams("https://ifish.site");
params.setSslSocketFactory(SSLHelper.getSSLSingleFactory(this));
// 因爲是自籤不受權威機構認證,所以繞過不檢查域名ssl。
params.setHostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
});

x.http().get(params, new Callback.CommonCallback<String>() {
    @Override
    public void onSuccess(String result) {
        Log.d(TAG, "onSuccess...result = " + result);
    }
    @Override
    public void onError(Throwable ex, boolean isOnCallback) {
        Log.d(TAG, "onError...ex = " + ex.getMessage());
    }
    @Override
    public void onCancelled(CancelledException cex) { }
    @Override
    public void onFinished() { }
});

雙向認證的寫法:
雙向認證要求的是客戶端也需要持有一份自己的私鑰key、服務端要有一份客戶端的公鑰證書。但是由於Android系統限制,我們需要把client.p12轉成client.bks格式,才能被訪問到。介紹一個轉化工具,叫做“Portecle”,親測可用的下載和使用鏈接如下:https://blog.csdn.net/zhangyong125/article/details/50402183,也可以自己去官網免費下載,如果是Linux系統, 可以用 java -jar protecle.jar 來運行,然後按照上文鏈接的使用方法,把對應的 client.p12轉成client.bks格式。
對應的 SSLHelper.java添加一個雙向認證方法:

public static SSLSocketFactory getSSLDoubleFactory(Context context) {
    try {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null);
        InputStream is = context.getAssets().open("server-cert.cer");
        keyStore.setCertificateEntry("0", certificateFactory.generateCertificate(is));
        if(is != null) {
            is.close();
        }
        SSLContext sslContext = SSLContext.getInstance("TLS");
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);

        // 初始化雙向客戶端keyStore
        KeyStore clientKeyStore = KeyStore.getInstance("BKS");
        clientKeyStore.load(context.getAssets().open("client.bks"), "123456".toCharArray());
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(clientKeyStore, "123456".toCharArray());
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
        return sslContext.getSocketFactory();
    } catch (CertificateException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (UnrecoverableKeyException e) {
        e.printStackTrace();
    } catch (KeyManagementException e) {
        e.printStackTrace();
    }
    return null;
}

然後Activity裏面,params.setSslSocketFactory(SSLHelper.getSSLSingleFactory(this)); 換成 params.setSslSocketFactory(SSLHelper.getSSLDoubleFactory(this)); 即可。

六、總結

疑難點:
1.涉及的知識點比較多,很多不是Android本身的東西;
2.https單向/雙向原理不太好理解,可以看該文https://juejin.im/post/5c9cbf1df265da60f6731f0a;
3.僅驗證單雙向認證來說,沒必要生成根CA證書,部分文章生成太多證書會導致配置上容易亂。

調試驗證技巧:
  原Android網絡請求框架龐大,對應的域名是生產環境的域名,絕對不能隨便動後臺的生產環境配置。因此需要自己的一臺服務器,自己搭建一個簡單的後臺Demo https請求,以及Android的Demo網絡請求app,從而方便Debug。

參考:
https://www.zhihu.com/question/29620953 – SSL中,公鑰、私鑰、證書的後綴名都是些啥?
https://juejin.im/post/5c9cbf1df265da60f6731f0a – 扯一扯HTTPS單向認證、雙向認證、抓包原理、反抓包策略
https://zhuanlan.zhihu.com/p/60392573 – 爲了抓包某app,我折騰了10天,原來他是用SSL Pinning防抓包的
https://www.cnblogs.com/yelao/p/9486882.html – Nginx https 雙向認證
https://blog.csdn.net/jsc702325/article/details/76724010 – Android 用自簽名證書實現https請求
https://www.cnblogs.com/guogangj/p/4118605.html – 那些證書相關的玩意兒(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)
https://blog.csdn.net/zhangyong125/article/details/50402183 – P12證書轉BKS證書

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章