物联网 MQTT通信升级安全加密MQTTS

477 阅读3分钟

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();}