EMQX SSL双向认证配置与mqtt.fx客户端验证【仅供参考】
一准备工作
yum install openssl
yum install vim
在opt目录下创建ssl目录用来临时存储生成的证书文件
mkdir /opt/ssl
cd /opt/ssl/
cp /etc/pki/tls/openssl.cnf ./
rm -rf /etc/pki/CA/*.old
生成证书索引库数据库文件
touch /etc/pki/CA/index.txt
指定第一个颁发证书的序列号
echo 01 > /etc/pki/CA/serial
证书生成
方法一【这个是我这边测试,服务器与安卓客户端都可用的一种】
CA证书的生成
openssl req -x509 -new -days 3650 -keyout ca.key -out rootCA.crt -nodes
参数:
Country Name (2 letter code) [XX]:国家【中国---CN】
State or Province Name (full name) []:省份
Locality Name (eg, city) [Default City]:城市
Organization Name (eg, company) [Default Company Ltd]:组织名称
Organizational Unit Name (eg, section) []:组织单元名称
Common Name (eg, your name or your server's hostname) []:服务器IP
Email Address []: 邮箱地址
为server端生成证书
一、生成私钥
openssl genrsa -out server.key 2048
二、生成证书请求csr文件
openssl req -new -key server.key -out server.csr
参数填写与前面类似
A challenge password []: 密码
An optional company name []:公司名称
三、生成证书
openssl ca -in server.csr -out server.crt -cert rootCA.crt -keyfile ca.key -days 3650
为Client端生成证书
一、生成私钥:
openssl genrsa -out client.key 2048
二、生成证书请求:
openssl req -new -key client.key -out client.csr
参数与服务端证书生成类似,不过这里我用到ip是客户端ip
三、生成证书
openssl ca -in client.csr -out client.crt -cert rootCA.crt -keyfile ca.key
修改emqx/etc/emqx.conf配置文件
将生成的CA文件复制到/opt/emqx/etc/certs目录下
listener.ssl.external.keyfile = /opt/emqx/etc/certs/server.key
listener.ssl.external.certfile = /opt/emqx/etc/certs/server.crt
listener.ssl.external.cacertfile = /opt/emqx/etc/certs/rootCA.crt
listener.ssl.external.verify = verify_peer
listener.ssl.external.fail_if_no_peer_cert = true
方法二【并非所有场景都适合】
生成CA key和证书(为了方便我这里客户端与服务器共用一个)
生成CA key【采用2048字节】
openssl genrsa -out ca.key 2048
生成CA 证书【默认3650天】
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -subj "/CN=www.emqx.io" -out ca.pem
生成服务端key和证书
openssl genrsa -out server.key 2048
注意将IP修改为服务器IP
openssl req -new -key ./server.key -out server.csr -subj "/CN=127.0.0.1"
openssl x509 -req -in ./server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 3650 -sha256
生成客户端key和证书
openssl genrsa -out client.key 2048
注意将IP修改为客户端IP
openssl req -new -key ./client.key -out client.csr -subj "/CN=127.0.0.1"
openssl x509 -req -in ./client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem -days 3650 -sha256
修改emqx/etc/emqx.conf配置文件
将生成的CA文件复制到/opt/emqx/etc/certs目录下
## SSL Options
listener.ssl.external.handshake_timeout = 15
listener.ssl.external.keyfile = /etc/certs/server.key
listener.ssl.external.certfile = /etc/certs/server.pem
## 开启双向认证
listener.ssl.external.cacertfile = /etc/certs/ca.pem
listener.ssl.external.verify = verify_peer
listener.ssl.external.fail_if_no_peer_cert = true
启用用户名密码验证【可选】
emqx是默认开启匿名认证的,即客户端不需要任何认证信息即可连上emqx服务器,但生产环境这样肯定是不行的。emqx也支持很多种认证方式,这里我选着最简单的一种,用户名密码认证
#先要把emq的匿名认证关了,在emqx.conf文件
allow_anonymous = false
**重启emqx服务**
#加载用户名认证插件
./bin/emqx_ctl plugins load emqx_auth_username
#添加用户
./bin/emqx_ctl users add <Username> <Password>
使用MQTT.fx客户端验证是否配置成功
mqtt.fx下载地址:http://www.jensd.de/apps/mqttfx/
我这里选用的是windows1.7.1的版本
MQTT.fx客户端使用
创建emqx连接
若是启用了 用户名密码 认证,需要在这填写用户名密码信息
配置客户端ssl连接证书(方法一)
配置客户端ssl连接证书(方法二)
连接emqx
发送消息
编码实现
导入MAVEN依赖
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>
我这里需要将部分证书文件转换成我所需要的格式备用-【若是有其他的实现,可以不用参考我的】
cd 进入证书所在目录【方法二】
将ca.pem 与 client.pem 转化为.crt格式
openssl x509 -outform der -in your-ca.pem -out your-ca.crt
openssl x509 -outform der -in your-client.pem -out your-client.crt
将client.key转换为.pem文件【方法一、方法二通用,java代码连接需要】
openssl pkcs8 -topk8 -inform PEM -in client.key -outform PEM -nocrypt -out client-key-pkcs8.pem
将crt文件转化为p12 文件 再转换为bks文件 供安卓客户端使用
crt转p12
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12
p12 转bks
keytool -importkeystore -srckeystore client.p12 -srcstoretype pkcs12 -destkeystore client.bks -deststoretype bks -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-ext-jdk15on-157.jar
转换过程输入的密码即为证书创建时输入的密码
若是转换出来的文件不可用,请参考:https://blog.csdn.net/qq_36992688/article/details/78861883
Java代码
MQTT静态常量类
/**
* MQTT静态参数常量类
*/
public class MqttConstant {
//MQTT 服务器基础配置
/**
* MQTT服务器IP、端口
*/
public final static String MQTT_IP_PORT = "ssl://127.0.0.1:8883";
/**
* MQTT服务器登录 用户名 elinker
*/
public final static String MQTT_USERNAME = "username";
/**
* MQTT服务器登录 密码
*/
public final static String MQTT_PASSWORD = "password";
/**
* MQTT客户端【应用程序】 ID【自定义】
*/
public final static String MQTT_CLIENTID = "test";
/**
* MQTT客户端【应用程序】 订阅主题【自定义】
*/
public final static String MQTT_TOPIC = "web/pubClient";
//SSL双向认证 配置文件目录
/**
* CA证书
*/
public final static String SSL_CA_CRT = "ca.crt证书文件路径";
/**
* 客户端证书
*/
public final static String SSL_CLIENT_CRT = "client.crt证书文件路径";
/**
* 客户端证书key
*/
public final static String SSL_CLIENT_KEY_PKCS8_PEM = "client-key-pkcs8.pem证书文件路径";
/**
* 客户端证书密码
*/
public final static String SSL_CLIENT_PASSWORD = "client_password";
//消息发送的类型
/**
* 尽力而为。消息发送者会想尽办法发送消息,但是遇到意外并不会重试
*/
public final static int QOS_UNRELIABLE = 0;
/**
* 至少一次。消息接收者如果没有知会或者知会本身丢失,消息发送者会再次发送以保证消息接收者至少会收到一次,当然可能造成重复消息。
*/
public final static int QOS_REPEAT = 1;
/**
* 恰好一次。保证这种语义肯待会减少并发或者增加延时,不过丢失或者重复消息是不可接受的时候,级别2是最合适的。
*/
public final static int QOS_JUST = 2;
}
MQTT生产者客户端
/**
* 服务器【应用程序】Mqtt消息推送工具类
*/
public class MqttPushUtil {
public static Logger logger = Logger.getLogger(MqttPushUtil.class);
public static MqttClient mqttClient;
public static MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
static {
try{
//创建客户端
mqttClient = new MqttClient(MqttConstant.MQTT_IP_PORT, MqttConstant.MQTT_CLIENTID, new MemoryPersistence());
//创建链接参数
mqttConnectOptions = new MqttConnectOptions();
//在客户端断开连接时是否缓存 订阅消息
mqttConnectOptions.setCleanSession(true);
//设置连接的用户名
mqttConnectOptions.setUserName(MqttConstant.MQTT_USERNAME);
//设置连接的密码
mqttConnectOptions.setPassword(MqttConstant.MQTT_PASSWORD.toCharArray());
//开启自动重连
mqttConnectOptions.setAutomaticReconnect(true);
//设置超时时间 单位为秒
mqttConnectOptions.setConnectionTimeout(30);
//设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
mqttConnectOptions.setKeepAliveInterval(20);
//ssl 双向认证相关配置
SSLSocketFactory factory = EmqxSSLFactory.getSSLSocktet(MqttConstant.SSL_CA_CRT, MqttConstant.SSL_CLIENT_CRT,
MqttConstant.SSL_CLIENT_KEY_PKCS8_PEM, MqttConstant.SSL_CLIENT_PASSWORD);
mqttConnectOptions.setSocketFactory(factory);
// MqttPushServerUtil.setClient(mqttClient);
mqttClient.connect(mqttConnectOptions);
}catch (Exception e){
e.printStackTrace();
logger.error("连接MQTT服务器发生异常", e);
try{
mqttClient.disconnect();;
mqttClient.close();
}catch (MqttException el){
e.printStackTrace();
logger.error("连接异常时-----》断开与MQTT服务器连接操作发生异常", el);
}
}
}
public static void receiveSubscription(){
try{
mqttClient.setCallback(new MqttCallback() {
/**
*检测到断开连接
* @param throwable
*/
@Override
public void connectionLost(Throwable throwable) {
//MQTT客户端断线重连
reconnection();
}
/**
* 接收订阅消息处理
* @param topic
* @param mqttMessage
* @throws Exception
*/
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
System.out.println("topic:"+topic);
System.out.println("Qos:"+mqttMessage.getQos());
System.out.println("message content:"+new String(mqttMessage.getPayload()));
}
/**
* 消息发布结果
* @param iMqttDeliveryToken
*/
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
logger.info("MQTT消息发布的结果:---------"+ iMqttDeliveryToken.isComplete());
}
});
mqttClient.subscribe(MqttConstant.MQTT_TOPIC, MqttConstant.QOS_JUST);
}catch (Exception e){
e.printStackTrace();
logger.error("MQTT客户端---公租房系统:接收消息时发生异常!", e);
}
}
/**
* MQTT客户端断线重连
*/
public static void reconnection(){
logger.error("与MQTT服务器断开连接,尝试重新连接!");
try {
if(!mqttConnectOptions.isAutomaticReconnect()){
mqttClient.reconnect();
}
}catch (MqttException e){
logger.error("MQTT重新连接发生异常!", e);
}
}
/**
* 发布非可靠的消息【消息服务质量:0】
* @param topic 发布主题
* @param pushMessage 消息内容
*/
public static boolean publishUnreliable(String topic, String pushMessage) {
return publish(MqttConstant.QOS_UNRELIABLE, topic, pushMessage);
}
/**
* 以至少收到一次的模式发送消息【可能重复,消息服务质量:1】
* @param topic 发布主题
* @param pushMessage 消息内容
*/
public static boolean publishLeastOnce(String topic, String pushMessage) {
return publish(MqttConstant.QOS_REPEAT, topic, pushMessage);
}
/**
* 发送可靠的保证 能且只能收到一次的消息【消息服务质量:2】
* @param topic
* @param pushMessage
*/
public static boolean publishReliable(String topic, String pushMessage) {
return publish(MqttConstant.QOS_JUST, topic, pushMessage);
}
/**
* 发布主题和消息队列
* @param qos
* @param topic
* @param pushMessage
* @return
*/
public static boolean publish(int qos, String topic, String pushMessage) {
try {
// 创建消息
MqttMessage message = new MqttMessage(pushMessage.getBytes());
// 设置消息的服务质量
message.setQos(qos);
// 发布消息
mqttClient.publish(topic, message);
} catch (MqttException e) {
logger.error("发布消息时发生异常!",e);
return false;
}
return true;
}
}
EmqxSSL连接工厂类
public class EmqxSSLFactory {
public static javax.net.ssl.SSLSocketFactory getSSLSocktet(String caPath, String crtPath, String keyPath, String password) {
try{
CertificateFactory cAf = CertificateFactory.getInstance("X.509");
FileInputStream caIn = new FileInputStream(caPath);
X509Certificate ca = (X509Certificate) cAf.generateCertificate(caIn);
KeyStore caKs = KeyStore.getInstance("JKS");
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(caKs);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream crtIn = new FileInputStream(crtPath);
X509Certificate caCert = (X509Certificate) cf.generateCertificate(crtIn);
crtIn.close();
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("certificate", caCert);
ks.setKeyEntry("private-key", getPrivateKey(keyPath), password.toCharArray(),
new java.security.cert.Certificate[]{caCert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, password.toCharArray());
SSLContext context = SSLContext.getInstance("TLSv1");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return context.getSocketFactory();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static PrivateKey getPrivateKey(String path) throws Exception {
org.apache.commons.codec.binary.Base64 base64 = new Base64();
byte[] buffer = base64.decode(getPem(path));
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
}
private static String getPem(String path) throws Exception {
FileInputStream fin = new FileInputStream(path);
BufferedReader br = new BufferedReader(new InputStreamReader(fin));
String readLine = null;
StringBuilder sb = new StringBuilder();
while ((readLine = br.readLine()) != null) {
if (readLine.charAt(0) == '-') {
continue;
} else {
sb.append(readLine);
sb.append('\r');
}
}
fin.close();
return sb.toString();
}
}
测试类
public class MqttPushClientUtil {
public static void main(String[] args) throws MqttException {
String HOST = MqttConstant.MQTT_IP_PORT;
String[] TOPIC = {"dev/mac/+", "dev/village/+"};
int[] qos = new int[]{2, 2};
String clientid = "subClient";
String userName = "username";
String passWord = "password";
try {
// host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
MqttClient client = new MqttClient(HOST, clientid, new MemoryPersistence());
// MQTT的连接设置
MqttConnectOptions options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
SSLSocketFactory factory = EmqxSSLFactory.getSSLSocktet(MqttConstant.SSL_CA_CRT, MqttConstant.SSL_CLIENT_CRT,
MqttConstant.SSL_CLIENT_KEY_PKCS8_PEM, MqttConstant.SSL_CLIENT_PASSWORD);
options.setSocketFactory(factory);
options.setCleanSession(true);
// 设置连接的用户名
options.setUserName(userName);
// 设置连接的密码
options.setPassword(passWord.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 设置回调函数
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
System.out.println("connectionLost");
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
System.out.println("topic:"+topic);
System.out.println("Qos:"+message.getQos());
System.out.println("message content:"+new String(message.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------"+ token.isComplete());
}
});
client.connect(options);
//订阅消息
client.subscribe(TOPIC, qos);
} catch (Exception e) {
e.printStackTrace();
}
Map<String, Object> map = new HashMap<>();
map.put("index", 1);
map.put("text", "设备信息");
MqqtPushServiceUtil.pushDeviceMsg("00:00:a4:01:01:01", "12", map);
try{
Thread.sleep(5000);
}catch (Exception e){
e.printStackTrace();
}
map.put("index", 2);
map.put("text", "小区信息");
MqqtPushServiceUtil.pushVillageMsg(123, "12", map);
}
}
参考
证书格式转换: https://vimsky.com/article/3608.html
其他证书生成方式:
emqtt安全连接ssl配置 自签证书 单向认证连接 加密 ssl/tls,mosquitto客户端ssl单向认证连接测试
emqx使用自制CA证书登录配置(双向认证)
EMQ X 服务器 SSL/TLS 安全连接配置指南
MQTT研究之EMQ:【SSL双向验证】
MQTT研究之EMQ:【JAVA代码构建X509证书】
其他编码实现
MQTT Java客户端Eclipse paho实现数据的发送和接收
emqtt 试用(八)ssl认证 - 代码验证
MQTT.fx客户端安装与使用
MQTT.fx的安装和使用
EMQX权限验证
emqx服务器的权限验证(四)