繼上一篇介紹如何在多種語言之間使用SSL加密通信,今天我們關注Java端的證書創建以及支持SSL的NioSocket服務端開發。完整源碼
一、創建keystore文件
網上大多數是通過jdk命令創建秘鑰文件,但是有時候我們需要將配套的秘鑰以及證書讓多個模塊來使用,他們很可能是由不同語言開發。在這樣的情形下,我們通常會選擇openssl。
生成服務端的秘鑰文件
openssl genrsa -out server.key 2048
這個秘鑰文件是經過Base64編碼后生成的,你可以使用文本工具打開,有時候這樣的編碼文件又稱為pem文件。
創建基於當前秘鑰的證書請求文件
openssl req -new -key server.key -out server.csr
生成證書請求文件會要求你輸入一些相關信息,這些信息會同秘鑰一起被加密存儲在.csr文件中。它將被用來向正規的CA機構去申請證書。它也是經過Base64編碼后的。
申請X509證書
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
我們申請自簽名的X509證書,有效期1年,證書包含了公鑰和相關信息。由於自簽名證書不是由公認的CA機構簽發,因此使用它來作為服務端證書的時候,瀏覽器會提示告警信息。不過這不妨礙我們在內部環境中使用。
創建PKCS#12文件
openssl pkcs12 -export -clcerts -in server.crt -inkey server.key -out server.p12
PKCS#12是秘鑰交換的標准證書。在加密通信的過程中,如果所有的信息都使用非對稱加密,性能和時間損耗都非常大。因此,根據SSL握手規則,通信雙方首先利用非對稱加密算法協商出一個臨時通信秘鑰,然后在本次會話中僅使用基於當前秘鑰對信息進行對稱加密。會話結束即丟棄,不保存不復用。p12文件中包含了之前生成的私鑰信息和申請的公鑰信息及所有相關數據。
利用JDK生成keystore證書
keytool -importkeystore -srckeystore server.p12 -destkeystore server.jks -srcstoretype pkcs12 -deststoretype jks
這樣生成的證書由於使用的是同一個私鑰文件,因此.jks文件與.crt文件是同源的。在多語言支持的大系統中它們可以相互認證,也便於統一管理。
請注意,牽涉到加密通信的系統往往都比較復雜,證書鏈都必須統一保存。很少會使用各自的工具在不同的場景下獨立使用,因此即使是Java開發者也依然應該掌握如何利用openssl生成完整證書的流程。
二、開發基於SSLEngine的非阻塞服務端
服務端的開發與客戶端區別不大,下面說明初始化和握手流程。其他部分的介紹可以參考我的上一篇博客。
服務端初始化
服務端初始化的過程除了需要監聽指定端口和處理客戶端連接以外,主要是需要初始化SSLContext。SSLContext是整個SSL通信的基礎,也可以認為是生成SSLEngine和SSLSession的工廠方法。具體通信的加解密過程又后者完成。
/** * 初始化 SSL安全層 */
private SSLContext sslContext;
private void initSSL() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, FileNotFoundException, IOException, UnrecoverableKeyException, KeyManagementException { sslContext = SSLContext.getInstance("SSL"); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); TrustManagerFactory tmf = TrustManagerFactory.getInstance("X.509"); KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream("server.jks"), keystorepass); kmf.init(ks, keypass); tmf.init(ks); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new java.security.SecureRandom()); }
server.jkd對應之前生成的證書文件,路徑根據自己項目的實際路徑指定。keystorepass和keypass是生成證書時輸入的秘鑰。
SSL握手和SSLEngine初始化
正如前文介紹的一樣,SSL握手協議中規定了交換秘鑰和協商對稱加密的過程。因此,實際上在JDK的抽象中,SSL的握手過程本質上是對SSLEngine的初始化。因此與客戶端不同的地方在於,服務端需要在有客戶端連接進入后再進行SSLEngine的初始化,並保證每一個新連接對應一個SSLEngine對象。當客戶端會話關閉后,釋放對應的SSLEngine。
/** * 服務端握手操作 */ private SSLEngine sslHandshake(SocketChannel socket) throws IOException { SSLEngine sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(false); SSLSession sslSession = sslEngine.getSession(); ByteBuffer remoteAppData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); ByteBuffer localNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize()); ByteBuffer remoteNetData = ByteBuffer.allocate(sslSession.getPacketBufferSize()); sslEngine.beginHandshake(); SSLEngineResult.HandshakeStatus 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; } } return sslEngine; }
其它與客戶端共性的部分,不再贅述。
三、HTTPS相關配置
配置tomcat
對於Java開發者而言,對Tomcat應該不陌生。下面的配置基於tomcat7。
conf/server.xml
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol" maxThreads="150" SSLEnabled="true" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreFile="server.jks" keystorePass="password" />
配置springboot
利用springboot開發微服務應用的時候,可以直接部署jar包。下面的配置基於springboot 2.x以上版本。
application.yml
server:
port: 8081
ssl:
key-store: classpath:server.jks
enabled: true
key-store-password: password
key-store-type: JKS
證書路徑為resources下或在啟動配置中自由指定。如果配置成功在啟動日志上會打印出8081(https)的相關消息,如果你需要讓容器同時支持http和https也可以利用@Configuration通過代碼加載配置,網上的資料很全,不再贅述。
不過這樣配置其實並不能完成前后端分離的訪問請求,因為瀏覽器轉發的時候會默認對證書進行驗證。由於我們的證書不是通過公認的CA機構簽發,因此會被默認阻止。當然你也可以通過設置讓瀏覽器放行,不過對於實際項目而言意義不大。
Android 6.0以上版本由於默認使用TLS通信,因此上面的配置可以應對移動端的訪問限制。下面的配置是針對后端僅支持http,android端的配置:
<application /*其它配置*/ android:usesCleartextTraffic="true">
總結
以我個人對未來的展望,由於微服務和高可用的部署會被應用到越來越多的場景中,各種語言間的相互調用會成為常態。底層硬件和一些對性能要求較高的場景自然需要C/C++作為支持,但是涉及業務層和互聯網方向的應用更多還是會以Java和其他高級語言為主。相互驅動,互為支持必不可少,而安全通信也會更加被重視。當然在實際的項目中,為了獲得更加穩定的支撐,我們可能會選擇使用框架。不過,能夠深入學習對開發者而言非常重要。我利用兩篇博客總結了在多語言支持下與安全通信相關的主要知識體系,基本以應用為主,少量結合了一些理論知識。希望能夠為大家的學習有所幫助。
相關博客:
