在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
有人遇到這個錯誤,升級到1.8就好了,然而我是1.7JDK仍然管用,升級JDK這個解決方案看起來相當靠譜,遇到如上錯誤的不妨試一試,不過上面的錯誤99%都是密鑰沒有生成對
大概就這么多內容了,由於我也是剛接觸NIO,對HTTP什么的也不太懂,說錯的地方希望各位不要背后笑話我,而是寫在評論區,謝謝。
除了上面文章中的引用,還要感謝以下文章的作者:
netty中實現雙向認證的SSL連接(有源碼喲)
以及 學習Android客戶端和服務器端SSLSocket交互的總結 (這個找不到出處了)