本文將詳細介紹如何在Java端、C++端和NodeJs端實作基于SSL/TLS的加密通信,重點分析Java端利用SocketChannel和SSLEngine從握手到資料發送/接收的完整程序,本文也涵蓋了在Ubuntu系統上利用OpenSSL和Libevent如何創建一個支持SSL的服務端,文章中介紹的知識點并未全部在SMSS專案中實作,因此筆者會列出所有相關原始碼以方便讀者查閱,提醒:由于知識點較多,分享涵蓋了多種語言,預計的學習時間可能會大于3小時,為了保證讀者能有良好的學習體驗,繼續前請先安排好時間,如果遇到困難,您也可以根據自己的實際情況有選擇的學習,也歡迎與我交流,
一 相關前置知識
libevent網路庫:libevent是一個用c語言撰寫的高性能支持事件回應的網路庫,編譯libevent前需要確保目標機器上已經完成對openssl的編譯,否則生成的動態庫中可能會缺少呼叫openssl的介面,這里選擇的openssl版本為1.1.1d,如果你選擇1.0以前的版本可能與后面的代碼示例有所不同,
electron桌面應用:electron是一套依賴google的V8引擎直接使用HTML/JS/CSS創建桌面應用的跨平臺解決方案,如果你需要開發輕量化的桌面端應用,electron基本是不二選擇,從個人的實踐來看,無論是開發生態還是開發效率都強于Qt,使用electron可以呼叫nodejs相關介面完成與系統的互動,
Java-nio開發包:基本是現在作為Java中高級開發的必備技能,
javax.net.ssl開發包:屬于Java對SSL/TLS支持的比較底層的開發包,目前在應用中更多會選擇Netty等集成式框架,如果你的專案中需要一些定制化功能可以選擇它作為支持,建議在專案中慎重使用,由于一些特殊原因,Java只提供了SSLSocket物件,底層只支持阻塞式訪問,文章最后會提供一個我個人實作的SSLSocketChannel物件,方便讀者在基礎上進行二次封裝,
SSL/TLS通信:安全通信的目的是在原有的tcp/ip層和應用層之間增加了一個稱之為SSL/TLS的加/解密層來實作的,在網路協議層中的位置大致如下:

在OSI七層網路協議的定義中,它處于表示層,程式開發的方式一般是在完成tcp/ip建立連接后,開始ssl/tls握手,發布ssl的服務端需要具備一個私鑰檔案(.key)以及與私鑰配套的證書檔案(.crt),證書包含了公鑰和對公鑰的簽名,還有一些用來證明源安全的資訊,證書需要到專門的機構申請并且有年費要求,鑒于各位讀者僅用于自學,后面生成的證書我們會做自簽名,ssl/tls握手的目的是在客戶端和服務端之間協商一個安全的對稱秘鑰,用來為本次會話的訊息加解密,由于這對秘鑰僅通信的服務端和客戶端持有,會話結束即消失,
二 libevent和openssl
生成x.509證書
首選在安裝好openssl的機器上創建私鑰檔案:server.key
> openssl genrsa -out server.key 2048
得到私鑰檔案后我們需要一個證書請求檔案:server.csr,將來你可以拿這個證書請求向正規的證書管理機構申請證書
> openssl req -new -key server.key -out server.csr
最后我們生成自簽名的x.509證書(有效期365天):server.crt
> openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
x.509證書是密碼學里公鑰證書的格式標準,被應用在包括ssl/tls等多項場景中,
OpenSSL加密通信介面分析

與ssl/tls通信相關的介面基本可以分為兩大類,SSL_CTX通信背景關系和SSL直接通信介面,下面逐一分析:
- SSL_CTX_new:新版本摒棄了一些老的介面,目前建議基本統一使用此方法來創建通信背景關系
- SSL_CTX_free:釋放SSL_CTX*
- SSL_CTX_use_certificate_file:設定證書檔案
- SSL_CTX_use_PrivateKey_file:設定私鑰檔案,與上面的證書檔案必須配套否則檢測不通過
- SSL_CTX_check_private_key:檢查私鑰和證書檔案
- SSL_new:方法一創建完成的背景關系在通過此方法創建配套的SSL*
- SSL_set_fd:與上面創建的SSL和socket_fd系結
- SSL_accept:服務端握手方法
- SSL_connect:客戶端握手方法
- SSL_write:訊息發送,內部會對明文訊息加密并呼叫socket發送
- SSL_read:訊息接收,內部會從socket接收到密文資料再解碼成文明回傳
- SSL_shutdown:通知對方關閉本次加密會話
- SSL_free:釋放SSL*
C++撰寫socket利用openssl介面開發測驗代碼
在熟悉以上基本概念之后,根據測驗先行和敏捷開發的原則,我們接下來就要直接使用c++開發一個socket測驗程式,并利用openssl介面進行加密通信,以下代碼的開發和運行系統為ubuntu 16.04 LTS,openssl版本為1.1.1d 10 Sep 2019,開發工具為Visual Studio Code 1.41.1,
服務端原始碼 server.cpp
#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <cstring> #include <netinet/in.h> #include <string> #include "openssl/ssl.h" #include "openssl/err.h" using namespace std; // 前置申明 struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file); int main(int argc, char *argv[]) { ssl_ctx_st *ssl_ctx = InitSSLServer("../server.crt", "../server.key"); // 引入之前生成好的私鑰檔案和證書檔案 int sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(10020); // 指定通信埠 int res = ::bind(sock, (sockaddr *)&sin, sizeof(sin)); if (res == -1) { return -1; } listen(sock, 1); // 開始監聽 // 只接受一次客戶端的連接 int client_fd = accept(sock, 0, 0); cout << "Client accept success!" << endl; ssl_st *ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, client_fd); res = SSL_accept(ssl); // 執行SSL層握手 if (res != 1) { ERR_print_errors_fp(stderr); return -1; } // 握手完成,接受訊息并發送一次應答 char buf[1024] = {0}; int len = SSL_read(ssl, buf, sizeof(buf)); cout << buf << endl; string s = "Hi Client, I'm CppSSLSocket Server."; SSL_write(ssl, s.c_str(), s.size()); // 釋放資源 SSL_free(ssl); SSL_CTX_free(ssl_ctx); return 0; } struct ssl_ctx_st *InitSSLServer(const char *crt_file, const char *key_file) { // 創建通信背景關系 ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method()); if (!ssl_ctx) { cout << "ssl_ctx new failed" << endl; return nullptr; } int res = SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM); if (res != 1) { ERR_print_errors_fp(stderr); return nullptr; } res = SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM); if (res != 1) { ERR_print_errors_fp(stderr); return nullptr; } res = SSL_CTX_check_private_key(ssl_ctx); if (res != 1) { return nullptr; } return ssl_ctx; }
客戶端原始碼 client.cpp
#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <cstring> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include "openssl/ssl.h" #include "openssl/err.h" using namespace std; struct ssl_ctx_st *InitSSLClient(); int main(int argc, char *argv[]) { int sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr("127.0.0.1"); sin.sin_port = htons(10020); // 首先執行socket連接 int res = connect(sock, (sockaddr *)&sin, sizeof(sin)); if (res != 0) { return -1; } cout << "Client connect success." << endl; ssl_ctx_st *ssl_ctx = InitSSLClient(); ssl_st *ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, sock); // 進行SSL層握手 res = SSL_connect(ssl); if (res != 1) { ERR_print_errors_fp(stderr); return -1; } string send_msg = "Hello Server, I'm CppSSLSocket Client."; SSL_write(ssl, send_msg.c_str(), send_msg.size()); char recv_msg[1024] = {0}; int recv_len = SSL_read(ssl, recv_msg, sizeof(recv_msg)); recv_msg[recv_len] = '\0'; cout << recv_msg << endl; SSL_shutdown(ssl); SSL_free(ssl); SSL_CTX_free(ssl_ctx); return 0; } struct ssl_ctx_st *InitSSLClient() { // 創建一個ssl客戶端的背景關系 ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_client_method()); return ssl_ctx; }
編譯使用Makefile,客戶端的修改TARGET即可
TARGET=server.x SRC=$(wildcard *.cpp) OBJS=$(patsubst %.cpp,%.o,$(SRC)) LIBS=-lssl -lcrypto $(TARGET):$(SRC) g++ -std=c++11 $^ -o $@ $(LIBS) clean: rm -fr $(TARGET) $(OBJS)
如果在服務端和客戶端都可以正常發送和接收顯示訊息,即表示通信正常,
C++撰寫openssl與libevent安全通信服務端
當前專案使用的libevent版本為2.1,在編譯的時候需要在目標機器上預先編譯好openssl,否則編譯時檢測不到,無法生成對應介面,有關libevent的基礎可以參考smss開源系列的前期文章,這里不再贅述,考慮到同構系統的開發案例網上的資料相對豐富,同時筆者目前的作業大多為異構系統開發為主,因此這里選擇使用C++作為服務端,Java和NodeJs為客戶端的方式,如果讀者有需要也可以給我留言,我會補充Java作為服務端C++作為客戶端的相關案例,
目前使用libevent和openssl作為通信框架,在追求性能優先的物聯網專案中應用廣泛,開發難度也相對較低,libevent也提供了專門呼叫openssl的介面,它可以幫助我們管理SSL物件,不過SSL_CTX的維護還需要我們自己實作,與直接使用libevent創建服務端相比最大的區別在于我們需要自己創建socket并同時交給event_base和SSL_CTX來使用,
服務端原始碼 libevent_server.cpp
#include <iostream> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <cstring> #include <string> #include "openssl/ssl.h" #include "event2/event.h" #include "event2/listener.h" #include "event2/bufferevent.h" #include "event2/bufferevent_ssl.h" using namespace std; // 設定x.509證書檔案和私鑰檔案 ssl_ctx_st *InitServer(const char *crt_file, const char *key_file); // 創建通信ssl ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket); // 服務端連接監聽回呼函式 void EvconnlistenerCB(struct evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx); // 訊息讀、寫和事件回呼 void ReadCB(struct bufferevent *bev, void *ctx); void WriteCB(struct bufferevent *bev, void *ctx); void EventCB(struct bufferevent *bev, short what, void *ctx); static bool isSsl = false; int main(int argc, char *argv[]) { if (argc == 2) { if (strcmp(argv[1], "SSL") == 0) { isSsl = true; } } // 創建event_base event_base *base = event_base_new(); if (!base) { cout << "event_base_new fail" << endl; return -1; } // 創建SSL_CTX通信背景關系 ssl_ctx_st *ssl_ctx = InitServer("../server.crt", "../server.key"); // 創建socket sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(10020); evconnlistener *listener = evconnlistener_new_bind( base, EvconnlistenerCB, ssl_ctx, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, 10, (sockaddr *)&addr, sizeof(addr)); // 阻塞當前執行緒執行事件回圈 event_base_dispatch(base); // 釋放資源 SSL_CTX_free(ssl_ctx); event_base_free(base); return 0; } void EvconnlistenerCB(evconnlistener *listener, evutil_socket_t socket, struct sockaddr *addr, int socklen, void *ctx) { cout << "Server EvconnlistenerCB..." << endl; // 獲取當前的事件回圈背景關系 event_base *base = evconnlistener_get_base(listener); bufferevent *bev = nullptr; // 判斷當前是否啟用ssl通信模式 if (isSsl) { ssl_ctx_st *ssl_ctx = (ssl_ctx_st *)ctx; ssl_st *ssl = NewSSL(ssl_ctx, socket); // 創建bufferevent,當bufferevent關閉的時候,會同時釋放ssl資源 bev = bufferevent_openssl_socket_new(base, socket, ssl, BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, ssl); } else { bev = bufferevent_socket_new(base, socket, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, ReadCB, WriteCB, EventCB, base); } // 注冊事件型別 bufferevent_enable(bev, EV_READ | EV_WRITE); } /** * ssl背景關系初始化 * 考慮測驗簡潔的需要,這里沒有做多余判斷 */ ssl_ctx_st *InitServer(const char *crt_file, const char *key_file) { ssl_ctx_st *ssl_ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX_use_certificate_file(ssl_ctx, crt_file, SSL_FILETYPE_PEM); SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM); SSL_CTX_check_private_key(ssl_ctx); return ssl_ctx; } /** * 創建ssl介面并且和socket系結 */ ssl_st *NewSSL(ssl_ctx_st *ssl_ctx, int socket) { ssl_st *ssl = SSL_new(ssl_ctx); SSL_set_fd(ssl, socket); return ssl; } void ReadCB(bufferevent *bev, void *ctx) { char buf[1024] = {0}; int len = bufferevent_read(bev, buf, sizeof(buf) - 1); buf[len] = '\0'; cout << buf << endl; string msg = "hello client, I'm server.\n"; bufferevent_write(bev, msg.c_str(), msg.size()); bufferevent_write(bev, buf, len); } void WriteCB(bufferevent *bev, void *ctx) { } void EventCB(bufferevent *bev, short what, void *ctx) { cout << "EventCB: " << what << endl; if (what & BEV_EVENT_CONNECTED) { cout << "Event:BEV_EVENT_CONNECTED" << endl; } if (what & BEV_EVENT_ERROR && what & BEV_EVENT_READING) { cout << "Event:BEV_EVENT_READING" << endl; bufferevent_free(bev); } if (what & BEV_EVENT_ERROR && what & BEV_EVENT_WRITING) { cout << "Event:BEV_EVENT_WRITING" << endl; bufferevent_free(bev); } }
編譯用的Makefile檔案
TARGET=server.x SRC=https://www.cnblogs.com/learnhow/p/$(wildcard *.cpp) OBJS=$(patsubst %.cpp,%.o,$(SRC)) LIBS=-lssl -lcrypto -levent -levent_openssl $(TARGET):$(SRC) g++ -std=c++11 $^ -o $@ $(LIBS) clean: rm -fr $(TARGET) $(OBJS)
特別需要注意bufferevent_openssl_socket_new方法包含了對bufferevent和SSL的管理,因此當連接關閉的時候不再需要SSL_free,可執行檔案server.x接收SSL作為引數,作為是否啟用安全通信的標識,
讀者可以使用上一節生成的client.x與本節的程式通信,方便測驗結果,
三 *基于Node.js的(加密)通信測驗
*注:如果您不熟悉electron可以跳過本節,不妨礙后面的學習
由于electron不是本文的重點,因此如何創建和開發electron專案做過過多介紹,本例使用electron-vue作為模板,使用vue-cli直接創建,我們將分別使用Node.js的net包和tls包創建通信客戶端,
net.Socket連接示例:
this.socket = net.connect(10020, "127.0.0.1", () => { console.log("socket 服務器連接成功..."); this.socket.write("Hello Server, I'm Nodejs.", () => { console.log("發送完成~"); }); }); this.socket.on("data", data =https://www.cnblogs.com/learnhow/p/> { console.log(data.toString()); });
tls.connect連接示例:
this.socket = tls.connect( { host: "127.0.0.1", port: 10020, rejectUnauthorized: false }, () => { console.log("ssl 服務器連接成功..."); this.socket.write("Hello Server, I'm Nodejs.", () => { console.log("發送完成~"); }); } ); this.socket.on("data", data =https://www.cnblogs.com/learnhow/p/> { console.log(data.toString()); });
由于之前我們通過openssl生成的x.509證書為自簽名證書,因此在使用tls.connect的時候需要指定rejectUnauthorized屬性,
讀者可以利用這套代碼和上一節創建的server.x分別進行普通通信和安全通信,以判斷功能是否正常,
四 創建基于SSLEngine的NIO通信
如果說之前的知識你都能夠掌握,那么從這里開始才是本文的重點,也是難點所在,網上對于SSLEngine的介紹資料相對較少,且大多都沒有經過完整測驗,確實造成學習曲線過于陡峭,加之筆者認為Java對于SSLEngine的設計的確不太合理,因此強烈不建議讀者在實際專案中使用,事實上,SSL/TLS協議的握手程序非常復雜,涉及到加密和秘鑰交換等多個步驟,無論是基于C語言的openssl還是基于Node.js的tls.connect都將握手的程序封裝到內部,現在筆者將通過介紹SSLEngine讓你對這一程序有所了解,
ByteBuffer分析
io面向流(stream)開發,而nio面向緩沖(buffer)開發,很多人對此也不陌生,但是在作業中我發現能夠深入理解這句話的人比較少,什么叫面向流(stream)?為什么有區別于面向緩沖(buffer)?傳統io在向檔案或資料庫請求資料的時候,由于需要請求作業系統資源,因此存在需要等待回應的程序,它不同于單純的代碼執行只需要使用cpu資源,io操作還需要涉及總線資源,磁盤資源等,在這個程序中,由于無法確定資料什么時候會回傳,只能做阻塞等待,nio的做法相當于告知作業系統:我已經在用戶態申請好了一塊記憶體空間(buffer),當內核接收到資料以后請直接寫到我的空間中,因此,使用nio編程的特點之一就是對資料的處理往往需要通過回呼函式(callback),作為最常用的緩沖物件——ByteBuffer,你有多熟悉?
ByteBuffer最重要的三個屬性:
- capacity 表示該緩沖區的最大容量,任何操作最大容量的讀寫操作都屬于非法
- limit 如果當前是寫入態,limit等于capacity,如果當前是讀取態,limit表示當前一共有多少有效資料,注意,寫入態和讀取態是我創造的名詞,buffer本身并不存在這兩個狀態
- position 當前資料區的讀/寫位置指標
當你開始往buffer中寫入資料的時候,pos會不斷增加,limit等于cap,寫入完成后,如果你想要讀取資料,第一步必須進行翻轉(flip),翻轉以后的資料區pos為0,而limit則等于之前寫入的pos,如果在讀取資料的時候,無法一次性處理完,我們可以使用compact()方法將已經讀取的資料清除,
為了加深印象,請大家思考一個問題:如果我向一個ByteBuffer中寫入了資料,假設當前緩沖區的狀態為 java.nio.HeapByteBuffer[pos=1305 lim=16921 cap=16921],我又讀取了94個位元組,當前緩沖區狀態為 java.nio.HeapByteBuffer[pos=94 lim=1305 cap=16921],此時呼叫compact(),緩沖區的狀態是什么情況?
根據jdk官方檔案上的解釋,compact()方法會將緩沖區中的資料按位復制,pos復制到0,pos + 1復制到1,以此類推,最后是將limit-1復制到limit-pos,事實上方法內部還幫我們做了一次翻轉操作,當前的緩沖區狀態為 java.nio.HeapByteBuffer[pos=1211 lim=16921 cap=16921],

非阻塞SocketChannel
目前幾乎所有支持非阻塞的通信框架都基于React模式開發,通過在IO管道上注冊多個事件回呼以達到異步處理的效果,又因為回呼的使用原來越多,因此Java 8也提出了函式式介面的概念,同時引入蘭姆達運算式以讓用戶能夠設計出更適合閱讀和維護的代碼,
NIO在socket上的運用Java提供了SocketChannel和Selector物件,
非阻塞客戶端 NioSocket.java
package socket; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Set; public class NioSocket { /** * 連接方法 * * @param host 服務器主機地址 * @param port 服務器埠 */ public static void connection(String host, int port) throws IOException { Selector sel = Selector.open(); // 創建事件選擇器 InetSocketAddress addr = new InetSocketAddress(host, port); SocketChannel socket = SocketChannel.open(); // 創建非阻塞socket物件 socket.configureBlocking(false).register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ); // 配置非阻塞模式和向Selector注冊連接事件與資料可讀事件 socket.connect(addr); while (true) { // 等待間隔 if (sel.select(10) > 0) { Set<SelectionKey> keys = sel.selectedKeys(); for(SelectionKey key : keys) { keys.remove(key); // 移除事件并處理 if(key.isConnectable()) { socket.finishConnect(); String reqMsg = "Hello Server, I'm JavaClient."; ByteBuffer reqBuf = ByteBuffer.wrap(reqMsg.getBytes()); socket.write(reqBuf); } else if(key.isReadable()) { ByteBuffer respBuf = ByteBuffer.allocate(1024); int length = socket.read(respBuf); if(length > 0) { String respMsg = new String(respBuf.array(), 0, length); System.out.println(respMsg); } } } } } } public static void main(String[] args) { try { NioSocket.connection("127.0.0.1", 10020); } catch (IOException e) { e.printStackTrace(); } } }
當有注冊的事件產生的時候,我們能夠通過selectedKey()方法獲取完整的事件佇列,如果事件沒有被處理,會在下一次事件回圈中重新觸發,因此處理完成的事件需要從佇列中洗掉,
阻塞式加密通信 SSLSocket
接下來我們將難度升級,看一下利用SSLSocket如何開發加密通信的客戶端,Java為我們提供了javax.net.ssl包,里面都是與SSL/TLS加密通信相關的組件,由于服務端使用的是自簽名證書,因此我們需要重寫TrustManager的實作
package tls; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; public class X509SelfSignTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { for (int i = 0; i < chain.length; i++) { System.out.println(chain[i]); } } @Override public X509Certificate[] getAcceptedIssuers() { return null; } }
作為客戶端checkClientTrusted()和getAcceptedIssuers()方法都不會被呼叫,checkServerTrusted()方法用來檢查服務端的證書,我們只將證書內容列印出來,
package tls; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; public class Ssl { public SSLSocket connection(String host, int port) throws Exception { SSLContext context = SSLContext.getInstance("SSL"); context.init(null, new TrustManager[] {new X509SelfSignTrustManager()} , new java.security.SecureRandom()); SSLSocketFactory factory = context.getSocketFactory(); return (SSLSocket) factory.createSocket(host, port); } public static void main(String[] args) { Ssl ssl = new Ssl(); SSLSocket sslSocket = null; try { sslSocket = ssl.connection("127.0.0.1", 10020); OutputStream output = sslSocket.getOutputStream(); String msg = "Hello Server, I'm BioSSLClient."; output.write(msg.getBytes()); output.flush(); InputStream input = sslSocket.getInputStream(); byte[] buf = new byte[1024]; int len = input.read(buf); String ss = new String(buf, 0, len); System.out.println(ss); } catch (Exception e) { e.printStackTrace(); } finally { try { sslSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
首先是需要創建基于SSL協議的背景關系物件SSLContext
使用我們自己實作的證書管理器進行初始化
創建SSLSocketFactory,并通過它實體化SSLSocket
通信程序基本就是操作io流,這里不做贅述
SSLEngine——抽象化的握手和加/解密介面
先看一下規范的SSL/TLS握手步驟:

基本的通信大致可以分為4個程序:
- 選擇協議版本和會話ID
- 服務端發送證書和秘鑰交換資料
- 客戶端處理證書和生成秘鑰交換資料并發送給服務端
- 會話秘鑰協商成功,握手完成
因為SSLEngine僅僅是針對SSL層進行了抽象,因此底層通訊介面需要自己創建,因為打算使用nio,我將創建一個SocketChannel,
SSLEngine也通過SSLContext實體化,SSLContext還能夠實體化一個SSLSession物件,使用SSLSession幫助我們創建兩種快取:應用資料快取和網路資料快取,顧名思義,應用資料快取用來存盤明文資料,網路資料快取代表將要發送或接收到的密文資料,它們通過SSLEngine的wrap()和unwrap()方法相互轉換,使用SSLEngine的難點是執行握手操作,關鍵點在于如何理解內部的兩個列舉型別:
SSLEngineResult.HandshakeStatus:
- NEED_WRAP 當前有資料需要被加密并發送
- NEED_UNWRAP 當前有資料應該被讀取并解密
- NEED_TASK 需要執行運算任務
- FINISHED 握手完成
- NOT_HANDSHAKING 當前不處于握手狀態中
特別注意,FINISHED狀態只會在握手完成后的最后一步操作中出現,之后再獲取狀態都會顯示為NOT_HANDSHAKING(SSLEngine為什么會這樣設計我也沒看懂),我曾經以為NOT_HANDSHAKING狀態表示握手已斷開,一度很不理解,
SSLEngineResult.Status:在執行wrap()或unwrap()操作后
- OK 執行成功
- BUFFER_OVERFLOW 寫入快取區不足,通常表示unwrap()的第二個引數設定的buffer剩余空間不足
- BUFFER_UNDERFLOW 輸出緩沖區不足,通常表示wrap()的第一個引數設定的buffer中沒有資料
- CLOSED SSLEngine已經被關閉,無法執行任何方法
利用SSLEngine進行握手的時候,我們會多次使用wrap()和unwrap()方法,此時如果打開斷點你會發現明明沒有提供明文資料,經過wrap()后密文快取中卻有資料,或者接收到密文資料后經過unwrap()方法,卻沒得到任何明文資料,原因是,握手階段的任何資料都在SSLEngine內部處理(這個設計很奇怪,不明白Java的設計者們如此設計的初衷是什么),
package tls; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; public class NioSsl { private SocketChannel sc; private SSLEngine sslEngine; private Selector selector; private HandshakeStatus hsStatus; private Status status; private ByteBuffer localNetData; private ByteBuffer localAppData; private ByteBuffer remoteNetData; private ByteBuffer remoteAppData; public void connection(String host, int port) throws Exception { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom()); sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(true); SSLSession session = sslEngine.getSession(); localAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); localNetData = ByteBuffer.allocate(session.getPacketBufferSize()); remoteAppData = ByteBuffer.allocate(session.getApplicationBufferSize()); remoteNetData = ByteBuffer.allocate(session.getPacketBufferSize()); remoteNetData.clear(); SocketChannel channel = SocketChannel.open(); selector = Selector.open(); channel.configureBlocking(false).register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE); InetSocketAddress addr = new InetSocketAddress(host, port); channel.connect(addr); sslEngine.beginHandshake(); hsStatus = sslEngine.getHandshakeStatus(); while (true) { if (selector.select(10) > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey selectionKey = it.next(); it.remove(); handleSocketEvent(selectionKey); } } } } private void handleSocketEvent(SelectionKey key) throws IOException, InterruptedException { if (key.isConnectable()) { System.out.println("isConnectable..."); sc = (SocketChannel) key.channel(); sc.finishConnect(); doHandshake(); localAppData.clear(); localAppData.put("Hello Server, I'm NioSslClient.".getBytes()); localAppData.flip(); localNetData.clear(); SSLEngineResult result = sslEngine.wrap(localAppData, localNetData); hsStatus = result.getHandshakeStatus(); status = result.getStatus(); if (status == Status.OK) { localNetData.flip(); while (localNetData.hasRemaining()) { sc.write(localNetData); } } } else if (key.isReadable()) { System.out.println("isReadable..."); sc = (SocketChannel) key.channel(); remoteNetData.clear(); remoteAppData.clear(); int len = sc.read(remoteNetData); System.out.println("接受服務端加密資料長度:" + len); remoteNetData.flip(); SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData); hsStatus = result.getHandshakeStatus(); status = result.getStatus(); remoteAppData.flip(); byte[] buf = new byte[remoteAppData.limit()]; remoteAppData.get(buf); System.out.println(new String(buf)); } } private void doHandshake() throws IOException, InterruptedException { SSLEngineResult result; int count = 0; while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) { TimeUnit.MILLISECONDS.sleep(100); switch (hsStatus) { case NEED_TASK: System.out.println("當前握手狀態:NEED_TASK"); Runnable runnable; while ((runnable = sslEngine.getDelegatedTask()) != null) { runnable.run(); } hsStatus = sslEngine.getHandshakeStatus(); break; case NEED_UNWRAP: System.out.println("當前握手狀態:NEED_UNWRAP"); count = sc.read(remoteNetData); System.out.println("獲取位元組數:" + count); remoteNetData.flip(); remoteAppData.clear(); do { result = sslEngine.unwrap(remoteNetData, remoteAppData); } while (result.getStatus() == SSLEngineResult.Status.OK && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP); hsStatus = result.getHandshakeStatus(); status = result.getStatus(); remoteNetData.compact(); if (hsStatus == SSLEngineResult.HandshakeStatus.FINISHED) { System.out.println("===========" + hsStatus + "==========="); } break; case NEED_WRAP: System.out.println("當前握手狀態:NEED_WRAP"); localNetData.clear(); result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData); hsStatus = result.getHandshakeStatus(); status = result.getStatus(); if (status != Status.OK) { throw new RuntimeException("status: " + status); } localNetData.flip(); while (localNetData.hasRemaining()) { int len = sc.write(localNetData); System.out.println("發送位元組數:" + len); } hsStatus = sslEngine.getHandshakeStatus(); break; default: break; } } hsStatus = sslEngine.getHandshakeStatus(); System.out.println("===========" + hsStatus + "==========="); } public static void main(String[] args) { NioSsl nioSsl = new NioSsl(); try { nioSsl.connection("127.0.0.1", 10020); } catch (Exception e) { e.printStackTrace(); } } }
原始碼中我已經設定了睡眠時間和必要的訊息輸出,讀者可以復制到IDE中結合C++端的服務協同測驗,如果通信成功,你應該可以在客戶端看到x.509證書列印和13次狀態改變,去除NEED_TASK狀態,再對比SSL/TLS協議的握手規范學習,
SSLSocketChannel 原始碼
如果你能夠順利看到這里,那么恭喜你,在這篇知識分享的文章中,你應該多少有些識訓,為了準備這些東西,我用了幾乎整個2020年的春節假期(幸好假期延長了,否則時間還不夠),最后是我自己封裝的SSLSocketChannel,使用了函式式介面以及蘭姆達運算式,
package tls; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.LinkedList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.function.Supplier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.SSLEngineResult.HandshakeStatus; public class SSLSocketChannel { private volatile boolean isQuit = false; private SocketChannel socket = null; private Selector selector = null; private ExecutorService pool = null; private LinkedList<Function<byte[], byte[]>> readBufQueue = new LinkedList<>(); private LinkedList<Supplier<byte[]>> writeBufQueue = new LinkedList<>(); private Lock writeLock = new ReentrantLock(); private Lock readLock = new ReentrantLock(); private SSLEngine sslEngine; private HandshakeStatus hsStatus; private ByteBuffer localAppData, remoteAppData; private ByteBuffer localNetData, remoteNetData; public SSLSocketChannel() throws IOException { this.selector = Selector.open(); // 打開事件選擇器 this.pool = Executors.newSingleThreadExecutor(); } /** * 創建一個非堵塞的Socket并注冊連接事件和讀取事件 * * @param host * @param port * @throws IOException */ public void connect(String host, int port) throws IOException { InetSocketAddress addr = new InetSocketAddress(host, port); socket = SocketChannel.open(); socket.configureBlocking(false).register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ); socket.connect(addr); } /** * 網路事件回圈執行緒 * * @return 執行緒結束 */ public Future<Void> dispatch() { Future<Void> fut = this.pool.submit(() -> { while (!isQuit) { if (selector.select(10) > 0) { Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); if (key.isConnectable()) { socket.finishConnect(); this.sslHandshake(); } else if (key.isReadable()) { remoteNetData.clear(); int length = socket.read(remoteNetData); if (length > 0) { remoteNetData.flip(); remoteAppData.clear(); SSLEngineResult result = sslEngine.unwrap(remoteNetData, remoteAppData); if (handleResult(result)) { remoteAppData.flip(); byte[] b = new byte[remoteAppData.limit()]; remoteAppData.get(b); try { readLock.lock(); for (Function<byte[], byte[]> fn : readBufQueue) { byte[] r = fn.apply(b); if (r != null) { ByteBuffer buf = ByteBuffer.wrap(r); socket.write(buf); } } } finally { readLock.unlock(); } } } } iter.remove(); } } if (socket.isConnected() && writeBufQueue.size() > 0 && (hsStatus == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING || hsStatus == SSLEngineResult.HandshakeStatus.FINISHED)) { try { writeLock.lock(); Supplier<byte[]> sup = null; while ((sup = writeBufQueue.poll()) != null) { localAppData.clear(); localAppData.put(sup.get()); localAppData.flip(); localNetData.clear(); SSLEngineResult result = sslEngine.wrap(localAppData, localNetData); if (handleResult(result)) { localNetData.flip(); while (localNetData.hasRemaining()) { socket.write(localNetData); } } } } finally { writeLock.unlock(); } } } return null; }); this.pool.shutdown(); return fut; } /** * 添加資料進入發送佇列 * * @param 函式式介面 * @see Supplier */ public void write(Supplier<byte[]> s) { try { writeLock.lock(); writeBufQueue.push(s); } finally { writeLock.unlock(); } } /** * 添加接收器進入接收佇列 * * @param 函式式介面 * @see Function */ public void read(Function<byte[], byte[]> f) { try { readLock.lock(); readBufQueue.push(f); } finally { readLock.unlock(); } } /** * SSL/TLS 握手 * * @throws InterruptedException * @throws NoSuchAlgorithmException * @throws KeyManagementException * @throws IOException */ public void sslHandshake() throws InterruptedException, NoSuchAlgorithmException, KeyManagementException, IOException { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new TrustManager[] { new X509SelfSignTrustManager() }, new java.security.SecureRandom()); sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(true); SSLSession sslSession = sslEngine.getSession(); localAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 本地應用資料快取 localNetData = https://www.cnblogs.com/learnhow/p/ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 本地加密資料快取 remoteAppData = https://www.cnblogs.com/learnhow/p/ByteBuffer.allocate(sslSession.getApplicationBufferSize()); // 遠端應用資料快取 remoteNetData = https://www.cnblogs.com/learnhow/p/ByteBuffer.allocate(sslSession.getPacketBufferSize()); // 遠端加密資料快取 sslEngine.beginHandshake(); hsStatus = sslEngine.getHandshakeStatus(); SSLEngineResult result; // 回圈判斷指導握手完成 while (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED) { switch (hsStatus) { case NEED_WRAP: localNetData.clear(); result = sslEngine.wrap(ByteBuffer.allocate(0), localNetData); // 第一個引數設定空包,SSLEngine會將握手資料寫入網路包 hsStatus = result.getHandshakeStatus(); if (handleResult(result)) { localNetData.flip(); // 確保資料全部發送完成 while (localNetData.hasRemaining()) { socket.write(localNetData); } } break; case NEED_UNWRAP: int len = socket.read(remoteNetData); // 讀取網路資料 if (len == -1) { break; } remoteNetData.flip(); remoteAppData.clear(); do { result = sslEngine.unwrap(remoteNetData, remoteAppData); // 與握手相關的資料SSLEngine會自行處理,不會輸出至第二個引數 hsStatus = result.getHandshakeStatus(); } while (handleResult(result) && hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP); // 一次性沒有完成處理的資料通過壓縮的方式處理,等待下一次資料寫入 remoteNetData.compact(); break; case NEED_TASK: // SSLEngine后臺任務 Runnable runnable; while ((runnable = sslEngine.getDelegatedTask()) != null) { runnable.run(); } hsStatus = sslEngine.getHandshakeStatus(); break; default: break; } } // 握手完成將所有快取清空 localAppData.clear(); localNetData.clear(); remoteAppData.clear(); remoteNetData.clear(); } private boolean handleResult(SSLEngineResult result) { switch (result.getStatus()) { case OK: return true; case BUFFER_OVERFLOW: return false; case BUFFER_UNDERFLOW: return false; case CLOSED: return false; default: return false; } } public static void main(String[] args) { try { SSLSocketChannel sslSocketChannel = new SSLSocketChannel(); sslSocketChannel.connect("127.0.0.1", 10020); sslSocketChannel.dispatch(); sslSocketChannel.read((b) -> { String s = new String(b); System.out.println(s); return null; }); sslSocketChannel.write(() -> { return "hello ssl".getBytes(); }); } catch (IOException e) { e.printStackTrace(); } } }
相關文章:《開源專案SMSS開發指南》
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/16258.html
標籤:架構設計
上一篇:性能優化-資源的壓縮與合并
