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 | 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); | ... | ... |