Java通過SSLEngine與NIO實現(xiàn)HTTPS訪問的操作方法
Java使用NIO進行HTTPS協(xié)議訪問的時候,離不開SSLContext和SSLEngine兩個類。我們只需要在Connect操作、Connected操作、Read和Write操作中加入SSL相關(guān)的處理即可。
一、連接服務(wù)器之前先初始化SSLContext并設(shè)置證書相關(guān)的操作。
public void Connect(String host, int port) {
mSSLContext = this.InitSSLContext();
super.Connect(host, port);
}
在連接服務(wù)器前先創(chuàng)建SSLContext對象,并進行證書相關(guān)的設(shè)置。如果服務(wù)器不是使用外部公認的認證機構(gòu)生成的密鑰,可以使用基于公鑰CA的方式進行設(shè)置證書。如果是公認的認證證書一般只需要加載Java KeyStore即可。
1.1 基于公鑰CA
public SSLContext InitSSLContext() throws NoSuchAlgorithmException{
// 創(chuàng)建生成x509證書的對象
CertificateFactory caf = CertificateFactory.getInstance("X.509");
// 這里的CA_PATH是服務(wù)器的ca證書,可以通過瀏覽器保存Cer證書(Base64和DER都可以)
X509Certificate ca = (X509Certificate)caf.generateCertificate(new FileInputStream(CA_PATH));
KeyStore caKs = KeyStore.getInstance("JKS");
caKs.load(null, null);
// 將上面創(chuàng)建好的證書設(shè)置到倉庫里面,前面的`baidu-ca`只是一個別名可以任意不要出現(xiàn)重復(fù)即可。
caKs.setCertificateEntry("baidu-ca", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(caKs);
// 最后創(chuàng)建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);
// 最后創(chuàng)建SSLContext,將可信任證書列表傳入。
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(null, tmf.getTrustManagers(), null);
return context;
}
二、連接服務(wù)器成功后,需要創(chuàng)建SSLEngine對象,并進行相關(guān)設(shè)置與握手處理。
通過第一步生成的SSLContext創(chuàng)建SSLSocketFactory并將當前的SocketChannel進行綁定(注:很多別人的例子都沒有這步操作,如果只存在一個HTTPS的連接理論上沒有問題,但如果希望同時創(chuàng)建大量的HTTPS請求“可能”有問題,因為SSLEngine內(nèi)部使用哪個Socket進行操作數(shù)據(jù)是不確定,如果我的理解有誤歡迎指正)。
然后調(diào)用創(chuàng)建SSLEngine對象,并初始化操作數(shù)據(jù)的Buffer,然后開始進入握手階段。(注:這里創(chuàng)建的Buffer主要用于將應(yīng)用層數(shù)據(jù)加密為網(wǎng)絡(luò)數(shù)據(jù),將網(wǎng)絡(luò)數(shù)據(jù)解密為應(yīng)用層數(shù)據(jù)使用:“密文與明文”)。
public final void OnConnected() {
super.OnConnected();
// 設(shè)置socket,并創(chuàng)建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);
}
三、進行握手操作
下圖簡單展示了握手流程,由客戶端發(fā)起,通過一些列的數(shù)據(jù)交換最終完成握手操作。要成功與服務(wù)器建立連接,握手流程是非常重要的環(huán)節(jié),幸好SSEngine內(nèi)部已經(jīng)實現(xiàn)了證書驗證、交換等步驟,我們只需要在其上層執(zhí)行特定的行為(握手狀態(tài)處理)。

3.1 握手相關(guān)狀態(tài)(來自getHandshakeStatus方法)
NEED_WRAP當前握手狀態(tài)表示需要加密數(shù)據(jù),即將要發(fā)送的應(yīng)用層數(shù)據(jù)加密輸出為網(wǎng)絡(luò)層數(shù)據(jù),并執(zhí)行發(fā)送操作。
NEED_UNWRAP當前握手狀態(tài)表示需要對數(shù)據(jù)進行解密,即將收到的網(wǎng)絡(luò)層數(shù)據(jù)解密后成應(yīng)用層數(shù)據(jù)。
NEED_TASK當前握手狀態(tài)表示需要執(zhí)行任務(wù),因為有些操作可能比較耗時,如果不希望造成阻塞流程就需要開啟異步任務(wù)進行執(zhí)行。
FINISHED當前握手已完成
NOT_HANDSHAKING表示不需要握手,這個主要是再次連接時,為了加快速度而跳過握手流程。
3.2處理握手的方法
以下代碼展示了握手流程中的各種狀態(tài)的處理,主要的邏輯就是如果需要加密就執(zhí)行加密操作,如果需要執(zhí)行解密就執(zhí)行解密操作(廢話@_@!)。
protected void ProcessHandShake(SSLEngineResult result){
if(this.isClosed() || this.isShutdown()) return;
// 區(qū)分是來此WRAP UNWRAP調(diào)用,還是其他調(diào)用
SSLEngineResult.HandshakeStatus status;
if(result != null){
status = result.getHandshakeStatus();
}else{
status = mSSLEngine.getHandshakeStatus();
}
switch(status)
{
// 需要加密
case NEED_WRAP:
//判斷isOutboundDone,當true時,說明已經(jīng)不需要再處理任何的NEED_WRAP操作了.
// 因為已經(jīng)顯式調(diào)用過closeOutbound,且就算執(zhí)行wrap,
// SSLEngineReulst.STATUS也一定是CLOSED,沒有任何意義
if(mSSLEngine.isOutboundDone()){
// 如果還有數(shù)據(jù)則發(fā)送出去
if(mNetDataBuf.position() > 0) {
mNetDataBuf.flip();
mSocketChannel.WriteAndFlush(mNetDataBuf);
}
break;
}
// 執(zhí)行加密流程
this.ProcessWrapEvent();
break;
// 需要解密
case NEED_UNWRAP:
//判斷inboundDone是否為true, true說明peer端發(fā)送了close_notify,
// peer發(fā)送了close_notify也可能被unwrap操作捕獲到,結(jié)果就是返回的CLOSED
if(mSSLEngine.isInboundDone()){
//peer端發(fā)送關(guān)閉,此時需要判斷是否調(diào)用closeOutbound
if(mSSLEngine.isOutboundDone()){
return;
}
mSSLEngine.closeOutbound();
}
break;
case NEED_TASK:
// 執(zhí)行異步任務(wù),我這里是同步執(zhí)行的,可以弄一個異步線程池進行。
Runnable task = mSSLEngine.getDelegatedTask();
if(task != null){
task.run();
// executor.execute(task); 這樣使用異步也是可以的,
//但是異步就需要對ProcessHandShake的調(diào)用做特殊處理,因為異步的,像下面這直接是會導(dǎo)致瘋狂調(diào)用。
}
this.ProcessHandShake(null); // 繼續(xù)處理握手
break;
case FINISHED:
// 握手完成
mHandshakeCompleted = true;
this.OnHandCompleted();
return;
case NOT_HANDSHAKING:
// 不需要握手
if(!mHandshakeCompleted)
{
mHandshakeCompleted = true;
this.OnHandCompleted();
}
return;
}
}
四、數(shù)據(jù)的發(fā)送與接收
握手成功后就可以進行正常的數(shù)據(jù)發(fā)送與接收,但是需要額外在數(shù)據(jù)發(fā)送的時候進行加密操作,數(shù)據(jù)接收后進行解密操作。
這里需要額外說明一下,在握手期間也是會需要讀取數(shù)據(jù)的,因為服務(wù)器發(fā)送過來的數(shù)據(jù)需要我們執(zhí)行讀取并解密操作。而這個操作在一些其他的例子中直接使用了阻塞的讀取方式,我這里則是放在OnRead事件調(diào)用后進行處理,這樣才符合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);
// 發(fā)完成后清空buffer
mNetDataBuf.clear();
}
mAppDataBuf.clear();
// 如果沒有握手完成,則繼續(xù)調(diào)用握手處理
if(!mHandshakeCompleted)
this.ProcessHandShake(result);
}
4.2 解密操作(SelectionKey.OP_READ)
protected void ProcessUnWrapEvent(){
if(this.isClosed() || this.isShutdown()) return;
do{
// 執(zhí)行解密操作
SSLEngineResult res = mSSLEngine.unwrap(pNetDataBuf, pAppDataBuf);
if(!ProcessSSLStatus(res, false))
// 這里不需要對`pNetDataBuf`進行處理,因為ProcessSSLStatus里面已經(jīng)做好處理了。
return;
if(res.getStatus() == Status.CLOSED)
break;
// 未完成握手時,需要繼續(xù)調(diào)用握手處理
if(!mHandshakeCompleted)
this.ProcessHandShake(res);
}while(pNetDataBuf.hasRemaining());
// 數(shù)據(jù)都解密完了,這個就可以清空了。
if(!pNetDataBuf.hasRemaining())
pNetDataBuf.clear();
}
到此這篇關(guān)于Java通過SSLEngine與NIO實現(xiàn)HTTPS訪問的文章就介紹到這了,更多相關(guān)Java通過SSLEngine與NIO實現(xiàn)HTTPS訪問內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot Event Bus用法小結(jié)
Spring Boot Event Bus是Spring框架中事件驅(qū)動編程的一部分,本文主要介紹了Spring Boot Event Bus用法小結(jié),感興趣的可以了解一下2023-09-09
springBoot下實現(xiàn)java自動創(chuàng)建數(shù)據(jù)庫表
這篇文章主要介紹了springBoot下實現(xiàn)java自動創(chuàng)建數(shù)據(jù)庫表的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
Windows系統(tǒng)下Eclipse搭建ESP32編譯環(huán)境及安裝過程
Ecppse 使用了 ESP-IDF 中的 Makefile 支持。這意味著您需要從創(chuàng)建 ESP-IDF 項目開始。您可以使用 github 中的 idf-template 項目,接下來通過本文給大家介紹Windows系統(tǒng)下Eclipse搭建ESP32編譯環(huán)境及安裝過程,感興趣的朋友一起看看吧2021-10-10
SpringCloud Netfilx Ribbon負載均衡工具使用方法介紹
Ribbon是Netflix的組件之一,負責注冊中心的負載均衡,有助于控制HTTP和TCP客戶端行為。Spring Cloud Netflix Ribbon一般配合Ribbon進行使用,利用在Eureka中讀取的服務(wù)信息,在調(diào)用服務(wù)節(jié)點時合理進行負載2022-12-12
Hibernate基于ThreadLocal管理Session過程解析
這篇文章主要介紹了Hibernate基于ThreadLocal管理Session過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-10-10
SpringBoot多controller添加URL前綴的實現(xiàn)方法
這篇文章主要介紹了SpringBoot多controller添加URL前綴的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-02-02

