Netty4/Android6 SSL雙向認證 攻略 2016.10.13


  在Android端實現SSL可謂是遍地是坑,出錯的原因多種多樣,解決方法也各不相同,單單一篇文章不可能填完所有的坑,我會把解決問題的步驟和思路分享給大家。

  首先要感謝各位前輩的努力,由於SSL調了10來天才調通,中間還隔着10.1,好多文章已經忘了出處,有的還是同事給找的資料,我會盡量在結尾注釋出各位前輩的研究。

  

  一、Netty JAR包

  jar包在Netty的官網,Download處下載,下載到的壓縮包中有一個如“all-in-on”的jar。

  

  二、SSL密鑰

  所謂雙向認證,自然要有服務器和客戶端的證書,生成的步驟如下,閱讀下面的步驟之后可以大致領悟認證的流程,直接拿來用也是可以的。

  1.擴展keytool。生成證書要使用jdk中的keytool,直接用keytool生成的密鑰是.jks格式,在Android上只能用.bks格式的密鑰。為了生成bks格式的密鑰,首先要下載BouncyCastle,擴展keytool,使他能夠生成bks格式的密鑰。選擇Provider這一列中對應你的jdk版本的jar包。

  

  將下載的jar復制到%JRE_HOME%\lib\ext 和 %JDK_HOME%\jre\lib\ext 下

  然后打開%JRE_HOME%\lib\security\java.security,和%JDK_HOME%\jre\lib\security\java.security\java.security在下圖的位置上加一行:

  security.provider.11=org.bouncycastle.jce.provider.BouncyCastleProvider

    

  注意security.provider.11是依次序排下來的

  另外聽說bouncycastle在小米2s這個業界毒瘤上只有146版本的能用,手上沒有測試機,遇到問題的可以改下版本試試。

  2.生成服務器端的JKS密鑰庫kserver.keystore

  keytool -genkeypair -v -alias server -keyalg RSA -sigalg SHA1withRSA -keystore kserver.keystore

  可以看看《Java Security:keytool工具使用說明》理解下各個參數的意義,尤其你在開發過程中遇到如SSLHandShake failed之類的錯誤,一定要根據自己的情景設置各項參數。不能照搬網上的代碼。也可以看Java官方的文檔。下面解釋一下兩個參數:

  -sigalg SHA1withRSA

  -keyalg RSA

  這兩個參數是為了支持Android M,可以在Android Developer上找到Cipher suites這張表,有這樣一行  

  

  后面的數字表示支持的API Level

  現在回過頭來看一看這張表,Android N支持的SSL算法又變了,Android N馬上就要來了,到時候又要改一次代碼。你們如果知道怎么生成Android N的密鑰,請務必在評論中告訴我。

  3.從服務器端密鑰庫kserver.keystore中導出服務器證書

  keytool -exportcert -v -alias server -file server.cer -keystore kserver.keystore

  4.將導出的服務器端證書導入到客戶端信任密鑰庫tclient.bks中,其中客戶端信任密鑰庫自動生成,並且此時要特別指明信任密鑰庫是BKS類型的

  keytool -importcert -v -alias server -file server.cer -keystore tclient.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

  到這一步的時候留意一下CMD的輸出,這里列出了各項密鑰信息

  5.生成客戶端密鑰庫kclient.bks

  keytool -genkeypair -v -alias client -keyalg RSA -sigalg SHA1withRSA -keystore kclient.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

  6.導出客戶端證書

  keytool -exportcert -v -alias client -file client.cer -keystore kclient.bks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

  7.導入生成服務器端信任密鑰庫

  keytool -importcert -v -alias client -file client.cer -keystore tserver.keystore

   

  三、隨便說一說Netty

  懂Netty的不用往下看了,因為我也是剛接觸

  1.Netty中的pipeline.

    可以先網上搜下Netty的大致流程,我就不獻丑了。對於pipeline可以看一下《Netty權威指南》的第17章。

  對於應用了SSL的工程,pipeline的第一個必須是 

pipeline.addLast(new SslHandler(sslEngine));

 

  

pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 4, 4));
pipeline.addLast(new M2MMessageDecoder());
pipeline.addLast(new M2MMessageEncoder());

pipeline.addLast(new ClientHandler());

  上面是我用到的代碼,第一個用於分包

  粗略的理解就是把收到的一堆Byte分成若干個HTTP的幀。比較實用的有

  1)按行分割 

pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Delimiters.lineDelimiter()));

  這種分割方式要求服務器發送的數據必須要用\r\n結尾

 

 

 2)按\0分割 
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Delimiters.nulDelimiter()));

 

  這種分割方式要求服務器發送的數據必須要用\0結尾

  3)按幀長度分割

pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 4, 4));

  這種方式比較靈活,適用於自定義的協議,各參數的意義可以查看官方文檔

 

   點開源碼可以發現,這三個類都繼承於 ByteToMessageDecoder 而之后再pipeline中的ChannelHandler(ChannelHandler表示ChannelPipeline中的各個項,類似與Mina的Filter)都是繼承於MessageToMessageDecoder

  經過了上面的步驟之后pipeline的下一個Channelhandler將以ByteBuf的類型接受到消息數據。

  要想將ByteBuf解析為POJO,就要定義MessageToMessageDecoder,舉個簡單的例子,將收到的ByteBuf消息轉為byte[]發送給下一個ChannelHandler:

public class M2MMessageDecoder extends MessageToMessageDecoder<ByteBuf> {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        int totalByteLength = byteBuf.readableBytes();
        byte[] bytes = new byte[totalByteLength];
        byteBuf.readBytes(bytes);

        list.add(bytes);
    }
}

  decode方法中又一個List<Object> list,我們只需向這個list中插入任意格式對象,就完成了解碼。樣,在下一個ChannelHandler收到這個對象格式的消息。

  反過來,在encoder中需要把Object轉成byte[]

public class M2MMessageEncoder extends MessageToMessageEncoder<byte[]> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List<Object> list) throws Exception {
        list.add(Unpooled.copiedBuffer(bytes));
    }
}

  最后一個ChannelHandler用於處理業務邏輯,他繼承於SimpleChannelInboundHandler

  在收到服務器發出的數據時會回調channelRead0方法,如果重寫了channelRead方法,channelRead0將不會執行,

  注意:channelRead0會在收到特定格式數據時被調用(取決於模板)

       channelRead會在收到任意格式時被調用

   注意:可以添加任意多個ChannelHandler

 

  四、給個生成SSLContext的代碼

  寫個文章連點現成的代碼都不給,豈不是很沒誠意,這個拿去就能用,但仍然有坑

  

package com.sg.nettyblackglasses.server;

import android.content.Context;

import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

public class SslContextFactory
{
    private static final String    PROTOCOL    = "TLSv1.2";//我是坑 public static SSLContext getClientContext(Context c)
    {
        SSLContext clientContext = null;

        try {
            String keyStorePassword = "1234567";

            // 一定要聲明密鑰是BKS格式
            KeyStore ks = KeyStore.getInstance("BKS");
            ks.load(c.getResources().getAssets().open("kclient.bks"),
                    keyStorePassword.toCharArray());

            // 這里默認是SunX509
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(ks, keyStorePassword.toCharArray());

            // truststore
            KeyStore ts = KeyStore.getInstance("BKS");
            ts.load(c.getResources().getAssets().open("tclient.bks"),
                    keyStorePassword.toCharArray());

            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(ts);
            clientContext = SSLContext.getInstance(PROTOCOL);
            clientContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return clientContext;
    }
}

  坑就在這一行

private static final String    PROTOCOL = "TLSv1.2";

  按理說,這樣的代碼在Android L/M上是不能正常執行的

  

  這是developer.android上的文檔,上面明確指出TLSv1.2是不支持API LEVEL 20以下的,也就是4.4及以下,然而在實際使用中卻沒有任何問題,那么為什么不用TLSv1呢,因為據說TLSv1自身有Bug並不安全。。。雖然暫時沒有問題,但是隱約覺得這一定是個坑 已經被玩壞了。唉,反正是只能用它嘍。

 

  五、未解之謎

  據說Android6要用JDK1.8,然而我用1.7也沒什么問題

  javax.net.ssl.SSLHandshakeException: Handshake failed

  ...

  BAD_DH_P_LENGTH...

  有人遇到這個錯誤,升級到1.8就好了,然而我是1.7JDK仍然管用,升級JDK這個解決方案看起來相當靠譜,遇到如上錯誤的不妨試一試,不過上面的錯誤99%都是密鑰沒有生成對

  

  大概就這么多內容了,由於我也是剛接觸NIO,對HTTP什么的也不太懂,說錯的地方希望各位不要背后笑話我,而是寫在評論區,謝謝。

 

  除了上面文章中的引用,還要感謝以下文章的作者:

  netty中實現雙向認證的SSL連接(有源碼喲)

  netty源碼分析系列文章

  以及 學習Android客戶端和服務器端SSLSocket交互的總結 (這個找不到出處了)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM