从零开始搭建物联网云平台03-MQTT消息处理设计
在IoTechn项目中,大致可分为四个部分 1.用户终端 2.设备终端 3.设备网关(MQTT Broker) 4.应用服务器。小程序、App等用户终端主要通过HTTP和WebSocket通信,这里不做讨论。
权限鉴定
对暴露在公网的服务,都需要进行权限鉴定,作为设备网关的MQTT Broker也不例外。IoTechn使用的Broker为EMQX。EMQX提供了多种鉴权插件,包括数据库,缓存等鉴权。但是为了能够灵活鉴权,我们采用的是HTTP鉴权。
鉴权方式确定后,剩下的就是鉴权的业务逻辑了。鉴权业务可分为两类
1.连接许可鉴权。
@RequestMapping("/auth")
public ResponseEntity<?> doAuth(String clientid, String ip, String username, String password) {
if (this.whiteIps.contains(ip)) {
return ResponseEntity.ok("ignore");
}
if (!clientid.equals(username)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden!");
}
try {
deviceBizService.auth(ip, username, password);
logger.info("[设备连接] accessKeyId=" + username);
return ResponseEntity.ok("ignore");
} catch (ServiceException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden!");
}
}
当设备连接来自于IP白名单时,则直接鉴权成功,此白名单主要配置内网环境中的应用服务器。若不是白名单,则直接将鉴权工作交给DeviceBizService进行权限鉴定。
@Override
@DS(Const.DB_S)
public String auth(String ip, String username, String password) throws ServiceException {
DeviceDO deviceDO = deviceMapper.selectOne(new QueryWrapper<DeviceDO>()
.select("id", "access_key_id", "access_key_secret", "auth_type")
.eq("access_key_id", username));
if (deviceDO == null) {
throw new DeviceServiceException(DeviceExceptionDefinition.DEVICE_DEVICE_NOT_EXIST);
}
// 进行权限鉴定
if (deviceDO.getAuthType().intValue() == DeviceAuthType.EASY.getCode()) {
if (!deviceDO.getAccessKeySecret().equals(password)) {
throw new DeviceServiceException(DeviceExceptionDefinition.DEVICE_PERMISSION_DENY);
}
} else if (deviceDO.getAuthType().intValue() == DeviceAuthType.SAFE.getCode()) {
if (!MD5Util.md5(ip + deviceDO.getAccessKeySecret()).equalsIgnoreCase(password)) {
throw new DeviceServiceException(DeviceExceptionDefinition.DEVICE_PERMISSION_DENY);
}
} else {
throw new DeviceServiceException(DeviceExceptionDefinition.DEVICE_UNKNOWN_EXCEPTION);
}
return "ok";
}
设备鉴权鉴权密码分为两个模式,
1.简单模式,直接将AccessKeySecret作为鉴权密码,这样可降低接入门槛。
2.MD5模式,将MD5(IP + AccessKeySecret)作为鉴权密码,MD5模式的好处就是,即使被截获,若是没有黑到到该IP的跳板机,也无法鉴权成功。
2.Topic读写权限。
@RequestMapping("/acl")
public ResponseEntity<?> doAcl(String clientid, String username, String ip, String topic, Integer access) throws IOException {
if (this.whiteIps.contains(ip)) {
return ResponseEntity.ok("ignore");
}
if (topic.equals(DeviceConst.TOPIC_UP + username)) {
return ResponseEntity.ok("ignore");
}
if (topic.equals(DeviceConst.TOPIC_DOWN + username) && access == 1) {
// 命令TOPIC只允许白名单IP(即内网写)
return ResponseEntity.ok("ignore");
}
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden!");
}
一个设备可对两个TOPIC进行访问,一个下行TOPIC一个上行
消息处理
欲使服务器和硬件之间能够完成消息传递,大致可分为这几个步骤 1.构建消息体 2.消息序列化 3.网络流传输 4.消息反序列化 5.消息处理
以上行消息为例,1&2 是在设备上完成 3 属于二者共同完成 4&5 则是在服务器上完成。反之亦然。
对于服务器而言第一步要做的事就是消息反序列化, 所以定义了一个反序列化结构
public interface IotMqttDeserializer {
public <T> T parseObject(byte[] payload, Class<T> clazz);
public <T> List<T> parseArray(byte[] payload, Class<T> clazz);
}
第二部则是需要对消息进行处理,所以定义了消息处理接口
public interface IotMqttMessageHandler<T> {
public int handle(String accessKeyId, T msg);
public int getCode();
public Class<T> getMessageType();
}
IotMqttMessageHandler中定义了三个方法
1.handle 处理器核心方法 ,用于处理消息
2.getCode 获取消息类型Code,某一个Handler只能处理特定Code的消息。
3.getMessageType 获取反序列化结果的Java Class对象,主要用于反序列化
问:如何通过设备的消息找到具体的处理器?
答:通过消息Code 和 消息处理Handler映射来Hash表来找,所以你发现getCode有什么用了。
@Bean
public Map<Integer, IotMqttMessageHandler> messageHandleRouter(List<IotMqttMessageHandler> iotMqttConnectMessageHandlerList) {
return iotMqttConnectMessageHandlerList.stream().collect(Collectors.toMap(IotMqttMessageHandler::getCode, v -> v));
}
消息下行
通过MQTTClient for Java往设备下行Topic发送即可