Java通過SSLEngine與NIO實現HTTPS訪問


    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 }

 

     文章來自我的公眾號,大家如果有興趣可以關注,具體掃描關注下圖。


免責聲明!

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



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