Commit cc0d786d0fd62e075da186d45c4e504a22f9a769
1 parent
85c1b05b
feat(iot):【协议改造】emqx 初步改造(60%):支持 device register 设备注册
Showing
4 changed files
with
115 additions
and
12 deletions
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,7 +199,7 @@ public class IotEmqxProtocol implements IotProtocol { | ||
| 199 | router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 | 199 | router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB,防止大包攻击 |
| 200 | 200 | ||
| 201 | // 2. 创建处理器 | 201 | // 2. 创建处理器 |
| 202 | - IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId); | 202 | + IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this); |
| 203 | router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); | 203 | router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth); |
| 204 | router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); | 204 | router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl); |
| 205 | router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); | 205 | router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent); |
| @@ -517,4 +517,15 @@ public class IotEmqxProtocol implements IotProtocol { | @@ -517,4 +517,15 @@ public class IotEmqxProtocol implements IotProtocol { | ||
| 517 | .onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e)); | 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 | package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 3 | import cn.hutool.core.util.BooleanUtil; | 4 | import cn.hutool.core.util.BooleanUtil; |
| 4 | import cn.hutool.core.util.StrUtil; | 5 | import cn.hutool.core.util.StrUtil; |
| 5 | import cn.hutool.extra.spring.SpringUtil; | 6 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | 8 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | 9 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 10 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 11 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 10 | import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; | 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 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | 15 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 16 | +import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol; | ||
| 12 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 17 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | 18 | import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; |
| 14 | import io.vertx.core.json.JsonObject; | 19 | import io.vertx.core.json.JsonObject; |
| @@ -24,6 +29,7 @@ import java.util.Locale; | @@ -24,6 +29,7 @@ import java.util.Locale; | ||
| 24 | * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} | 29 | * 1. 设备认证接口 - 对应 EMQX HTTP 认证插件 {@link #handleAuth(RoutingContext)} |
| 25 | * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} | 30 | * 2. 设备事件处理接口 - 对应 EMQX Webhook 事件通知 {@link #handleEvent(RoutingContext)} |
| 26 | * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} | 31 | * 3. 设备 ACL 权限接口 - 对应 EMQX HTTP ACL 插件 {@link #handleAcl(RoutingContext)} |
| 32 | + * 4. 设备注册接口 - 集成一型一密设备注册 {@link #handleDeviceRegister(RoutingContext, String, String)} | ||
| 27 | * | 33 | * |
| 28 | * @author 芋道源码 | 34 | * @author 芋道源码 |
| 29 | */ | 35 | */ |
| @@ -57,13 +63,21 @@ public class IotEmqxAuthEventHandler { | @@ -57,13 +63,21 @@ public class IotEmqxAuthEventHandler { | ||
| 57 | */ | 63 | */ |
| 58 | private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected"; | 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 | private final String serverId; | 71 | private final String serverId; |
| 61 | 72 | ||
| 73 | + private final IotEmqxProtocol protocol; | ||
| 74 | + | ||
| 62 | private final IotDeviceMessageService deviceMessageService; | 75 | private final IotDeviceMessageService deviceMessageService; |
| 63 | private final IotDeviceCommonApi deviceApi; | 76 | private final IotDeviceCommonApi deviceApi; |
| 64 | 77 | ||
| 65 | - public IotEmqxAuthEventHandler(String serverId) { | 78 | + public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) { |
| 66 | this.serverId = serverId; | 79 | this.serverId = serverId; |
| 80 | + this.protocol = protocol; | ||
| 67 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | 81 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 68 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); | 82 | this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); |
| 69 | } | 83 | } |
| @@ -91,7 +105,13 @@ public class IotEmqxAuthEventHandler { | @@ -91,7 +105,13 @@ public class IotEmqxAuthEventHandler { | ||
| 91 | return; | 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 | boolean authResult = handleDeviceAuth(clientId, username, password); | 115 | boolean authResult = handleDeviceAuth(clientId, username, password); |
| 96 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); | 116 | log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult); |
| 97 | if (authResult) { | 117 | if (authResult) { |
| @@ -380,4 +400,73 @@ public class IotEmqxAuthEventHandler { | @@ -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,9 +117,9 @@ yudao: | ||
| 117 | # 针对引入的 MQTT 组件的配置 | 117 | # 针对引入的 MQTT 组件的配置 |
| 118 | # ==================================== | 118 | # ==================================== |
| 119 | - id: mqtt-json | 119 | - id: mqtt-json |
| 120 | - enabled: false | 120 | + enabled: true |
| 121 | protocol: mqtt | 121 | protocol: mqtt |
| 122 | - port: 1884 | 122 | + port: 1883 |
| 123 | serialize: json | 123 | serialize: json |
| 124 | mqtt: | 124 | mqtt: |
| 125 | max-message-size: 8192 # 最大消息大小(字节) | 125 | max-message-size: 8192 # 最大消息大小(字节) |
| @@ -129,7 +129,7 @@ yudao: | @@ -129,7 +129,7 @@ yudao: | ||
| 129 | # 针对引入的 EMQX 组件的配置 | 129 | # 针对引入的 EMQX 组件的配置 |
| 130 | # ==================================== | 130 | # ==================================== |
| 131 | - id: emqx-1 | 131 | - id: emqx-1 |
| 132 | - enabled: true | 132 | + enabled: false |
| 133 | protocol: emqx | 133 | protocol: emqx |
| 134 | port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) | 134 | port: 8090 # EMQX HTTP Hook 端口(/mqtt/auth、/mqtt/event) |
| 135 | emqx: | 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,7 +201,12 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 201 | MqttClient client = MqttClient.create(vertx, options); | 201 | MqttClient client = MqttClient.create(vertx, options); |
| 202 | 202 | ||
| 203 | try { | 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 | CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); | 210 | CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>(); |
| 206 | client.publishHandler(message -> { | 211 | client.publishHandler(message -> { |
| 207 | log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", | 212 | log.info("[testDeviceRegister][收到响应: topic={}, payload={}]", |
| @@ -209,11 +214,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | @@ -209,11 +214,9 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 209 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); | 214 | IotDeviceMessage response = CODEC.decode(message.payload().getBytes()); |
| 210 | responseFuture.complete(response); | 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 | // 4. 等待注册响应 | 221 | // 4. 等待注册响应 |
| 219 | IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); | 222 | IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); |