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 }

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