Java使用NIO進行HTTPS協議訪問的時候,離不開SSLContext和SSLEngine兩個類。我們只需要在Connect操作、Connected操作、Read和Write操作中加入SSL相關的處理即可。
一、連線伺服器之前先初始化SSLContext並設定證照相關的操作。
1 public void Connect(String host, int port) { 2 mSSLContext = this.InitSSLContext(); 3 super.Connect(host, port); 4 }
在連線伺服器前先建立SSLContext物件,並進行證照相關的設定。如果伺服器不是使用外部公認的認證機構生成的金鑰,可以使用基於公鑰CA的方式進行設定證照。如果是公認的認證證照一般只需要載入Java KeyStore即可。
1.1 基於公鑰CA
1 public SSLContext InitSSLContext() throws NoSuchAlgorithmException{ 2 // 建立生成x509證照的物件 3 CertificateFactory caf = CertificateFactory.getInstance("X.509"); 4 // 這裡的CA_PATH是伺服器的ca證照,可以通過瀏覽器儲存Cer證照(Base64和DER都可以) 5 X509Certificate ca = (X509Certificate)caf.generateCertificate(new FileInputStream(CA_PATH)); 6 KeyStore caKs = KeyStore.getInstance("JKS"); 7 caKs.load(null, null); 8 // 將上面建立好的證照設定到倉庫裡面,前面的`baidu-ca`只是一個別名可以任意不要出現重複即可。 9 caKs.setCertificateEntry("baidu-ca", ca); 10 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 11 tmf.init(caKs); 12 // 最後建立SSLContext,將可信任證照列表傳入。 13 SSLContext context = SSLContext.getInstance("TLSv1.2"); 14 context.init(null, tmf.getTrustManagers(), null); 15 return context; 16 }
1.2 載入Java KeyStore
1 public SSLContext InitSSLContext() throws NoSuchAlgorithmException{ 2 // 載入java keystore 倉庫 3 KeyStore caKs = KeyStore.getInstance("JKS"); 4 // 把生成好的jks證照載入進來 5 caKs.load(new FileInputStream(CA_PATH), PASSWORD.toCharArray()); 6 // 把載入好的證照放入信任的列表 7 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 8 tmf.init(caKs); 9 // 最後建立SSLContext,將可信任證照列表傳入。 10 SSLContext context = SSLContext.getInstance("TLSv1.2"); 11 context.init(null, tmf.getTrustManagers(), null); 12 return context; 13 }
二、連線伺服器成功後,需要建立SSLEngine物件,並進行相關設定與握手處理。
通過第一步生成的SSLContext建立SSLSocketFactory並將當前的SocketChannel進行繫結(注:很多別人的例子都沒有這步操作,如果只存在一個HTTPS的連線理論上沒有問題,但如果希望同時建立大量的HTTPS請求“可能”有問題,因為SSLEngine內部使用哪個Socket進行運算元據是不確定,如果我的理解有誤歡迎指正)。
然後呼叫建立SSLEngine物件,並初始化運算元據的Buffer,然後開始進入握手階段。(注:這裡建立的Buffer主要用於將應用層資料加密為網路資料,將網路資料解密為應用層資料使用:“密文與明文”)。
1 public final void OnConnected() { 2 super.OnConnected(); 3 // 設定socket,並建立SSLEngine,開始握手 4 SSLSocketFactory fx = mSSLContext.getSocketFactory(); 5 // 這裡將自己的channel傳進去 6 fx.createSocket(mSocketChannel.GetSocket(), mHost, mPort, false); 7 mSSLEngine = this.InitSSLEngine(mSSLContext); 8 // 初始化使用的BUFFER 9 int appBufSize = mSSLEngine.getSession().getApplicationBufferSize(); 10 int netBufSize = mSSLEngine.getSession().getPacketBufferSize(); 11 mAppDataBuf = ByteBuffer.allocate(appBufSize); 12 mNetDataBuf = ByteBuffer.allocate(netBufSize); 13 pAppDataBuf = ByteBuffer.allocate(appBufSize); 14 pNetDataBuf = ByteBuffer.allocate(netBufSize); 15 // 初始化完成,準備開啟握手 16 mSSLInitiated = true; 17 mSSLEngine.beginHandshake(); 18 this.ProcessHandShake(null); 19 }
三、進行握手操作
下圖簡單展示了握手流程,由客戶端發起,通過一些列的資料交換最終完成握手操作。要成功與伺服器建立連線,握手流程是非常重要的環節,幸好SSEngine內部已經實現了證照驗證、交換等步驟,我們只需要在其上層執行特定的行為(握手狀態處理)。
3.1 握手相關狀態(來自getHandshakeStatus方法)
NEED_WRAP 當前握手狀態表示需要加密資料,即將要傳送的應用層資料加密輸出為網路層資料,並執行傳送操作。
NEED_UNWRAP 當前握手狀態表示需要對資料進行解密,即將收到的網路層資料解密後成應用層資料。
NEED_TASK 當前握手狀態表示需要執行任務,因為有些操作可能比較耗時,如果不希望造成阻塞流程就需要開啟非同步任務進行執行。
FINISHED 當前握手已完成
NOT_HANDSHAKING 表示不需要握手,這個主要是再次連線時,為了加快速度而跳過握手流程。
3.2 處理握手的方法
以下程式碼展示了握手流程中的各種狀態的處理,主要的邏輯就是如果需要加密就執行加密操作,如果需要執行解密就執行解密操作(廢話@_@!)。
1 protected void ProcessHandShake(SSLEngineResult result){ 2 if(this.isClosed() || this.isShutdown()) return; 3 // 區分是來此WRAP UNWRAP呼叫,還是其他呼叫 4 SSLEngineResult.HandshakeStatus status; 5 if(result != null){ 6 status = result.getHandshakeStatus(); 7 }else{ 8 status = mSSLEngine.getHandshakeStatus(); 9 } 10 switch(status) 11 { 12 // 需要加密 13 case NEED_WRAP: 14 //判斷isOutboundDone,當true時,說明已經不需要再處理任何的NEED_WRAP操作了. 15 // 因為已經顯式呼叫過closeOutbound,且就算執行wrap, 16 // SSLEngineReulst.STATUS也一定是CLOSED,沒有任何意義 17 if(mSSLEngine.isOutboundDone()){ 18 // 如果還有資料則傳送出去 19 if(mNetDataBuf.position() > 0) { 20 mNetDataBuf.flip(); 21 mSocketChannel.WriteAndFlush(mNetDataBuf); 22 } 23 break; 24 } 25 // 執行加密流程 26 this.ProcessWrapEvent(); 27 break; 28 // 需要解密 29 case NEED_UNWRAP: 30 //判斷inboundDone是否為true, true說明peer端傳送了close_notify, 31 // peer傳送了close_notify也可能被unwrap操作捕獲到,結果就是返回的CLOSED 32 if(mSSLEngine.isInboundDone()){ 33 //peer端傳送關閉,此時需要判斷是否呼叫closeOutbound 34 if(mSSLEngine.isOutboundDone()){ 35 return; 36 } 37 mSSLEngine.closeOutbound(); 38 } 39 break; 40 case NEED_TASK: 41 // 執行非同步任務,我這裡是同步執行的,可以弄一個非同步執行緒池進行。 42 Runnable task = mSSLEngine.getDelegatedTask(); 43 if(task != null){ 44 task.run(); 45 // executor.execute(task); 這樣使用非同步也是可以的, 46 //但是非同步就需要對ProcessHandShake的呼叫做特殊處理,因為非同步的,像下面這直接是會導致瘋狂呼叫。 47 } 48 this.ProcessHandShake(null); // 繼續處理握手 49 break; 50 case FINISHED: 51 // 握手完成 52 mHandshakeCompleted = true; 53 this.OnHandCompleted(); 54 return; 55 case NOT_HANDSHAKING: 56 // 不需要握手 57 if(!mHandshakeCompleted) 58 { 59 mHandshakeCompleted = true; 60 this.OnHandCompleted(); 61 } 62 return; 63 } 64 }
四、資料的傳送與接收
握手成功後就可以進行正常的資料傳送與接收,但是需要額外在資料傳送的時候進行加密操作,資料接收後進行解密操作。
這裡需要額外說明一下,在握手期間也是會需要讀取資料的,因為伺服器傳送過來的資料需要我們執行讀取並解密操作。而這個操作在一些其他的例子中直接使用了阻塞的讀取方式,我這裡則是放在OnRead事件呼叫後進行處理,這樣才符合NIO模型。
4.1 加密操作(SelectionKey.OP_WRITE)
1 protected void ProcessWrapEvent(){ 2 if(this.isClosed() || this.isShutdown()) return; 3 SSLEngineResult result = mSSLEngine.wrap(mAppDataBuf, mNetDataBuf); 4 // 處理result 5 if(ProcessSSLStatus(result, true)){ 6 mNetDataBuf.flip(); 7 mSocketChannel.WriteAndFlush(mNetDataBuf); 8 // 發完成後清空buffer 9 mNetDataBuf.clear(); 10 } 11 mAppDataBuf.clear(); 12 // 如果沒有握手完成,則繼續呼叫握手處理 13 if(!mHandshakeCompleted) 14 this.ProcessHandShake(result); 15 }
4.2 解密操作(SelectionKey.OP_READ)
1 protected void ProcessUnWrapEvent(){ 2 if(this.isClosed() || this.isShutdown()) return; 3 do{ 4 // 執行解密操作 5 SSLEngineResult res = mSSLEngine.unwrap(pNetDataBuf, pAppDataBuf); 6 if(!ProcessSSLStatus(res, false)) 7 // 這裡不需要對`pNetDataBuf`進行處理,因為ProcessSSLStatus裡面已經做好處理了。 8 return; 9 if(res.getStatus() == Status.CLOSED) 10 break; 11 // 未完成握手時,需要繼續呼叫握手處理 12 if(!mHandshakeCompleted) 13 this.ProcessHandShake(res); 14 }while(pNetDataBuf.hasRemaining()); 15 // 資料都解密完了,這個就可以清空了。 16 if(!pNetDataBuf.hasRemaining()) 17 pNetDataBuf.clear(); 18 }
文章來自我的公眾號,大家如果有興趣可以關注,具體掃描關注下圖。