Java使用NIO進行HTTPS協議訪問的時候,離不開SSLContext和SSLEngine兩個類。我們只需要在Connect操作、Connected操作、Read和Write操作中加入SSL相關的處理即可。
一、連接服務器之前先初始化SSLContext并設置證書相關的操作。
1
2
3
4
|
public void Connect(String host, int port) { mSSLContext = this .InitSSLContext(); super .Connect(host, port); } |
在連接服務器前先創建SSLContext對象,并進行證書相關的設置。如果服務器不是使用外部公認的認證機構生成的密鑰,可以使用基于公鑰CA的方式進行設置證書。如果是公認的認證證書一般只需要加載Java KeyStore即可。
1.1 基于公鑰CA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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主要用于將應用層數據加密為網絡數據,將網絡數據解密為應用層數據使用:“密文與明文”)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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處理握手的方法
以下代碼展示了握手流程中的各種狀態的處理,主要的邏輯就是如果需要加密就執行加密操作,如果需要執行解密就執行解密操作(廢話@_@!)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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訪問內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/jeffxun/p/15142957.html