Java通過SSLEngine與NIO實現HTTPS訪問

爆走de蘿蔔發表於2021-08-20

    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 }

 

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

Java通過SSLEngine與NIO實現HTTPS訪問

相關文章