作為基于現(xiàn)代密碼學公鑰算法的安全協(xié)議,TLS/SSL 能在計算機通訊網(wǎng)絡(luò)上保證傳輸安全,EMQX 內(nèi)置對 TLS/SSL 的支持,包括支持單/雙向認證、X.509 證書、負載均衡 SSL 等多種安全認證。你可以為 EMQX 支持的所有協(xié)議啟用 SSL/TLS,也可以將 EMQX 提供的 HTTP API 配置為使用 TLS。
SSL/TLS 帶來的安全優(yōu)勢
-
強認證。?用 TLS 建立連接的時候,通訊雙方可以互相檢查對方的身份。在實踐中,很常見的一種身份檢查方式是檢查對方持有的 X.509 數(shù)字證書。這樣的數(shù)字證書通常是由一個受信機構(gòu)頒發(fā)的,不可偽造。
-
保證機密性。TLS 通訊的每次會話都會由會話密鑰加密,會話密鑰由通訊雙方協(xié)商產(chǎn)生。任何第三方都無法知曉通訊內(nèi)容。即使一次會話的密鑰泄露,并不影響其他會話的安全性。
-
完整性。?加密通訊中的數(shù)據(jù)很難被篡改而不被發(fā)現(xiàn)。
SSL/TLS 協(xié)議
TLS/SSL 協(xié)議下的通訊過程分為兩部分,第一部分是握手協(xié)議。握手協(xié)議的目的是鑒別對方身份并建立一個安全的通訊通道。握手完成之后雙方會協(xié)商出接下來使用的密碼套件和會話密鑰;第二部分是 record 協(xié)議,record 和其他數(shù)據(jù)傳輸協(xié)議非常類似,會攜帶內(nèi)容類型,版本,長度和荷載等信息,不同的是它所攜帶的信息是加密了的。
下面的圖片描述了 TLS/SSL 握手協(xié)議的過程,從客戶端的 "hello" 一直到服務(wù)器的 "finished" 完成握手。有興趣的同學可以找更詳細的資料看。對這個過程不了解也并不影響我們在?EMQX?中啟用這個功能。
為什么需要 SSL/TLS 雙向認證
雙向認證是指,在進行通信認證時要求服務(wù)端和客戶端都需要證書,雙方都要進行身份認證,以確保通信中涉及的雙方都是受信任的。 雙方彼此共享其公共證書,然后基于該證書執(zhí)行驗證、確認。一些對安全性要求較高的應用場景,就需要開啟雙向 SSL/TLS 認證。
SSL/TLS 證書準備
在雙向認證中,一般都使用自簽名證書的方式來生成服務(wù)端和客戶端證書,因此本文就以自簽名證書為例。
通常來說,我們需要數(shù)字證書來保證 TLS 通訊的強認證。數(shù)字證書的使用本身是一個三方協(xié)議,除了通訊雙方,還有一個頒發(fā)證書的受信第三方,有時候這個受信第三方就是一個 CA。和 CA 的通訊,一般是以預先發(fā)行證書的方式進行的。也就是在開始 TLS 通訊的時候,我們需要至少有 2 個證書,一個 CA 的,一個 EMQX 的,EMQX 的證書由 CA 頒發(fā),并用 CA 的證書驗證。
在這里,我們假設(shè)您的系統(tǒng)已經(jīng)安裝了 OpenSSL。使用 OpenSSL 附帶的工具集就可以生成我們需要的證書了。
生成自簽名 CA 證書
首先,我們需要一個自簽名的 CA 證書。生成這個證書需要有一個私鑰為它簽名,可以執(zhí)行以下命令來生成私鑰:
openssl genrsa -out ca.key 2048
?這個命令將生成一個密鑰長度為 2048 的密鑰并保存在?ca.key
?中。有了這個密鑰,就可以用它來生成 EMQX 的根證書了:
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem
根證書是整個信任鏈的起點,如果一個證書的每一級簽發(fā)者向上一直到根證書都是可信的,那個我們就可以認為這個證書也是可信的。有了這個根證書,我們就可以用它來給其他實體簽發(fā)實體證書了。
生成服務(wù)端證書
實體(在這里指的是 EMQX)也需要一個自己的私鑰對來保證它對自己證書的控制權(quán)。生成這個密鑰的過程和上面類似:
openssl genrsa -out emqx.key 2048
新建?openssl.cnf
?文件,
-
req_distinguished_name :根據(jù)情況進行修改,
-
alt_names:?
BROKER_ADDRESS
?修改為 EMQX 服務(wù)器實際的 IP 或 DNS 地址,例如:IP.1 = 127.0.0.1,或 DNS.1 = broker.xxx.com
注意:IP 和 DNS 二者保留其一即可,如果已購買域名,只需保留 DNS 并修改為你所使用的域名地址
[req]default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
countryName = CN
stateOrProvinceName = Zhejiang
localityName = Hangzhou
organizationName = EMQX
commonName = CA
[req_ext]
subjectAltName = @alt_names
[v3_req]
subjectAltName = @alt_names
[alt_names]
IP.1 = BROKER_ADDRESS
DNS.1 = BROKER_ADDRESS
然后以這個密鑰和配置簽發(fā)一個證書請求:
openssl req -new -key ./emqx.key -config openssl.cnf -out emqx.csr
然后以根證書來簽發(fā) EMQX 的實體證書:
openssl x509 -req -in ./emqx.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out emqx.pem -days 3650 -sha256 -extensions v3_req -extfile openssl.cnf
生成客戶端證書
雙向連接認證還需要創(chuàng)建客戶端證書,首先需要創(chuàng)建客戶端密鑰:
openssl genrsa -out client.key 2048
使用生成的客戶端密鑰來創(chuàng)建一個客戶端請求文件:
openssl req -new -key client.key -out client.csr -subj "/C=CN/ST=Zhejiang/L=Hangzhou/O=EMQX/CN=client"
最后使用先前生成好的服務(wù)端 CA 證書來給客戶端簽名,生成一個客戶端證書:
openssl x509 -req -days 3650 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem
準備好服務(wù)端和客戶端證書后,我們就可以在 EMQX 中啟用 TLS/SSL 雙向認證功能。
SSL/TLS 雙向連接的啟用及驗證
在 EMQX 中?mqtt:ssl
?的默認監(jiān)聽端口為 8883。
EMQX 配置
將前文中通過 OpenSSL 工具生成的?emqx.pem
、emqx.key
?及?ca.pem
?文件拷貝到 EMQX 的?etc/certs/
?目錄下,并參考如下配置修改?emqx.conf
:
## listener.ssl.$name is the IP address and port that the MQTT/SSL
## Value: IP:Port | Port
listener.ssl.external = 8883
## Path to the file containing the user's private PEM-encoded key.
## Value: File
listener.ssl.external.keyfile = etc/certs/emqx.key
## 注意:如果 emqx.pem 是證書鏈,請確保第一個證書是服務(wù)器的證書,而不是 CA 證書。
## Path to a file containing the user certificate.
## Value: File
listener.ssl.external.certfile = etc/certs/emqx.pem
## 注意:ca.pem 用于保存服務(wù)器的中間 CA 證書和根 CA 證書。可以附加其他受信任的 CA,用來進行客戶端證書驗證。
## Path to the file containing PEM-encoded CA certificates. The CA certificates
## Value: File
listener.ssl.external.cacertfile = etc/certs/ca.pem
## A server only does x509-path validation in mode verify_peer,
## as it then sends a certificate request to the client (this
## message is not sent if the verify option is verify_none).
##
## Value: verify_peer | verify_none
listener.ssl.external.verify = verify_peer
## Used together with {verify, verify_peer} by an SSL server. If set to true,
## the server fails if the client does not have a certificate to send, that is,
## sends an empty certificate.
##
## Value: true | false
listener.ssl.external.fail_if_no_peer_cert = true
創(chuàng)建 MQTT 連接
添加以下依賴到項目 pom.xml 文件中。文章來源:http://www.zghlxwxcb.cn/news/detail-682302.html
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
然后使用如下代碼創(chuàng)建 SSLUtils.java
文件。文章來源地址http://www.zghlxwxcb.cn/news/detail-682302.html
import cn.hutool.core.io.FileUtil;
import com.alibaba.cloud.commons.lang.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.springframework.core.io.ClassPathResource;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileReader;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
/**
* @author chenzhongchao
* @program hierway-guacamole
* @description
* @packagename com.hierway.modbus.client
* @date 2023-08-25 16:51
**/
public class SSLUtils {
public static SSLSocketFactory getSocketFactory( String caCrtFile,
String crtFile, String keyFile, String password)
throws Exception {
Security.addProvider(new BouncyCastleProvider());
if (StringUtils.isEmpty(password)){
password = "";
}
// load CA certificate
X509Certificate caCert = null;
String path = System.getProperty("user.dir");
if (!FileUtil.exist(caCrtFile)){
if (!FileUtil.exist(path+File.separator+caCrtFile)){
ClassPathResource classPathResource = new ClassPathResource(caCrtFile);
caCrtFile = classPathResource.getPath();
}else{
caCrtFile = path+File.separator+caCrtFile;
}
}
BufferedInputStream bis = FileUtil.getInputStream(caCrtFile);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
while (bis.available() > 0) {
caCert = (X509Certificate) cf.generateCertificate(bis);
}
// load client certificate
if (!FileUtil.exist(crtFile)){
if (!FileUtil.exist(path+File.separator+crtFile)){
ClassPathResource classPathResource = new ClassPathResource(crtFile);
crtFile = classPathResource.getPath();
}else{
crtFile = path+File.separator+crtFile;
}
}
bis = FileUtil.getInputStream(crtFile);
X509Certificate cert = null;
while (bis.available() > 0) {
cert = (X509Certificate) cf.generateCertificate(bis);
}
// load client private key
if (!FileUtil.exist(keyFile)){
if (!FileUtil.exist(path+File.separator+keyFile)){
ClassPathResource classPathResource = new ClassPathResource(keyFile);
keyFile = classPathResource.getPath();
}else{
keyFile = path+File.separator+keyFile;
}
}
PEMParser pemParser = new PEMParser(new FileReader(FileUtil.file(keyFile)));
Object object = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
KeyPair key = converter.getKeyPair((PEMKeyPair) object);
pemParser.close();
// CA certificate is used to authenticate server
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(caKs);
// client key and certificates are sent to server so it can authenticate
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("certificate", cert);
ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),
new java.security.cert.Certificate[]{cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());
// finally, create SSL socket factory
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return context.getSocketFactory();
}
}
MemoryPersistence persistence = new MemoryPersistence();
try {
//當未設(shè)置標識時隨機一個
if (StringUtils.isEmpty(clientId)) {
clientId = UUID.randomUUID().toString().replace("-", "");
}
mqttAsyncClient = new MqttAsyncClient(serverURI, clientId, persistence);
// MQTT 連接選項
MqttConnectOptions connOpts = new MqttConnectOptions();
//設(shè)置MQTT連接賬號密碼
connOpts.setUserName(userName);
if (!StringUtils.isEmpty(password)){
connOpts.setPassword(password.toCharArray());
}
connOpts.setCleanSession(true);
if (serverURI.indexOf("ssl")>=0){
SSLSocketFactory socketFactory = SSLUtils.getSocketFactory(cacert, clientCert, clientKey, clientPassword);
connOpts.setSocketFactory(socketFactory);
}
connOpts.setAutomaticReconnect(true);
// 設(shè)置心跳秒數(shù)
connOpts.setKeepAliveInterval(60);
// 設(shè)置回調(diào)
mqttReconnectCallback =new MqttReconnectCallback();
mqttAsyncClient.setCallback(mqttReconnectCallback);
mqttAsyncClient.connect(connOpts);
} catch (Exception e) {
}
到了這里,關(guān)于EMQX啟用雙向SSL/TLS安全連接以及java連接的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!