一、Maven依赖
<!-- MQTT 相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
<!-- 证书加密 -->
<dependency>
<groupId>bouncycastle</groupId>
<artifactId>bcprov-jdk15</artifactId>
<version>140</version>
</dependency>
二、yml文件配置
mqtt:
username: test # 账号
password: abc123 # 密码
urls: ssl://220.178.10.89:9919 # mqtt连接tcp地址
clientId: 3401000011 # 客户端Id,每个启动的id要不同
topics: topic1,topic2 # 订阅的主题,多个逗号分隔
timeout: 300000 # 超时时间
keepalive: 600000 #心跳时间,即多长时间未收到数据认为断连
crtFilePath: C://DevTest证书/AlanDevTest.crt
三、配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* MQTT配置类
*
* @date 2020/12/14
*/
@Data
@Configuration
@ConfigurationProperties("spring.mqtt")
public class MqttProperties {
private String urls;
private String username;
private String password;
private String clientId;
private String topics;
private Integer timeout;
private Integer keepalive;
}
四、订阅配置类
import chc.sync.util.SslUtil;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
@Configuration
@Slf4j
public class MqttInboundConfiguration {
@Autowired
private MqttProperties mqttProperties;
@Value("${spring.mqtt.crtFilePath}")
private String crtFilePath;
@Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
@Bean
public MqttConnectOptions getMqttConnectOptions() {
MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
//再次重连接时,还是原来的session,就可以收到离线消息
mqttConnectOptions.setCleanSession(false);
mqttConnectOptions.setServerURIs(mqttProperties.getUrls().split(","));
mqttConnectOptions.setKeepAliveInterval(mqttProperties.getKeepalive());
mqttConnectOptions.setMaxInflight(1000);
mqttConnectOptions.setUserName(mqttProperties.getUsername());
mqttConnectOptions.setPassword(mqttProperties.getPassword().toCharArray());
mqttConnectOptions.setAutomaticReconnect(true);
try {
mqttConnectOptions.setSocketFactory(SslUtil.getSocketFactory(crtFilePath));
} catch (Exception e) {
log.info("证书读取失败");
}
return mqttConnectOptions;
}
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setConnectionOptions(getMqttConnectOptions());
return factory;
}
@Bean
public MessageProducer inbound(MqttPahoClientFactory mqttPahoClientFactory) {
String[] inboundTopics = mqttProperties.getTopics().split(",");
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getClientId(),
mqttPahoClientFactory, inboundTopics);
adapter.setCompletionTimeout(mqttProperties.getTimeout());
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
@Bean
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return message -> {
MessageHeaders headers = message.getHeaders();
String receivedTopic = (String) headers.get("mqtt_receivedTopic");
Object payload = message.getPayload();
log.info("主题:{} ;收到消息:{}", receivedTopic, payload);
};
}
}
五、推送消息配置类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@Configuration
public class MqttOutboundConfiguration {
@Autowired
private MqttProperties mqttProperties;
@Autowired
private MqttPahoClientFactory mqttPahoClientFactory;
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound() {
MqttPahoMessageHandler messageHandler =
new MqttPahoMessageHandler(mqttProperties.getClientId(), mqttPahoClientFactory);
messageHandler.setAsync(true);
messageHandler.setDefaultTopic(mqttProperties.getTopics());
return messageHandler;
}
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
}
六、发送消息的接口
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
/**
* 发送信息
*
* @param data 消息内容
*/
void sendToMqtt(String data);
/**
* 指定主题发送信息
*
* @param topic 主题
* @param payload 消息内容
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
/**
* 指定主题和qos发送信息
*
* @param topic 主题
* @param qos 服务质量
* @param payload 消息内容
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
/**
* 指定主题、qos和消息类型发送信息
*
* @param topic 主题
* @param qos 服务质量
* @param payload 消息内容
* @param retained 是否保留消息
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, @Header(MqttHeaders.RETAINED) boolean retained, String payload);
}
七、证书工具类
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class SslUtil {
/**
* 获取 tls 安全套接字工厂 (单向认证,服务器端认证)
*
* @param caCrtFile null:使用系统默认的 ca 证书来验证。 非 null:指定使用的 ca 证书来验证服务器的证书。
* @return tls 套接字工厂
* @throws Exception
*/
public static SSLSocketFactory getSocketFactory(final String caCrtFile) throws NoSuchAlgorithmException, IOException, KeyStoreException, CertificateException, KeyManagementException {
Security.addProvider(new BouncyCastleProvider());
//===========加载 ca 证书==================================
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
if (null != caCrtFile) {
// 加载本地指定的 ca 证书
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(caCrtFile)))));
X509Certificate caCert = (X509Certificate) 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);
// 把ca作为信任的 ca 列表,来验证服务器证书
tmf.init(caKs);
} else {
//使用系统默认的安全证书
tmf.init((KeyStore) null);
}
// ============finally, create SSL socket factory==============
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(null, tmf.getTrustManagers(), null);
return context.getSocketFactory();
}
/**
* 双向认证
*/
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();
}
}
八、实际配置使用时遇到的坑
- TLS和SSL实际是一样的,配置URL时前缀由tcp://改为ssl://
- 使用加密传输,必须有认证证书,需要MQTT服务器搭建方给出;
- 一个Client最多只能有一个连接在连,否则多方会互挤,一直断连。
九、遗留问题
- 断连重连后,订阅的同一主题(主题内存有retain消息)下可能收到多条重复消息
- 不能一直保持连接,时常出现断连
十、参考地址
https://blog.csdn.net/ko0491/article/details/103530090