參考上文,上文完成了 MQTT Server 的基本功能,但是還差了 SSL 的功能。
一. 添加過程
以 Netty 為基礎(chǔ)加上 SSL 功能比較簡單,幾行代碼就可以
// 創(chuàng)建SSL上下文
if (option.isSsl()) {
sslContext = SslContextBuilder.forServer(
new File(option.getServerCertFile()),
new File(option.getKeyFile()))
.trustManager(new File(option.getCaCertFile())).build();
}
......
if(sslContext!=null) {
// 將SSL上下文添加到ChannelPipeline中
channelPipeline.addLast(sslContext.newHandler(ch.alloc()));
}
需要證書文件的提供,包括3個(gè)文件
/**
* CA證書文件,一般叫ca.crt
* 指令參考:openssl req -new -x509 -keyout ca.key -out ca.crt -days 36500
*/
private String caCertFile;
/**
* an X.509 certificate chain file in PEM format
* server證書文件,一般叫server.crt
* 指令參考:openssl x509 -req -days 36500 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
* ,如果需要和域名或ip綁定需要加上 -extfile <(printf "subjectAltName=IP:127.0.0.1")
*/
private String serverCertFile;
/**
* a PKCS#8 private key file in PEM format
* 私鑰文件,因?yàn)閚etty只支持pkcs8,所以需要2個(gè)步驟,第二個(gè)步驟改成pkcs8格式
* openssl genrsa -des3 -out server.key 1024
* openssl pkcs8 -topk8 -in server.key -out pkcs8_server.key -nocrypt
*/
private String keyFile;
二. 證書創(chuàng)建
主要參考 https://www.cnblogs.com/exmyth/p/14808872.html
1.生成ca證書
openssl req -new -x509 -keyout ca.key -out ca.crt -days 36500
在本目錄得到 ca.key 和 ca.crt 文件
2.生成服務(wù)端和客戶端私鑰
openssl genrsa -des3 -out server.key 1024
openssl genrsa -des3 -out client.key 1024
3.根據(jù) key 生成 csr 文件
openssl req -new -key server.key -out server.csr
openssl req -new -key client.key -out client.csr
4.根據(jù) ca 證書 server.csr 和 client.csr 生成 x509 證書
openssl x509 -req -days 36500 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
openssl x509 -req -days 36500 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
需要和ip綁定則加上參數(shù),我都是在本地測試,所以設(shè)置ip為127.0.0.1
openssl x509 -req -days 36500 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -extfile <(printf "subjectAltName=IP:127.0.0.1")
openssl x509 -req -days 36500 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -extfile <(printf "subjectAltName=IP:127.0.0.1")
5.將 key 文件進(jìn)行 PKCS#8 編碼
openssl pkcs8 -topk8 -in server.key -out pkcs8_server.key -nocrypt
openssl pkcs8 -topk8 -in client.key -out pkcs8_client.key -nocrypt
最后得到有用的文件分別為
服務(wù)器端: ca.crt、server.crt、pkcs8_server.key
客戶端端: ca.crt、client.crt、pkcs8_client.key
我這里生成了一套在 cert 目錄下,大家可以測試使用。
三. 碰到的問題
因?yàn)橐粋€(gè)老的 bug 導(dǎo)致一致沒有調(diào)通。
/**
* 服務(wù)端 當(dāng)讀超時(shí)時(shí) 會(huì)調(diào)用這個(gè)方法
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
logger.info("MQTT Server Channel timeout:{}", ctx.channel().id().asLongText());
ctx.close();
}
這個(gè)地方有一個(gè)錯(cuò)誤,userEventTriggered 并不只是超時(shí)的時(shí)候才會(huì)觸發(fā),以前在很多項(xiàng)目下沒有引起問題是因?yàn)橐郧耙恢睕]有用到 SSL ,加上 SSL 就導(dǎo)致錯(cuò)誤了,因?yàn)?SSL 會(huì)在連接的時(shí)候會(huì)觸發(fā)這個(gè)事件函數(shù),但是我強(qiáng)行關(guān)閉連接了,導(dǎo)致一直有問題。最后改成以下代碼就可以了:
/**
* 服務(wù)端 當(dāng)讀超時(shí)時(shí) 會(huì)調(diào)用這個(gè)方法,SSL的時(shí)候也會(huì)觸發(fā)SslCloseCompletionEvent,SslHandshakeCompletionEvent
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
logger.info("MQTT Server Channel Event:{},{}", ctx.channel().id().asLongText(), evt.toString());
if (evt instanceof IdleStateEvent) {
ctx.close();
}
}
四. 測試
用了3種方式來測試
-
利用客戶端工具 MQTTX
微信圖片_20230818173235.png
配置如上圖,證書其中 key 文件選擇的是 pkcs8 格式的,因?yàn)閜kcs12格式的文件有密碼,但是這個(gè)工具沒有找到設(shè)置密碼的地方。
利用Netty實(shí)現(xiàn) java 的 mqtt 客戶端,主要設(shè)置 SSL 的代碼就二句代碼
final SslContext sslCtx = SslContextBuilder.forClient()
//雙向驗(yàn)證
.keyManager(new File("D:\\Work\\framework\\mqttserver-duoxian\\cert\\client.crt"),
new File("D:\\Work\\framework\\mqttserver-duoxian\\cert\\pkcs8_client.key"))
.trustManager(new File("D:\\Work\\framework\\mqttserver-duoxian\\cert\\ca.crt"))// CA證書,驗(yàn)證對方證書
// 不驗(yàn)證SERVER
// .trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
......
ch.pipeline().addLast(sslCtx.newHandler(ch.alloc()));
基本和服務(wù)端的用戶類似,如果不用雙向認(rèn)證就只需要用到 ca.crt 。
注意 netty 只支持 pkcs8 格式的密鑰文件。
這個(gè)測試類叫 MqttSslClientNetty.java 大家可以在 git 的 src 下目錄找到。
- 利用 eclipse.paho 的 mqtt 庫
這個(gè)也是 MQTT 的常用庫,寫法比 netty 復(fù)雜一些,主要代碼如下:
// 加載密鑰文件和證書文件
String privateKeyFile = "D:\\Work\\framework\\mqttserver-duoxian\\cert\\client.key";
String certificateFile = "D:\\Work\\framework\\mqttserver-duoxian\\cert\\client.crt";
String caficateFile = "D:\\Work\\framework\\mqttserver-duoxian\\cert\\ca.crt";
//雙向認(rèn)證
//SSLSocketFactory socketFactory = getSocketFactory(caficateFile, certificateFile, privateKeyFile, "123456");
//單向認(rèn)證
SSLSocketFactory socketFactory = getSocketFactory(caficateFile);
同樣單向認(rèn)證只需要用到 ca 證書。那個(gè) getSocketFactory 函數(shù)有2個(gè)實(shí)現(xiàn),分別對應(yīng)單向驗(yàn)證和雙向驗(yàn)證。主要是用到了 org.bouncycastle.openssl 庫。
注意這里必須用 pkcs12 格式的密鑰文件。
這個(gè)測試類叫 MqttSslClientPaho.java 大家可以在 git 的 src 下目錄找到。
所有代碼都已經(jīng)提交到 git
