Android 應(yīng)用實現(xiàn) Https 雙向認(rèn)證

為什么需要雙向認(rèn)證

Https保證的是信道的安全,即客戶端和服務(wù)端通信報文的安全。但是無法保證中間人攻擊,所以雙向認(rèn)證解決的問題就是防止中間人攻擊。

中間人攻擊(Man-in-the-MiddleAttack)簡稱(MITM),是一種“間接”的入侵攻擊,這種攻擊模式是通過各種技術(shù)手段將受入侵者控制的一臺計算機虛擬放置在網(wǎng)絡(luò)連接中的兩臺通信計算機之間,這臺計算機就稱為“中間人”。若沒有開啟雙向認(rèn)證,中間人可以攔截客戶端發(fā)送的請求,然后篡改信息再發(fā)送到服務(wù)端;中間人也可以攔截服務(wù)端返回的信息,再發(fā)送到客戶端。所以使用Https的單向認(rèn)證或雙向認(rèn)證能夠有效防止中間人攻擊。

注:無論Ca證書還是自簽證書都需要雙向認(rèn)證。

雙向認(rèn)證原理

1、服務(wù)端認(rèn)證客戶端原理

客戶端有自己的bks證書auth_client.bks,并將導(dǎo)出的auth_client_pub.cer證書導(dǎo)入到服務(wù)端證書auth_server.keystore中,這樣服務(wù)端就將客戶端證書添加到信任列表中,從而能夠讓帶有該auth_client_pub.cer證書信息的客戶端訪問服務(wù)。

2、客戶端認(rèn)證服務(wù)端原理

服務(wù)端有自己的證書(ca頒發(fā)的或者是自己創(chuàng)建的)auth_server.keystore,并導(dǎo)出auth_server_pub.cer證書,將該證書導(dǎo)入到客戶端證書

auth_truststore.jks中,注意:這里不是導(dǎo)入到auth_client.jks中,而是導(dǎo)入生成另一個證書auth_truststore.jks,最后再將jks證書轉(zhuǎn)化成bks證書。

實現(xiàn)過程

一、服務(wù)端證書

創(chuàng)建服務(wù)端證書

keytool -genkeypair -alias auth_server -keyalg RSA -validity 36500 -keypass auth_server -storepass auth_server -keystore /Users/renzhongrui/android/certs/auth_server.keystore

導(dǎo)出服務(wù)端證書公鑰

keytool -export -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

二、客戶端證書

創(chuàng)建客戶端證書(andoird不能用keystore格式的密鑰庫,所以先生成jks格式,再用Portecle工具轉(zhuǎn)成bks格式)

keytool -genkeypair -alias auth_client -keyalg RSA -validity 36500 -keypass auth_client -storepass auth_client -keystore /Users/renzhongrui/android/certs/auth_client.jks

導(dǎo)出客戶端證書公鑰

keytool -export -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_client.jks -storepass auth_client 

三、證書交換

將客戶端證書導(dǎo)入服務(wù)端keystore中,再將服務(wù)端證書導(dǎo)入客戶端auth_truststore中, 一個keystore可以導(dǎo)入多個證書,生成證書列表。

將客戶端公鑰導(dǎo)入到服務(wù)端keystore證書中,使得服務(wù)端能夠信任客戶端。

keytool -import -v -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

生成客戶端信任證書庫auth_truststore.jks,即將服務(wù)端公鑰導(dǎo)入到客戶端jks證書中,使得客戶端能夠信任服務(wù)端。

keytool -import -v -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore

最后驗證一下,查看證書庫中的所有證書

keytool -list -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

keytool -list -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore

四、證書轉(zhuǎn)換

下載portecle.jar(https://sourceforge.net/projects/portecle/),解壓后運行jar包:
java -jar portecle.jar

1、點擊File菜單選擇Open Keystore File,選擇創(chuàng)建好的auth_client.jks或auth_truststore.jks證書,輸入密碼。

2、選中導(dǎo)入的證書,點擊Tools菜單,選擇Change Keystore Type,再選擇BKS類型,再次輸入密碼,確認(rèn)之后,會顯示成功。

3、最后點擊File菜單,選擇Save Keystore File As,將證書保存的指定路徑。

五、配置服務(wù)

證書準(zhǔn)備好之后,就可以進行集成測試了,服務(wù)使用Spring Boot創(chuàng)建或者使用Nginx代理。

使用Spring Boot服務(wù)

1、添加配置

server:
  port: 443
  server:
    tomcat:
      uri-encoding: UTF-8
開啟https,配置跟證書對應(yīng)
  ssl:
    key-store: classpath:auth_server.keystore
    key-store-type: JKS
    key-store-password: auth_server
    key-password: auth_server
    key-alias: auth_server
    enabled: true
    #是否需要進行認(rèn)證
    client-auth: need
    protocol: TLS # 默認(rèn)
    trust-store: classpath:auth_server.keystore
    trust-store-password: auth_server
    trust-store-type: JKS

2、添加代碼,這里配置80端口重定向到443,也可以改成別的端口。

public class PackApplication implements WebMvcConfigurer {


    public static void main(String[] args) {
        SpringApplication.run(PackApplication.class, args);
    }


    @Bean
    public Connector connector(){
        Connector connector=new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(80);
        connector.setSecure(false);
        connector.setRedirectPort(443);
        return connector;
    }


    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector){
        TomcatServletWebServerFactory tomcat=new TomcatServletWebServerFactory(){
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint=new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection=new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(connector);
        return tomcat;
    }
}

使用Nginx服務(wù)配置

Nginx配置與Spring Boot服務(wù)配置略有不同。

server {
        listen       443;
        server_name  192.168.200.101; # 代理服務(wù)IP
        ssl on; # 開啟Https


        ssl_certificate      /usr/local/nginx/conf/https/auth_server.cer; # auth_server.keystore導(dǎo)出的cer證書
        ssl_certificate_key  /usr/local/nginx/conf/https/auth_server.key; # auth_server.keystore導(dǎo)出的私鑰
        ssl_client_certificate /usr/local/nginx/conf/https/auth_client.cer; # auth_client.keystore導(dǎo)出的cer

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_verify_client optional; # 配置校驗客戶端策略,設(shè)置成optional時候可以開啟白名單接口


        ssl_protocols TLSv1.1 TLSv1.2;


        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  off;


        location / { # 需要雙向驗證https的接口
            if ($ssl_client_verify != SUCCESS) {
                 return 401;
            }
            proxy_pass http://192.168.200.101:8008;
            proxy_connect_timeout 600;
            proxy_read_timeout 600;
        }


        location /aarm/downloadUpdateFile { # 獲取證書版本和下載證書接口,不需要驗證Https
            proxy_pass http://192.169.200.101:8008;
            proxy_connect_timeout 600;
            proxy_read_timeout 600;
        }
    }

六、配置客戶端

在客戶端app中使用OkHttp來進行網(wǎng)絡(luò)訪問,所以需要配置OkHttp來進行證書認(rèn)證。

1、將上面創(chuàng)建的auth_client.bks和auth_truststore.bks證書放到assets目錄下

2、初始化OkHttp

OkHttpClient okHttpClient = new OkHttpClient.Builder()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(10, TimeUnit.SECONDS)
  .sslSocketFactory(Https.getSSLCertifcation(getApplicationContext()))//獲取SSLSocketFactory
  .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName驗證器
  .build();

重點需要看一下Https類的實現(xiàn):

public class Https {
    private final static String CLIENT_PRI_KEY = "auth_client.bks";
    private final static String TRUSTSTORE_PUB_KEY = "auth_truststore.bks";
    private final static String CLIENT_BKS_PASSWORD = "auth_client";
    private final static String TRUSTSTORE_BKS_PASSWORD = "auth_truststore";
    private final static String KEYSTORE_TYPE = "BKS";
    private final static String PROTOCOL_TYPE = "TLS";
    private final static String CERTIFICATE_FORMAT = "X509";

    public static SSLSocketFactory getSSLCertifcation(Context context) {
        SSLSocketFactory sslSocketFactory = null;
        try {
            // 服務(wù)器端需要驗證的客戶端證書,其實就是客戶端的keystore
            KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 客戶端信任的服務(wù)器端證書
            KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);//讀取證書
            InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
            InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);//加載證書
            keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
            trustStore.load(tsIn, TRUSTSTORE_BKS_PASSWORD.toCharArray());
            ksIn.close();
            tsIn.close();
            //初始化SSLContext
            SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_FORMAT);
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_FORMAT);
            trustManagerFactory.init(trustStore);
            keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
            sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
            sslSocketFactory = sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sslSocketFactory;
    }
}

還有一個UnSafeHostnameVerifier類

private class UnSafeHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String hostname, SSLSession session) {
    return true;
  }
}

此時再進行網(wǎng)絡(luò)請求,就能夠訪問到帶有雙向認(rèn)證的服務(wù)端接口了。當(dāng)然一般網(wǎng)上的博客到這就結(jié)束了,但是這樣真的就完事了嗎,其實真正的設(shè)計才剛開始,如果只是了解原理,讀到這里就可以了,下面才是真實應(yīng)用場景。

真實場景實現(xiàn)

原理還是那個原理,就看怎么合理的使用了。在真實開發(fā)環(huán)境中,需要解決幾個問題:

auth_client.bks和auth_truststore.bks是需要動態(tài)下發(fā)的
不是所有的接口都需要進行雙向認(rèn)證

動態(tài)下發(fā)auth_client.bks和auth_truststore.bks
1、auth_client.bks和auth_truststore.bks的制作需要在本地工具完成,然后通過管理端上傳到服務(wù)器,并且改變證書的版本號;

2、客戶端需要訪問證書版本,來判斷是否需要更新證書,如果需要更新則下載證書。

這里會引出兩個問題:

1、請求版本號的接口和下載證書的接口不能進行雙向認(rèn)證,否則無法下發(fā)證書。

2、不進行雙向認(rèn)證的接口是不安全的,所以,請求版本號的接口的返回值是需要加密的;

針對第一個問題處理方式

服務(wù)端需要配置白名單,將請求版本號的接口和下載證書的接口過濾掉;

客戶端OkHttp首次初始化不能進行雙向認(rèn)證,等下載完證書之后,需要再次進行OkHttp初始化;

針對第二個問題處理方式

需要本地工具創(chuàng)建RSA公私鑰對,用于請求版本號接口的加解密;

服務(wù)端使用私鑰對報文加密,客戶端保存公鑰,并使用公鑰對報文解密。

客戶端使用公鑰解密后的報文格式:

{
    "version":1,
    "authType":2,
    "clientBksPath":"https://localhost/downloadUpdateFile?fileName=auth_client.bks",
    "trustBksPath":"https://localhost/downloadUpdateFile?fileName=auth_truststore.bks",
    "authKey":"auth_client"
}

version: 表示每一次更換證書的版本;
authType:0 表示不開啟認(rèn)證,1 表示開啟單向認(rèn)證,2 表示開啟雙向認(rèn)證
clientBksPath:auth_client.bks下載路徑
trustBksPath:auth_truststore.bks下載路徑
authKey:auth_client.bks證書密碼
客戶端每次啟動都要獲取服務(wù)端證書版本,并將證書信息存儲到本地文件或者數(shù)據(jù)庫中,通過對比服務(wù)端證書版本和數(shù)據(jù)庫中版本來判斷是否需要證書更新。

注:這樣設(shè)計的好處是當(dāng)證書過期時,能夠動態(tài)下發(fā)證書,但會引出一個問題,客戶端要安全的存儲公鑰信息,一般做法是將公鑰存儲到so文件里,再配合應(yīng)用加固手段進行保護,不過這個就不是通信安全的問題了,而是apk安全的問題。

其他證書操作
1、查看keystore證書公鑰

keytool -list -rfc --keystore release.keystore | openssl x509 -inform pem -pubkey

2、查看keystore證書私鑰

先轉(zhuǎn)成pfx格式

keytool -v -importkeystore -srckeystore release.keystore -srcstoretype jks -srcstorepass 123456 -destkeystore keystore/release.pfx -deststoretype pkcs12 -deststorepass 123456 -destkeypass 123456

再查看證書私鑰

openssl pkcs12 -in release.pfx -nocerts -nodes

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容