如何讓你的傳輸更安全——NIO 模式和 BIO 模式實現 SSL 協議通訊

超人汪小建發表於2017-06-09

對於SSL/TLS協議,如果要每個開發者都自己去實現顯然會帶來不必要的麻煩,正是為了解決這個問題Java為廣大開發者提供了Java安全套接字擴充套件——JSSE,它包含了實現Internet安全通訊的一系列包的集合,是SSL和TLS的純Java實現,同時它是一個開放的標準,每個公司都可以自己實現JSSE,通過它可以透明地提供資料加密、伺服器認證、資訊完整性等功能,就像使用普通的套接字一樣使用安全套接字,大大減輕了開發者的負擔,使開發者可以很輕鬆將SSL協議整合到程式中,並且JSSE能將安全隱患降到了最低點。

在用JSSE實現SSL通訊過程中主要會遇到以下類和介面,由於過程中涉及到加解密、金鑰生成等運算的框架和實現,所以也會間接用到JCE包的一些類。如圖為JSSE介面的主要類圖:

如何讓你的傳輸更安全——NIO 模式和 BIO 模式實現 SSL 協議通訊

① 通訊核心類——SSLSocket和SSLServerSocket。對於使用過socket進行通訊開發的朋友比較好理解,它們對應的就是Socket與ServerSocket,只是表示實現了SSL協議的Socket和ServerSocket,同時它們也是Socket與ServerSocket的子類。SSLSocket負責的事情包括設定加密套件、管理SSL會話、處理握手結束時間、設定客戶端模式或伺服器模式。
② 客戶端與伺服器端Socket工廠——SSLSocketFactory和SSLServerSocketFactory。在設計模式中工廠模式是專門用於生產出需要的例項,這裡也是把SSLSocket、SSLServerSocket物件建立的工作交給這兩個工廠類。
③ SSL會話——SSLSession。安全通訊握手過程需要一個會話,為了提高通訊的效率,SSL協議允許多個SSLSocket共享同一個SSL會話,在同一個會話中,只有第一個開啟的SSLSocket需要進行SSL握手,負責生成金鑰及交換金鑰,其餘SSLSocket都共享金鑰資訊。
④ SSL上下文——SSLContext。它是對整個SSL/TLS協議的封裝,表示了安全套接字協議的實現。主要負責設定安全通訊過程中的各種資訊,例如跟證照相關的資訊。並且負責構建SSLSocketFactory、SSLServerSocketFactory和SSLEngine等工廠類。
⑤ SSL非阻塞引擎——SSLEngine。假如你要進行NIO通訊,那麼將使用這個類,它讓通過過程支援非阻塞的安全通訊。
⑥ 金鑰管理器——KeyManager。此介面負責選擇用於證實自己身份的安全證照,發給通訊另一方。KeyManager物件由KeyManagerFactory工廠類生成。
⑦ 信任管理器——TrustManager。此介面負責判斷決定是否信任對方的安全證照,TrustManager物件由TrustManagerFactory工廠類生成。
⑧ 金鑰證照儲存設施——KeyStore。這個物件用於存放安全證照,安全證照一般以檔案形式存放,KeyStore負責將證照載入到記憶體。

通過上面這些類就可以完成SSL協議的安全通訊了,在利用SSL/TLS進行安全通訊時,客戶端跟伺服器端都必須要支援SSL/TLS協議,不然將無法進行通訊。而且客戶端和伺服器端都可能要設定用於證實自己身份的安全證照,並且還要設定信任對方的哪些安全證照。

關於身份認證方面有個名詞叫客戶端模式,一般情況客戶端要對伺服器端的身份進行驗證,但是無需向伺服器證實自己的身份,這樣不用向對方證實自己身份的通訊端我們就說它處於客戶模式,否則成它處於伺服器模式。SSLSocket的setUseClientMode(Boolean mode)方法可以設定客戶端模式或伺服器模式。

BIO模式實現SSL通訊

使用BIO模式實現SSL通訊除了對一些證照金鑰生成外,只需使用JDK自帶的SSLServerSocket和SSLSocket等相關類的API即可實現,簡潔直觀。

① 解決證照問題。

一般而言作為伺服器端必須要有證照以證明這個伺服器的身份,並且證照應該描述此伺服器所有者的一些基本資訊,例如公司名稱、聯絡人名等。證照由所有人以密碼形式簽名,基本不可偽造,證照獲取的途徑有兩個:一是從權威機構購買證照,權威機構擔保它發出的證照的真實性,而且這個權威機構被大家所信任,進而你可以相信這個證照的有效性;另外一個是自己用JDK提供的工具keytool建立一個自我簽名的證照,這種情況下一般是我只想要保證資料的安全性與完整性,避免資料在傳送的過程中被竊聽或篡改,此時身份的認證已不重要,重點已經在端與端傳輸的祕密性上,證照的作用只體現在加解密簽名。

另外,關於證照的一些概念在這裡陳述,一個證照是一個實體的數字簽名,這個實體可以是一個人、一個組織、一個程式、一個公司、一個銀行,同時證照還包含這個實體的公共鑰匙,此公共鑰匙是這個實體的數字關聯,讓所有想同這個實體發生信任關係的其他實體用來檢驗簽名。而這個實體的數字簽名是實體資訊用實體的私鑰加密後的資料,這條資料可以用這個實體的公共鑰匙解密,進而鑑別實體的身份。這裡用到的核心演算法是非對稱加密演算法。

SSL協議通訊涉及金鑰儲存的檔案格式比較多,很容易搞混,例如xxx.cer、xxx.pfx、xxx.jks、xxx.keystore、xxx.truststore等格式檔案。如圖,搞清楚他們有助於理解後面的程式,.cer格式檔案俗稱證照,但這個證照中沒有私鑰,只包含了公鑰;.pfx格式檔案也稱為證照,它一般供瀏覽器使用,而且它不僅包含了公鑰,還包含了私鑰,當然這個私鑰是加密的,不輸入密碼是解不了密的;.jks格式檔案表示java金鑰儲存器(java key store),它可以同時容納N個公鑰跟私鑰,是一個金鑰庫;.keystore格式檔案其實跟.jks基本是一樣的,只是不同公司叫法不太一樣,預設生成的證照儲存庫格式;.truststore格式檔案表示信任證照儲存庫,它僅僅包含了通訊對方的公鑰,當然你可以直接把通訊對方的jks作為信任庫(就算如此你也只能知道通訊對方的公鑰,要知道金鑰都是加密的,你無從獲取,只要演算法不被破解)。有些時候我們需要把pfx或cert轉化為jks以便於用java進行ssl通訊,例如一個銀行只提供了pfx證照,而我們想用java進行ssl通訊時就要將pfx轉化為jks格式。

如何讓你的傳輸更安全——NIO 模式和 BIO 模式實現 SSL 協議通訊

按照理論上,我們一共需要準備四個檔案,兩個keystore檔案和兩個truststore檔案,通訊雙方分別擁有一個keystore和一個truststore,keystore用於存放自己的金鑰和公鑰,truststore用於存放所有需要信任方的公鑰。這裡為了方便直接使用jks即keystore替代truststore(免去證照導來導去),因為對方的keystore包含了自己需要的信任公鑰。

下面使用jdk自帶的工具分別生成伺服器端證照,通過如下命令並輸入姓名、組織單位名稱、組織名稱、城市、省份、國家資訊即可生成證照密碼為tomcat的證照,此證照存放在密碼也為tomcat的tomcat.jks證照儲存庫中。如果你繼續建立證照將繼續往tomcat.jks證照儲存庫中新增證照。如果你僅僅輸入keytool -genkey -alias tomcat -keyalg RSA -keypass tomcat -storepass tomcat,不指定證照儲存庫的檔名及路徑,則工具會在使用者的home directory目錄下生產一個“.keystore”檔案作為證照儲存庫。

如何讓你的傳輸更安全——NIO 模式和 BIO 模式實現 SSL 協議通訊

類似的,客戶端證照也用此方式進行生成。如下

如何讓你的傳輸更安全——NIO 模式和 BIO 模式實現 SSL 協議通訊

② 服務端TomcatSSLServer.java

public class TomcatSSLServer {
  private static final String SSL_TYPE = "SSL";
  private static final String KS_TYPE = "JKS";
  private static final String X509 = "SunX509";
  private final static int PORT = 443;
  private static TomcatSSLServer sslServer;
  private SSLServerSocket svrSocket;

  public static TomcatSSLServer getInstance() throws Exception {
    if (sslServer == null) {
      sslServer = new TomcatSSLServer();
    }
    return sslServer;
  }

  private TomcatSSLServer() throws Exception {
    SSLContext sslContext = createSSLContext();
    SSLServerSocketFactory serverFactory = sslContext.getServerSocketFactory();
    svrSocket = (SSLServerSocket) serverFactory.createServerSocket(PORT);
    svrSocket.setNeedClientAuth(true);
    String[] supported = svrSocket.getEnabledCipherSuites();
    svrSocket.setEnabledCipherSuites(supported);
  }

  private SSLContext createSSLContext() throws Exception {
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(X509);
    String serverKeyStoreFile = "c:\\tomcat.jks";
    String svrPassphrase = "tomcat";
    char[] svrPassword = svrPassphrase.toCharArray();
    KeyStore serverKeyStore = KeyStore.getInstance(KS_TYPE);
    serverKeyStore.load(new FileInputStream(serverKeyStoreFile), svrPassword);
    kmf.init(serverKeyStore, svrPassword);
    String clientKeyStoreFile = "c:\\client.jks";
    String cntPassphrase = "client";
    char[] cntPassword = cntPassphrase.toCharArray();
    KeyStore clientKeyStore = KeyStore.getInstance(KS_TYPE);
    clientKeyStore.load(new FileInputStream(clientKeyStoreFile), cntPassword);
    tmf.init(clientKeyStore);
    SSLContext sslContext = SSLContext.getInstance(SSL_TYPE);
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
    return sslContext;
  }

  public void startService() {
    SSLSocket cntSocket = null;
    BufferedReader ioReader = null;
    PrintWriter ioWriter = null;
    String tmpMsg = null;
    while (true) {
      try {
        cntSocket = (SSLSocket) svrSocket.accept();
        ioReader = new BufferedReader(new InputStreamReader(cntSocket.getInputStream()));
        ioWriter = new PrintWriter(cntSocket.getOutputStream());
        while ((tmpMsg = ioReader.readLine()) != null) {
          System.out.println("客戶端通過SSL協議傳送資訊:" + tmpMsg);
          tmpMsg = "歡迎通過SSL協議連線";
          ioWriter.println(tmpMsg);
          ioWriter.flush();
        }
      } catch (IOException e) {
        e.printStackTrace();
      } finally {
        try {
          if (cntSocket != null) cntSocket.close();
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    }
  }

  public static void main(String[] args) throws Exception {
    TomcatSSLServer.getInstance().startService();
  }
}複製程式碼

基本順序是先得到一個SSLContext例項,再對SSLContext例項進行初始化,金鑰管理器及信任管理器作為引數傳入,證照管理器及信任管理器按照指定的金鑰儲存器路徑和密碼進行載入。接著設定支援的加密套件,最後讓SSLServerSocket開始監聽客戶端傳送過來的訊息。

③ 客戶端TomcatSSLClient.java

public class TomcatSSLClient {
  private static final String SSL_TYPE = "SSL";
  private static final String X509 = "SunX509";
  private static final String KS_TYPE = "JKS";
  private SSLSocket sslSocket;

  public TomcatSSLClient(String targetHost, int port) throws Exception {
    SSLContext sslContext = createSSLContext();
    SSLSocketFactory sslcntFactory = (SSLSocketFactory) sslContext.getSocketFactory();
    sslSocket = (SSLSocket) sslcntFactory.createSocket(targetHost, port);
    String[] supported = sslSocket.getSupportedCipherSuites();
    sslSocket.setEnabledCipherSuites(supported);
  }

  private SSLContext createSSLContext() throws Exception {
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(X509);
    String clientKeyStoreFile = "c:\\client.jks";
    String cntPassphrase = "client";
    char[] cntPassword = cntPassphrase.toCharArray();
    KeyStore clientKeyStore = KeyStore.getInstance(KS_TYPE);
    clientKeyStore.load(new FileInputStream(clientKeyStoreFile), cntPassword);
    String serverKeyStoreFile = "c:\\tomcat.jks";
    String svrPassphrase = "tomcat";
    char[] svrPassword = svrPassphrase.toCharArray();
    KeyStore serverKeyStore = KeyStore.getInstance(KS_TYPE);
    serverKeyStore.load(new FileInputStream(serverKeyStoreFile), svrPassword);
    kmf.init(clientKeyStore, cntPassword);
    tmf.init(serverKeyStore);
    SSLContext sslContext = SSLContext.getInstance(SSL_TYPE);
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
    return sslContext;
  }

  public String sayToSvr(String sayMsg) throws IOException {
    BufferedReader ioReader = new BufferedReader(new InputStreamReader(sslSocket.getInputStream()));
    PrintWriter ioWriter = new PrintWriter(sslSocket.getOutputStream());
    ioWriter.println(sayMsg);
    ioWriter.flush();
    return ioReader.readLine();
  }

  public static void main(String[] args) throws Exception {
    TomcatSSLClient sslSocket = new TomcatSSLClient("127.0.0.1", 443);
    BufferedReader ioReader = new BufferedReader(new InputStreamReader(System.in));
    String sayMsg = "";
    String svrRespMsg = "";
    while ((sayMsg = ioReader.readLine()) != null) {
      svrRespMsg = sslSocket.sayToSvr(sayMsg);
      if (svrRespMsg != null && !svrRespMsg.trim().equals("")) {
        System.out.println("伺服器通過SSL協議響應:" + svrRespMsg);
      }
    }
  }
}複製程式碼

客戶端的前面操作基本跟伺服器端的一樣,先建立一個SSLContext例項,再用金鑰管理器及信任管理器對SSLContext進行初始化,當然這裡金鑰儲存的路徑是指向客戶端的client.jks。接著設定加密套件,最後使用SSLSocket進行通訊。

注意伺服器端有行程式碼svrSocket.setNeedClientAuth(true);它是非常重要的一個設定方法,用於設定是否驗證客戶端的身份。假如我們把它註釋掉或設定為false,此時客戶端將不再需要自己的金鑰管理器,即伺服器不需要通過client.jks對客戶端的身份進行驗證,把金鑰管理器直接設定為null也可以跟伺服器端進行通訊。

最後談談信任管理器,它的職責是決定是否信任遠端的證照,那麼它憑藉什麼去判斷呢?如果不顯式設定信任儲存器的檔案路徑,將遵循如下規則:①如果系統屬性javax.NET.ssl.truststore指定了truststore檔案,那麼信任管理器將去jre路徑下的lib/security目錄尋找這個檔案作為信任儲存器;②如果沒設定①中的系統屬性,則去尋找一個%java_home%/lib/security/jssecacerts檔案作為信任儲存器;③如果jssecacerts不存在而cacerts存在,則cacerts作為信任儲存器。

至此,一個利用JSSE實現BIO模式的SSL協議通訊的例子已完成。

NIO模式實現SSL通訊

在jdk1.5之前,由於網際網路還沒快速發展起來,對於常見的應用使用BIO模式即可滿足需求,而這時jdk的JSSE介面也僅僅只是提供了基於流的安全套接字,但隨著網路的發展,BIO模型明顯已經不足以滿足一些高併發多連線接入的場景,體現在機器上就是要不同的執行緒模型以至於能最大程度地壓榨計算器的運算,於是此時引入了NIO模式,原來基於流的阻塞模式IO只需使用SSLServerSocket和SSLSocket即可完成SSL通訊,而JDK中對於NIO模式並沒有提供與之對應的“SSLServerSocketChannel”和“SSLSocketChannel”,這是由NIO模式決定的,很難設計一個“SSLServerSocketChannel”類與Selector互動,強行地引入將帶來更多的問題,這更像解決一個問題引入了三個問題,並且還會導致API更加複雜,另外Nio細節也不適合遮蔽,它應該由應用開發層去控制。所有的這些都決定了jdk不會也不能有NIO安全套接字。

jdk1.5為了支援NIO模式的SSL通訊,引入了SSLEngine引擎,它負責了底層ssl協議的握手、加密、解密、關閉會話等等操作,根據前面SSL\TLS協議章節我們知道SSL協議在握手階段會有十三個步驟,在握手過程中不會有應用層的資料傳輸,只有在握手認證完成後雙方才會進行應用層資料交換。大致把握手分為四階段:
①客戶端傳送hello訊息;
②服務端響應hello訊息且傳送附帶的認證訊息;
③客戶端向客戶端傳送證照和其他認證訊息;
④完成握手。

SSLEngine在握手過程中定義了五種HandshakeStatus狀態,【NEED_WRAP、NEED_UNWRAP、NEED_TASK、FINISHED、NOT_HANDSHAKING】,通過他們實現協議通訊過程中狀態管理,按照四個階段其中的狀態是這樣轉換的,剛開始它的狀態為NEED_UNWRAP,表示等待解包,讀取客戶端資料並解包後,把狀態置為NEED_WRAP,表示等待打包,打包完向客戶端響應資料後狀態又重置為NEED_UNWRAP,如此切換直至握手完成時狀態被置為FINISHED,表示握手已經完成,此後狀態置為NOT_HANDSHAKING,表示已經不在握手階段了。另外還有一個NEED_TASK狀態表示SSLEngine有額外的任務需要執行,而且這些任務都是比較耗時或者可能阻塞的,例如訪問金鑰檔案、連線遠端證照認證服務、金鑰管理器使用何種認證方式作為客戶端認證等等操作。為了保證NIO特性,這些操作不能直接由當前執行緒操作,當前執行緒只會把狀態改為NEED_TASK,後面處理執行緒會交由其他執行緒處理。

看看程式是如何使用nio模式進行ssl通訊的,主要看服務端如何實現。

public class NioSSLServer {
  private SSLEngine sslEngine;
  private Selector selector;
  private SSLContext sslContext;
  private ByteBuffer netInData;
  private ByteBuffer appInData;
  private ByteBuffer netOutData;
  private ByteBuffer appOutData;
  private static final String SSL_TYPE = "SSL";
  private static final String KS_TYPE = "JKS";
  private static final String X509 = "SunX509";
  private final static int PORT = 443;

  public void run() throws Exception {
    createServerSocket();
    createSSLContext();
    createSSLEngine();
    createBuffer();
    while (true) {
      selector.select();
      Iterator<SelectionKey> it = selector.selectedKeys().iterator();
      while (it.hasNext()) {
        SelectionKey selectionKey = it.next();
        it.remove();
        handleRequest(selectionKey);
      }
    }
  }

  private void createBuffer() {
    SSLSession session = sslEngine.getSession();
    appInData = ByteBuffer.allocate(session.getApplicationBufferSize());
    netInData = ByteBuffer.allocate(session.getPacketBufferSize());
    appOutData = ByteBuffer.wrap("Hello\n".getBytes());
    netOutData = ByteBuffer.allocate(session.getPacketBufferSize());
  }

  private void createSSLEngine() {
    sslEngine = sslContext.createSSLEngine();
    sslEngine.setUseClientMode(false);
  }

  private void createServerSocket() throws Exception {
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(false);
    selector = Selector.open();
    ServerSocket serverSocket = serverChannel.socket();
    serverSocket.bind(new InetSocketAddress(PORT));
    serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  }

  private void createSSLContext() throws Exception {
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(X509);
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(X509);
    String serverKeyStoreFile = "c:\\tomcat.jks";
    String svrPassphrase = "tomcat";
    char[] svrPassword = svrPassphrase.toCharArray();
    KeyStore serverKeyStore = KeyStore.getInstance(KS_TYPE);
    serverKeyStore.load(new FileInputStream(serverKeyStoreFile), svrPassword);
    kmf.init(serverKeyStore, svrPassword);
    String clientKeyStoreFile = "c:\\client.jks";
    String cntPassphrase = "client";
    char[] cntPassword = cntPassphrase.toCharArray();
    KeyStore clientKeyStore = KeyStore.getInstance(KS_TYPE);
    clientKeyStore.load(new FileInputStream(clientKeyStoreFile), cntPassword);
    tmf.init(clientKeyStore);
    sslContext = SSLContext.getInstance(SSL_TYPE);
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
  }

  private void handleRequest(SelectionKey key) throws Exception {
    if (key.isAcceptable()) {
      ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
      SocketChannel channel = ssc.accept();
      channel.configureBlocking(false);
      doHandShake(channel);
    } else if (key.isReadable()) {
      if (sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
        SocketChannel sc = (SocketChannel) key.channel();
        netInData.clear();
        appInData.clear();
        sc.read(netInData);
        netInData.flip();
        SSLEngineResult engineResult = sslEngine.unwrap(netInData, appInData);
        doTask();
        if (engineResult.getStatus() == SSLEngineResult.Status.OK) {
          appInData.flip();
          System.out.println(new String(appInData.array()));

        }
        sc.register(selector, SelectionKey.OP_WRITE);
      }
    } else if (key.isWritable()) {
      SocketChannel sc = (SocketChannel) key.channel();
      netOutData.clear();
      SSLEngineResult engineResult = sslEngine.wrap(appOutData, netOutData);
      doTask();
      netOutData.flip();
      while (netOutData.hasRemaining())
        sc.write(netOutData);
      sc.register(selector, SelectionKey.OP_READ);
    }
  }

  private void doHandShake(SocketChannel sc) throws IOException {
    boolean handshakeDone = false;
    sslEngine.beginHandshake();
    HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
    while (!handshakeDone) {
      switch (hsStatus) {
        case FINISHED:
          break;
        case NEED_TASK:
          hsStatus = doTask();
          break;
        case NEED_UNWRAP:
          netInData.clear();
          sc.read(netInData);
          netInData.flip();
          do {
            SSLEngineResult engineResult = sslEngine.unwrap(netInData, appInData);
            hsStatus = doTask();
          } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP
              && netInData.remaining() > 0);
          netInData.clear();
          break;
        case NEED_WRAP:
          SSLEngineResult engineResult = sslEngine.wrap(appOutData, netOutData);
          hsStatus = doTask();
          netOutData.flip();
          sc.write(netOutData);
          netOutData.clear();
          break;
        case NOT_HANDSHAKING:
          sc.configureBlocking(false);
          sc.register(selector, SelectionKey.OP_READ);
          handshakeDone = true;
          break;
      }
    }
  }

  private HandshakeStatus doTask() {
    Runnable task;
    while ((task = sslEngine.getDelegatedTask()) != null) {
      new Thread(task).start();
    }
    return sslEngine.getHandshakeStatus();
  }

  public static void main(String[] args) throws Exception {
    new NioSSLServer().run();
  }
}複製程式碼

根據程式大致說明程式過程,
①建立用於非阻塞通訊的主要物件ServerSocketChannel和Selector、繫結埠、註冊接收事件;
②建立SSL上下文,此過程主要是根據前面建立好的金鑰儲存器tomcat.jks和client.jks去建立金鑰管理器和信任管理器,並通過金鑰管理器和信任管理器去初始化SSL上下文;
③建立SSL引擎,主要通過SSL上下文建立SSL引擎,並將它設為不驗證客戶端身份;
④建立緩衝區,使用SSL協議通訊的過程中涉及到四個緩衝區,如下圖,netInData表示實際從網路接收到的位元組流,它是包含了SSL協議和應用資料的位元組流,通過SSLEngine引擎進行認證解密等處理後的應用可直接使用的資料則用appInData表示,同樣地,應用層要傳遞的資料為appOutData,而經過SSLEngine引擎認證加密處理後放到網路中傳輸的位元組流則為netOutData;
⑤接下去開始監聽處理客戶端的連線請求,一旦有可接受的連線則會先進行SSL協議握手,完成握手後才能進行傳輸,即對通道的讀寫操作。

握手操作是一個比較複雜的過程,必須要保證握手完成後才能進行應用層資料交換,所以這裡使用一個while迴圈不斷做握手操作直到完成。前面已經介紹了握手階段會有五種狀態,【NEED_WRAP、NEED_UNWRAP、NEED_TASK、FINISHED、NOT_HANDSHAKING】,由於SSL協議握手的報文都由SSLEngine引擎自動生成,所以我們只需對不同狀態做不同操作即可,例如,NEED_UNWRAP狀態則呼叫unwrap方法,NEED_WRAP則呼叫wrap方法,NEED_TASK則使用其他執行緒處理委託任務,握手報文自動由這些方法完成,當握手完成後狀態則被置為FINISHED,接著狀態變為NOT_HANDSHAKING,表示已經不在握手階段了,已經可以進行應用層通訊了,此時整個SSL握手結束。

應用層安全通訊過程其實也是靠SSLEngine引擎的unwrap和wrap方法對資料進行加解密並且對通訊雙方進行認證,例如應用層讀操作是將netInData和appInData傳入unwrap方法,處理後的appInData即為應用需要的資料,而寫操作則是將appOutData和netOutData傳入wrap方法,處理後的netOutData即為傳輸給對方的資料。

至此,通過在網路與應用直接增加一個SSLEngine引擎層,則實現了安全通訊,並且使用了NIO模式讓服務端擁有更加出色的處理效能。

====廣告時間,可直接跳過====

鄙人的新書《Tomcat核心設計剖析》已經在京東預售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

=========================

歡迎關注:

如何讓你的傳輸更安全——NIO 模式和 BIO 模式實現 SSL 協議通訊
這裡寫圖片描述

相關文章