Java通過SSLEngine與NIO實現HTTPS訪問的操作方法

Java使用NIO進行HTTPS協議訪問的時候,離不開SSLContext和SSLEngine兩個類。我們隻需要在Connect操作、Connected操作、Read和Write操作中加入SSL相關的處理即可。

一、連接服務器之前先初始化SSLContext並設置證書相關的操作。

public void Connect(String host, int port) {
     mSSLContext = this.InitSSLContext();
     super.Connect(host, port);  
 }

在連接服務器前先創建SSLContext對象,並進行證書相關的設置。如果服務器不是使用外部公認的認證機構生成的密鑰,可以使用基於公鑰CA的方式進行設置證書。如果是公認的認證證書一般隻需要加載Java KeyStore即可。

1.1 基於公鑰CA

public SSLContext InitSSLContext() throws NoSuchAlgorithmException{
  // 創建生成x509證書的對象
  CertificateFactory caf = CertificateFactory.getInstance("X.509");
  // 這裡的CA_PATH是服務器的ca證書,可以通過瀏覽器保存Cer證書(Base64和DER都可以)
  X509Certificate ca = (X509Certificate)caf.generateCertificate(new FileInputStream(CA_PATH));
  KeyStore caKs = KeyStore.getInstance("JKS");
  caKs.load(null, null);
  // 將上面創建好的證書設置到倉庫裡面,前面的`baidu-ca`隻是一個別名可以任意不要出現重復即可。
  caKs.setCertificateEntry("baidu-ca", ca);
  TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
      tmf.init(caKs);
  // 最後創建SSLContext,將可信任證書列表傳入。
  SSLContext context = SSLContext.getInstance("TLSv1.2");
  context.init(null, tmf.getTrustManagers(), null);
  return context;
}

1.2 加載Java KeyStore

public SSLContext InitSSLContext() throws NoSuchAlgorithmException{
  // 加載java keystore 倉庫
  KeyStore caKs = KeyStore.getInstance("JKS");
  // 把生成好的jks證書加載進來
  caKs.load(new FileInputStream(CA_PATH), PASSWORD.toCharArray());
  // 把加載好的證書放入信任的列表
  TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
  tmf.init(caKs);
  // 最後創建SSLContext,將可信任證書列表傳入。
  SSLContext context = SSLContext.getInstance("TLSv1.2");
  context.init(null, tmf.getTrustManagers(), null);
  return context;
}

二、連接服務器成功後,需要創建SSLEngine對象,並進行相關設置與握手處理。

通過第一步生成的SSLContext創建SSLSocketFactory並將當前的SocketChannel進行綁定(註:很多別人的例子都沒有這步操作,如果隻存在一個HTTPS的連接理論上沒有問題,但如果希望同時創建大量的HTTPS請求“可能”有問題,因為SSLEngine內部使用哪個Socket進行操作數據是不確定,如果我的理解有誤歡迎指正)。

然後調用創建SSLEngine對象,並初始化操作數據的Buffer,然後開始進入握手階段。(註:這裡創建的Buffer主要用於將應用層數據加密為網絡數據,將網絡數據解密為應用層數據使用:“密文與明文”)。

public final void OnConnected() {
  super.OnConnected();
  // 設置socket,並創建SSLEngine,開始握手
  SSLSocketFactory fx = mSSLContext.getSocketFactory();
  // 這裡將自己的channel傳進去
  fx.createSocket(mSocketChannel.GetSocket(), mHost, mPort, false);
  mSSLEngine = this.InitSSLEngine(mSSLContext);
  // 初始化使用的BUFFER
  int appBufSize = mSSLEngine.getSession().getApplicationBufferSize();
  int netBufSize = mSSLEngine.getSession().getPacketBufferSize();
  mAppDataBuf = ByteBuffer.allocate(appBufSize);
  mNetDataBuf = ByteBuffer.allocate(netBufSize);
  pAppDataBuf = ByteBuffer.allocate(appBufSize);
  pNetDataBuf = ByteBuffer.allocate(netBufSize);
  // 初始化完成,準備開啟握手
  mSSLInitiated = true;
  mSSLEngine.beginHandshake();
  this.ProcessHandShake(null);
}

三、進行握手操作

下圖簡單展示瞭握手流程,由客戶端發起,通過一些列的數據交換最終完成握手操作。要成功與服務器建立連接,握手流程是非常重要的環節,幸好SSEngine內部已經實現瞭證書驗證、交換等步驟,我們隻需要在其上層執行特定的行為(握手狀態處理)。

3.1 握手相關狀態(來自getHandshakeStatus方法)

NEED_WRAP當前握手狀態表示需要加密數據,即將要發送的應用層數據加密輸出為網絡層數據,並執行發送操作。

NEED_UNWRAP當前握手狀態表示需要對數據進行解密,即將收到的網絡層數據解密後成應用層數據。

NEED_TASK當前握手狀態表示需要執行任務,因為有些操作可能比較耗時,如果不希望造成阻塞流程就需要開啟異步任務進行執行。

FINISHED當前握手已完成

NOT_HANDSHAKING表示不需要握手,這個主要是再次連接時,為瞭加快速度而跳過握手流程。

3.2處理握手的方法

以下代碼展示瞭握手流程中的各種狀態的處理,主要的邏輯就是如果需要加密就執行加密操作,如果需要執行解密就執行解密操作(廢話@_@!)。

protected void ProcessHandShake(SSLEngineResult result){
 if(this.isClosed() || this.isShutdown()) return;
 // 區分是來此WRAP UNWRAP調用,還是其他調用
 SSLEngineResult.HandshakeStatus status;
 if(result != null){
  status = result.getHandshakeStatus();
 }else{
  status = mSSLEngine.getHandshakeStatus();
 }
 switch(status)
 {
  // 需要加密
  case NEED_WRAP:
      //判斷isOutboundDone,當true時,說明已經不需要再處理任何的NEED_WRAP操作瞭.
      // 因為已經顯式調用過closeOutbound,且就算執行wrap,
      // SSLEngineReulst.STATUS也一定是CLOSED,沒有任何意義
      if(mSSLEngine.isOutboundDone()){
        // 如果還有數據則發送出去
        if(mNetDataBuf.position() > 0) {
            mNetDataBuf.flip();
            mSocketChannel.WriteAndFlush(mNetDataBuf);
        }
        break;
      }
      // 執行加密流程
      this.ProcessWrapEvent();
      break;
  // 需要解密
  case NEED_UNWRAP:
   //判斷inboundDone是否為true, true說明peer端發送瞭close_notify,
   // peer發送瞭close_notify也可能被unwrap操作捕獲到,結果就是返回的CLOSED
   if(mSSLEngine.isInboundDone()){
    //peer端發送關閉,此時需要判斷是否調用closeOutbound
    if(mSSLEngine.isOutboundDone()){
     return;
    }
    mSSLEngine.closeOutbound();
   }
   break;
  case NEED_TASK:
   // 執行異步任務,我這裡是同步執行的,可以弄一個異步線程池進行。
   Runnable task = mSSLEngine.getDelegatedTask();
   if(task != null){
    task.run();
    // executor.execute(task); 這樣使用異步也是可以的,
    //但是異步就需要對ProcessHandShake的調用做特殊處理,因為異步的,像下面這直接是會導致瘋狂調用。
   }
   this.ProcessHandShake(null);  // 繼續處理握手
   break;
  case FINISHED:
   // 握手完成
   mHandshakeCompleted = true;
   this.OnHandCompleted();
   return;
  case NOT_HANDSHAKING:
   // 不需要握手
   if(!mHandshakeCompleted)
   {
    mHandshakeCompleted = true;
    this.OnHandCompleted();
   }
   return;
 }
}

四、數據的發送與接收

握手成功後就可以進行正常的數據發送與接收,但是需要額外在數據發送的時候進行加密操作,數據接收後進行解密操作。

這裡需要額外說明一下,在握手期間也是會需要讀取數據的,因為服務器發送過來的數據需要我們執行讀取並解密操作。而這個操作在一些其他的例子中直接使用瞭阻塞的讀取方式,我這裡則是放在OnRead事件調用後進行處理,這樣才符合NIO模型。

4.1加密操作(SelectionKey.OP_WRITE)

protected void ProcessWrapEvent(){
 if(this.isClosed() || this.isShutdown()) return;
 SSLEngineResult result = mSSLEngine.wrap(mAppDataBuf, mNetDataBuf);
 // 處理result
 if(ProcessSSLStatus(result, true)){
  mNetDataBuf.flip();
  mSocketChannel.WriteAndFlush(mNetDataBuf);
  // 發完成後清空buffer
  mNetDataBuf.clear();
 }
 mAppDataBuf.clear();
 // 如果沒有握手完成,則繼續調用握手處理
 if(!mHandshakeCompleted)
   this.ProcessHandShake(result);
}

4.2 解密操作(SelectionKey.OP_READ)

protected void ProcessUnWrapEvent(){
 if(this.isClosed() || this.isShutdown()) return;
 do{
  // 執行解密操作
  SSLEngineResult res = mSSLEngine.unwrap(pNetDataBuf, pAppDataBuf);
  if(!ProcessSSLStatus(res, false))
      // 這裡不需要對`pNetDataBuf`進行處理,因為ProcessSSLStatus裡面已經做好處理瞭。
   return;
  if(res.getStatus() == Status.CLOSED)
   break;
  // 未完成握手時,需要繼續調用握手處理
  if(!mHandshakeCompleted)
   this.ProcessHandShake(res);
 }while(pNetDataBuf.hasRemaining());
 // 數據都解密完瞭,這個就可以清空瞭。
 if(!pNetDataBuf.hasRemaining())
   pNetDataBuf.clear();
}

到此這篇關於Java通過SSLEngine與NIO實現HTTPS訪問的文章就介紹到這瞭,更多相關Java通過SSLEngine與NIO實現HTTPS訪問內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: