Commit cc0d786d0fd62e075da186d45c4e504a22f9a769

Authored by YunaiV
1 parent 85c1b05b

feat(iot):【协议改造】emqx 初步改造(60%):支持 device register 设备注册

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java
... ... @@ -199,7 +199,7 @@ public class IotEmqxProtocol implements IotProtocol {
199 199 router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击
200 200  
201 201 // 2. 创建处理器
202   - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
  202 + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this);
203 203 router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
204 204 router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl);
205 205 router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
... ... @@ -517,4 +517,15 @@ public class IotEmqxProtocol implements IotProtocol {
517 517 .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e));
518 518 }
519 519  
  520 + /**
  521 + * 延迟发布消息到 MQTT Broker
  522 + *
  523 + * @param topic 主题
  524 + * @param payload 消息内容
  525 + * @param delayMs 延迟时间(毫秒)
  526 + */
  527 + public void publishDelayMessage(String topic, byte[] payload, long delayMs) {
  528 + vertx.setTimer(delayMs, id -> publishMessage(topic, payload));
  529 + }
  530 +
520 531 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxAuthEventHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.core.util.BooleanUtil;
4 5 import cn.hutool.core.util.StrUtil;
5 6 import cn.hutool.extra.spring.SpringUtil;
6 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 8 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 12 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  13 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
11 15 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
12 17 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
13 18 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
14 19 import io.vertx.core.json.JsonObject;
... ... @@ -24,6 +29,7 @@ import java.util.Locale;
24 29 * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)}
25 30 * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)}
26 31 * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)}
  32 + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)}
27 33 *
28 34 * @author 芋道源码
29 35 */
... ... @@ -57,13 +63,21 @@ public class IotEmqxAuthEventHandler {
57 63 */
58 64 private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
59 65  
  66 + /**
  67 + * 认证类型标识 - 设备注册
  68 + */
  69 + private static final String AUTH_TYPE_REGISTER = "|authType=register|";
  70 +
60 71 private final String serverId;
61 72  
  73 + private final IotEmqxProtocol protocol;
  74 +
62 75 private final IotDeviceMessageService deviceMessageService;
63 76 private final IotDeviceCommonApi deviceApi;
64 77  
65   - public IotEmqxAuthEventHandler(String serverId) {
  78 + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
66 79 this.serverId = serverId;
  80 + this.protocol = protocol;
67 81 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
68 82 this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
69 83 }
... ... @@ -91,7 +105,13 @@ public class IotEmqxAuthEventHandler {
91 105 return;
92 106 }
93 107  
94   - // 2. 执行认证
  108 + // 2.1 情况一:判断是否为注册请求
  109 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
  110 + handleDeviceRegister(context, username, password);
  111 + return;
  112 + }
  113 +
  114 + // 2.2 情况二:执行认证
95 115 boolean authResult = handleDeviceAuth(clientId, username, password);
96 116 log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
97 117 if (authResult) {
... ... @@ -380,4 +400,73 @@ public class IotEmqxAuthEventHandler {
380 400 }
381 401 }
382 402  
  403 + // ========= 注册处理 =========
  404 +
  405 + /**
  406 + * 处理设备注册请求(一型一密)
  407 + *
  408 + * @param context 路由上下文
  409 + * @param username 用户名
  410 + * @param password 密码(签名)
  411 + */
  412 + private void handleDeviceRegister(RoutingContext context, String username, String password) {
  413 + try {
  414 + // 1. 解析设备信息
  415 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  416 + if (deviceInfo == null) {
  417 + log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username);
  418 + sendAuthResponse(context, RESULT_DENY);
  419 + return;
  420 + }
  421 +
  422 + // 2. 调用注册 API
  423 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
  424 + .setProductKey(deviceInfo.getProductKey())
  425 + .setDeviceName(deviceInfo.getDeviceName())
  426 + .setSign(password);
  427 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  428 + result.checkError();
  429 +
  430 + // 3. 允许连接
  431 + log.info("[handleDeviceRegister][设备注册成功: {}]", username);
  432 + sendAuthResponse(context, RESULT_ALLOW);
  433 +
  434 + // 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅)
  435 + sendRegisterResultMessage(username, result.getData());
  436 + } catch (Exception e) {
  437 + log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage());
  438 + sendAuthResponse(context, RESULT_DENY);
  439 + }
  440 + }
  441 +
  442 + /**
  443 + * 发送注册结果消息给设备
  444 + * <p>
  445 + * 注意:延迟 5 秒发送,等待设备连接成功并完成订阅。
  446 + *
  447 + * @param username 用户名
  448 + * @param result 注册结果
  449 + */
  450 + @SuppressWarnings("DataFlowIssue")
  451 + private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) {
  452 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  453 + Assert.notNull(deviceInfo, "设备信息不能为空");
  454 + try {
  455 + // 1. 构建响应消息
  456 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
  457 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null);
  458 +
  459 + // 2. 编码消息
  460 + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, "Alink");
  461 +
  462 + // 3. 构建响应主题并延迟发布(等待设备连接成功并完成订阅)
  463 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method,
  464 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true);
  465 + protocol.publishDelayMessage(replyTopic, encodedData, 5000);
  466 + log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic);
  467 + } catch (Exception e) {
  468 + log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e);
  469 + }
  470 + }
  471 +
383 472 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -117,9 +117,9 @@ yudao:
117 117 # 针对引入的 MQTT 组件的配置
118 118 # ====================================
119 119 - id: mqtt-json
120   - enabled: false
  120 + enabled: true
121 121 protocol: mqtt
122   - port: 1884
  122 + port: 1883
123 123 serialize: json
124 124 mqtt:
125 125 max-message-size: 8192 # 最大消息大小(字节)
... ... @@ -129,7 +129,7 @@ yudao:
129 129 # 针对引入的 EMQX 组件的配置
130 130 # ====================================
131 131 - id: emqx-1
132   - enabled: true
  132 + enabled: false
133 133 protocol: emqx
134 134 port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event)
135 135 emqx:
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
... ... @@ -201,7 +201,12 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
201 201 MqttClient client = MqttClient.create(vertx, options);
202 202  
203 203 try {
204   - // 2. 设置消息处理器,接收注册响应
  204 + // 2. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
  205 + client.connect(SERVER_PORT, SERVER_HOST)
  206 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  207 + log.info("[testDeviceRegister][连接成功,等待注册响应...]");
  208 +
  209 + // 3.1 设置消息处理器,接收注册响应
205 210 CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
206 211 client.publishHandler(message -> {
207 212 log.info("[testDeviceRegister][收到响应: topic={}, payload={}]",
... ... @@ -209,11 +214,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
209 214 IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
210 215 responseFuture.complete(response);
211 216 });
212   -
213   - // 3. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
214   - client.connect(SERVER_PORT, SERVER_HOST)
215   - .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
216   - log.info("[testDeviceRegister][连接成功,等待注册响应...]");
  217 + // 3.2 订阅 _reply 主题
  218 + String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", PRODUCT_KEY, deviceName);
  219 + subscribe(client, replyTopic);
217 220  
218 221 // 4. 等待注册响应
219 222 IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
... ...