MQTT中间件 采用RabbiMQ 提供的mqtt插件实现。
第一步:mqtt中间件 升级支持 mqtts通信
项目中采用RabbiMQ提供的mqtt插件,通过以下配置实现RabbitMQ支持SSL,开放mqtts访问端口。
#CA 机构证书,双向认证的时候需要,单向认证可以不用,
#ssl_options.cacertfile = /home/ssl/ca.crt
#服务端 SSL 认证文件
ssl_options.certfile = /home/ssl/server.crtssl_options.keyfile = /home/ssl/server.key#RabbitMQ SSL 对等校验配置
# 可选值 verify_none verify_peer
#verify_peer 启用SSL对等校验
#verify_none 禁用SSL对等校验
ssl_options.verify = verify_none
#可选值 true false
# true 拒绝不提供SSL证书的客户端
# false 允许不提供SSL证书的客户端
ssl_options.fail_if_no_peer_cert = false# default TLS-enabled port for MQTT connectionsmqtt.listeners.ssl.default = 8883mqtt.listeners.tcp.default = 1883
重启RabbitMQ,通过查看日志可确认TSL配置是否成功开启,以下为启动成功示例,可以通过查看启用端口,确定开启成功。
started MQTT TLS listener on [::]:8883
started TLS (SSL) listener on [::]:5671
2023-10-10 11:06:14.921424+08:00 [info] <0.1398.0> MQTT retained message store: rabbit_mqtt_retained_msg_store_dets
2023-10-10 11:06:14.923332+08:00 [info] <0.1419.0> started MQTT TCP listener on [::]:1883
2023-10-10 11:06:14.926033+08:00 [info] <0.1439.0> started MQTT TLS listener on [::]:8883
2023-10-10 11:06:14.930647+08:00 [info] <0.669.0> Ready to start client connection listeners
2023-10-10 11:06:14.933348+08:00 [info] <0.1486.0> started TCP listener on [::]:5672
2023-10-10 11:06:14.936841+08:00 [info] <0.1506.0> started TLS (SSL) listener on [::]:5671
可能出现问题:
SSL认证文件加载失败,提示文件不存在或者不可读取
2023-10-10 11:05:27.581459+08:00 [error] <0.130.0> Error preparing configuration in phase validation:
2023-10-10 11:05:27.581522+08:00 [error] <0.130.0> - ssl_options.keyfile invalid, file does not exist or cannot be read by the node
排查:
1、检查配置文件中的路径和SSL认证文件位置是否一致。若采用docker部署的,需要检查SSL认证文件的目录是否挂载到容器,配置文件路径是否和容器内的挂载目录是否一致。
2、检查SSL认证文件权限
官网提示:The files must exist and have the appropriate permissions。未说明需要哪些权限,赋予777权限肯定是可以的,其他的权限未验证。
第二步:客户端升级
开放语言Java,
依赖:
org.eclipse.paho.client.mqttv3 :提供mqtt客户端工具
bcpkix-jdk15on:提供SSL认证文件加载
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.47</version></dependency>
<dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId></dependency>
核心代码
mqtt连接属性配置,
MQTT 下 hostUrl = tcp://192.168.20.211:1883;
MQTTS 下 hostUrl = ssl://192.168.20.211:8883;
@Beanpublic MqttConnectOptions mqttConnectOptions() { MqttConnectOptions options = new MqttConnectOptions(); // 设置连接的用户名 if (!username.trim().equals("")) { options.setUserName(username); } options.setCleanSession(false); // 设置连接的密码 options.setPassword(password.toCharArray()); // 设置连接的地址 options.setServerURIs(new String[]{hostUrl}); //TSL支持需要加载 SSLSocketFactory,普通通信不用可以注释 try { options.setSocketFactory(getSocketFactory("D://ca.crt", "D://cli.crt", "D://client.key", "")); } catch (Exception e) { } // 设置超时时间 单位为秒 options.setConnectionTimeout(15); // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线 // 但这个方法并没有重连的机制 options.setKeepAliveInterval(10); // 设置“遗嘱”消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的“遗嘱”消息。 options.setWill(willTopic, offlineMessage.getBytes(StandardCharsets.UTF_8), 2, true); return options;}
启用双向认证,客户端需要加载SSL证书,创建SSL通信通道。
SSL验证文件加载。
public static SSLSocketFactory getSocketFactory(final String caCrtFile, final String crtFile, final String keyFile, final String password) throws Exception { Security.addProvider(new BouncyCastleProvider()); // load CA certificate PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(caCrtFile))))); X509Certificate caCert = (X509Certificate)reader.readObject(); reader.close(); // load client certificate reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(crtFile))))); X509Certificate cert = (X509Certificate)reader.readObject(); reader.close(); // load client private key reader = new PEMReader( new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyFile)))), new PasswordFinder() { @Override public char[] getPassword() { return password.toCharArray(); } } ); KeyPair key = (KeyPair)reader.readObject(); reader.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(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(caKs); // client key and certificates are sent to server so it can authenticate us 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();}
启用单向认证,客户端不加载SSL验证文件
public static SSLSocketFactory getSocketFactory(final String caCrtFile, final String crtFile, final String keyFile, final String password) throws Exception { // finally, create SSL socket factory SSLContext context = SSLContext.getInstance("TLSv1.2"); context.init(null, new X509TrustManager[]{new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }}, new SecureRandom()); return context.getSocketFactory();}