Commit 83990086fa8b630276d7847fef73048cc03e1858

Authored by YunaiV
1 parent 9156aef4

feat(iot):【协议改造】设备注册,跟阿里云 iot 进一步对齐,使用 sign 替代 password 参数

Showing 27 changed files with 240 additions and 211 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
... ... @@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
29 29 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
30 30 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
31 31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  32 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
32 33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
33 34 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
34 35 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
... ... @@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
819 820 if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
820 821 throw exception(DEVICE_REGISTER_DISABLED);
821 822 }
822   - // 1.3 验证 productSecret
823   - if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
  823 + // 1.3 【重要!!!】验证签名
  824 + if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(),
  825 + product.getProductSecret(), reqDTO.getSign())) {
824 826 throw exception(DEVICE_REGISTER_SECRET_INVALID);
825 827 }
826 828 return TenantUtils.execute(product.getTenantId(), () -> {
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
... ... @@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO {
27 27 private String deviceName;
28 28  
29 29 /**
30   - * 产品密钥
  30 + * 注册签名
  31 + *
  32 + * @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
31 33 */
32   - @NotEmpty(message = "产品密钥不能为空")
33   - private String productSecret;
  34 + @NotEmpty(message = "签名不能为空")
  35 + private String sign;
34 36  
35 37 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotProductAuthUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.crypto.digest.DigestUtil;
  5 +import cn.hutool.crypto.digest.HmacAlgorithm;
  6 +
  7 +/**
  8 + * IoT 产品【动态注册】认证工具类
  9 + * <p>
  10 + * 用于一型一密场景,使用 productSecret 生成签名
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class IotProductAuthUtils {
  15 +
  16 + /**
  17 + * 生成设备动态注册签名
  18 + *
  19 + * @param productKey 产品标识
  20 + * @param deviceName 设备名称
  21 + * @param productSecret 产品密钥
  22 + * @return 签名
  23 + */
  24 + public static String buildSign(String productKey, String deviceName, String productSecret) {
  25 + String content = buildContent(productKey, deviceName);
  26 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
  27 + .digestHex(content);
  28 + }
  29 +
  30 + /**
  31 + * 验证设备动态注册签名
  32 + *
  33 + * @param productKey 产品标识
  34 + * @param deviceName 设备名称
  35 + * @param productSecret 产品密钥
  36 + * @param sign 待验证的签名
  37 + * @return 是否验证通过
  38 + */
  39 + public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
  40 + String expectedSign = buildSign(productKey, deviceName, productSecret);
  41 + return expectedSign.equals(sign);
  42 + }
  43 +
  44 + /**
  45 + * 构建签名内容
  46 + *
  47 + * @param productKey 产品标识
  48 + * @param deviceName 设备名称
  49 + * @return 签名内容
  50 + */
  51 + private static String buildContent(String productKey, String deviceName) {
  52 + return "deviceName" + deviceName + "productKey" + productKey;
  53 + }
  54 +
  55 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java
... ... @@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource;
21 21 import org.eclipse.californium.core.CoapServer;
22 22 import org.eclipse.californium.core.config.CoapConfig;
23 23 import org.eclipse.californium.elements.config.Configuration;
24   -import org.springframework.util.Assert;
  24 +import cn.hutool.core.lang.Assert;
25 25  
26 26 import java.util.concurrent.TimeUnit;
27 27  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/upstream/IotCoapRegisterHandler.java
... ... @@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
33 33 Assert.notNull(request, "请求体不能为空");
34 34 Assert.notBlank(request.getProductKey(), "productKey 不能为空");
35 35 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
36   - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
  36 + Assert.notBlank(request.getSign(), "sign 不能为空");
37 37  
38 38 // 2. 调用动态注册
39 39 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/upstream/IotHttpRegisterHandler.java
... ... @@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
35 35 Assert.notNull(request, "请求参数不能为空");
36 36 Assert.notBlank(request.getProductKey(), "productKey 不能为空");
37 37 Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
38   - Assert.notBlank(request.getProductSecret(), "productSecret 不能为空");
  38 + Assert.notBlank(request.getSign(), "sign 不能为空");
39 39  
40 40 // 2. 调用动态注册
41 41 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
... ... @@ -4,7 +4,6 @@ import jakarta.validation.constraints.Min;
4 4 import jakarta.validation.constraints.NotNull;
5 5 import lombok.Data;
6 6  
7   -// done @AI:validator 参数校验。也看看其他几个配置类有没有类似问题
8 7 /**
9 8 * IoT 网关 MQTT 协议配置
10 9 *
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
... ... @@ -26,7 +26,8 @@ import io.vertx.mqtt.MqttTopicSubscription;
26 26 import io.vertx.mqtt.messages.MqttPublishMessage;
27 27 import lombok.Getter;
28 28 import lombok.extern.slf4j.Slf4j;
29   -import org.springframework.util.Assert;
  29 +import cn.hutool.core.lang.Assert;
  30 +import cn.hutool.core.util.StrUtil;
30 31  
31 32 import java.util.List;
32 33  
... ... @@ -41,6 +42,13 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
41 42 public class IotMqttProtocol implements IotProtocol {
42 43  
43 44 /**
  45 + * 注册连接的 clientId 标识
  46 + *
  47 + * @see #handleEndpoint(MqttEndpoint)
  48 + */
  49 + private static final String AUTH_TYPE_REGISTER = "|authType=register|";
  50 +
  51 + /**
44 52 * 协议配置
45 53 */
46 54 private final ProtocolInstanceProperties properties;
... ... @@ -93,7 +101,7 @@ public class IotMqttProtocol implements IotProtocol {
93 101 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
94 102 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
95 103 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
96   - this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService, deviceApi);
  104 + this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
97 105 this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
98 106  
99 107 // 初始化下行消息订阅者
... ... @@ -112,7 +120,6 @@ public class IotMqttProtocol implements IotProtocol {
112 120 return IotProtocolTypeEnum.MQTT;
113 121 }
114 122  
115   - // done @AI:这个方法的整体注释风格,参考 IotTcpProtocol 的 start 方法。
116 123 @Override
117 124 public void start() {
118 125 if (running) {
... ... @@ -209,13 +216,18 @@ public class IotMqttProtocol implements IotProtocol {
209 216 * @param endpoint MQTT 连接端点
210 217 */
211 218 private void handleEndpoint(MqttEndpoint endpoint) {
  219 + // 1. 如果是注册请求,注册待认证连接;否则走正常认证流程
212 220 String clientId = endpoint.clientIdentifier();
213   -
214   - // 1. 委托 authHandler 处理连接认证
215   - // done @AI:register topic 不需要注册,需要判断下;当前逻辑已支持(设备可在未认证状态发送 register 消息,registerHandler 会处理)
216   - if (!authHandler.handleAuthenticationRequest(endpoint)) {
217   - endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
  221 + if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
  222 + // 情况一:设备注册请求
  223 + registerHandler.handleRegister(endpoint);
218 224 return;
  225 + } else {
  226 + // 情况二:普通认证请求
  227 + if (!authHandler.handleAuthenticationRequest(endpoint)) {
  228 + endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
  229 + return;
  230 + }
219 231 }
220 232  
221 233 // 2.1 设置异常和关闭处理器
... ... @@ -224,9 +236,8 @@ public class IotMqttProtocol implements IotProtocol {
224 236 clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
225 237 endpoint.close();
226 238 });
227   - // done @AI:closeHandler 处理底层连接关闭(网络中断、异常等),disconnectHandler 处理 MQTT DISCONNECT 报文
228   - endpoint.closeHandler(v -> cleanupConnection(endpoint));
229   - endpoint.disconnectHandler(v -> {
  239 + endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等)
  240 + endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文
230 241 log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
231 242 cleanupConnection(endpoint);
232 243 });
... ... @@ -239,7 +250,6 @@ public class IotMqttProtocol implements IotProtocol {
239 250 endpoint.publishReleaseHandler(endpoint::publishComplete);
240 251  
241 252 // 4.1 设置订阅处理器
242   - // done @AI:使用 CollectionUtils.convertList 简化
243 253 endpoint.subscribeHandler(subscribe -> {
244 254 List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName);
245 255 log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
... ... @@ -265,21 +275,16 @@ public class IotMqttProtocol implements IotProtocol {
265 275 private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
266 276 String clientId = endpoint.clientIdentifier();
267 277 try {
268   - // 根据 topic 分发到不同 handler
  278 + // 1. 处理业务消息
269 279 String topic = message.topicName();
270 280 byte[] payload = message.payload().getBytes();
271   - if (registerHandler.isRegisterMessage(topic)) {
272   - registerHandler.handleRegister(endpoint, topic, payload);
273   - } else {
274   - upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
275   - }
  281 + upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
276 282  
277   - // 根据 QoS 级别发送相应的确认消息
  283 + // 2. 根据 QoS 级别发送相应的确认消息
278 284 handleQoSAck(endpoint, message);
279 285 } catch (Exception e) {
280 286 log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
281 287 clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
282   - cleanupConnection(endpoint);
283 288 endpoint.close();
284 289 }
285 290 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
... ... @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
21 21 @RequiredArgsConstructor
22 22 public abstract class IotMqttAbstractHandler {
23 23  
24   - // done @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式
  24 + // TODO @AI:当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式
25 25 /**
26 26 * 默认编解码类型(MQTT 使用 Alink 协议)
27 27 */
... ... @@ -31,7 +31,6 @@ public abstract class IotMqttAbstractHandler {
31 31 protected final IotDeviceMessageService deviceMessageService;
32 32  
33 33 // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic
34   - // done @AI:当前通过 deviceMessageService.encodeDeviceMessage 编码,保持简洁
35 34 /**
36 35 * 发送成功响应到设备
37 36 *
... ... @@ -89,6 +88,6 @@ public abstract class IotMqttAbstractHandler {
89 88 }
90 89 }
91 90  
92   - // done @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse
  91 + // TODO @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse(必须抽取!!!)
93 92  
94 93 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java
... ... @@ -14,7 +14,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
14 14 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
15 15 import io.vertx.mqtt.MqttEndpoint;
16 16 import lombok.extern.slf4j.Slf4j;
17   -import org.springframework.util.Assert;
  17 +import cn.hutool.core.lang.Assert;
18 18  
19 19 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
20 20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -43,13 +43,13 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler {
43 43 this.serverId = serverId;
44 44 }
45 45  
46   - // (暂时不改)TODO @AI:【动态注册】在 clientId 包含 |authType=register 时,进行动态注册设备;校验是 clientId、username、password 三者组合;它是拼接 productSecret 的哈希值;所以 IotDeviceAuthUtils 里面的 buildContent 要改造;
47 46 /**
48 47 * 处理 MQTT 连接(认证)请求
49 48 *
50 49 * @param endpoint MQTT 连接端点
51 50 * @return 认证是否成功
52 51 */
  52 + @SuppressWarnings("DataFlowIssue")
53 53 public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
54 54 String clientId = endpoint.clientIdentifier();
55 55 String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
... ... @@ -59,9 +59,9 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler {
59 59  
60 60 try {
61 61 // 1.1 解析认证参数
62   - Assert.hasText(clientId, "clientId 不能为空");
63   - Assert.hasText(username, "username 不能为空");
64   - Assert.hasText(password, "password 不能为空");
  62 + Assert.notBlank(clientId, "clientId 不能为空");
  63 + Assert.notBlank(username, "username 不能为空");
  64 + Assert.notBlank(password, "password 不能为空");
65 65 // 1.2 构建认证参数
66 66 IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
67 67 .setClientId(clientId)
... ... @@ -102,8 +102,6 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler {
102 102 .setDeviceId(device.getId())
103 103 .setProductKey(device.getProductKey())
104 104 .setDeviceName(device.getDeviceName())
105   - .setClientId(clientId)
106   - .setAuthenticated(true)
107 105 .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
108 106 connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
109 107 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2  
3   -import cn.hutool.core.util.ArrayUtil;
4   -import cn.hutool.core.util.StrUtil;
5   -import cn.iocoder.yudao.framework.common.exception.ServiceException;
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
6 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 6 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
9 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
10   -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
11 9 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
12 10 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
13 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
14 13 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
15 14 import io.vertx.mqtt.MqttEndpoint;
16 15 import lombok.extern.slf4j.Slf4j;
17   -import org.springframework.util.Assert;
18   -
19   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
20   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
21 16  
22 17 /**
23 18 * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
... ... @@ -27,114 +22,62 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
27 22 @Slf4j
28 23 public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
29 24  
30   - // done @AI:IotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来?IotMqttTopicUtils?已使用常量,保持简洁
31   - /**
32   - * register 请求的 topic 后缀
33   - */
34   - public static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
35   -
36 25 private final IotDeviceCommonApi deviceApi;
37 26  
38   - // done @AI:通过 springutil 处理;构造函数注入更清晰,保持原样
39 27 public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
40   - IotDeviceMessageService deviceMessageService,
41   - IotDeviceCommonApi deviceApi) {
  28 + IotDeviceMessageService deviceMessageService) {
42 29 super(connectionManager, deviceMessageService);
43   - this.deviceApi = deviceApi;
  30 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
44 31 }
45 32  
46 33 /**
47   - * 判断是否为注册消息
48   - *
49   - * @param topic 主题
50   - * @return 是否为注册消息
51   - */
52   - // done @AI:是不是搞到 IotMqttTopicUtils 里?当前实现简洁,保持原样
53   - public boolean isRegisterMessage(String topic) {
54   - return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX);
55   - }
56   -
57   - /**
58   - * 处理注册消息
  34 + * 处理注册连接
  35 + * <p>
  36 + * 通过 MQTT 连接的 username 解析设备信息,password 作为签名,直接处理设备注册
59 37 *
60 38 * @param endpoint MQTT 连接端点
61   - * @param topic 主题
62   - * @param payload 消息内容
  39 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
63 40 */
64   - public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) {
  41 + @SuppressWarnings("DataFlowIssue")
  42 + public void handleRegister(MqttEndpoint endpoint) {
65 43 String clientId = endpoint.clientIdentifier();
66   - IotDeviceMessage message = null;
  44 + String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
  45 + String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
  46 + String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
67 47 String productKey = null;
68 48 String deviceName = null;
69   - String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
70 49  
71 50 try {
72   - // 1.1 基础检查
73   - if (ArrayUtil.isEmpty(payload)) {
74   - return;
75   - }
76   - // 1.2 解析主题,获取 productKey 和 deviceName
77   - String[] topicParts = topic.split("/");
78   - Assert.isTrue(topicParts.length >= 4 && !StrUtil.hasBlank(topicParts[2], topicParts[3]),
79   - "topic 格式不正确,无法解析 productKey 和 deviceName");
80   - productKey = topicParts[2];
81   - deviceName = topicParts[3];
  51 + // 1.1 校验参数
  52 + Assert.notBlank(clientId, "clientId 不能为空");
  53 + Assert.notBlank(username, "username 不能为空");
  54 + Assert.notBlank(password, "password 不能为空");
  55 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
  56 + Assert.notNull(deviceInfo, "解析设备信息失败");
  57 + productKey = deviceInfo.getProductKey();
  58 + deviceName = deviceInfo.getDeviceName();
  59 + log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]",
  60 + clientId, productKey, deviceName);
  61 + // 1.2 构建注册参数
  62 + IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
  63 + .setProductKey(productKey)
  64 + .setDeviceName(deviceName)
  65 + .setSign(password);
82 66  
83   - // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
84   - message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
85   - Assert.notNull(message, "消息解码失败");
  67 + // 2. 调用动态注册 API
  68 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  69 + result.checkError();
86 70  
87   - // 3. 处理设备动态注册请求
88   - log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]",
89   - productKey, deviceName, message.getMethod());
90   - processRegisterRequest(message, productKey, deviceName, endpoint);
91   - } catch (ServiceException e) {
92   - log.warn("[handleRegister][业务异常,客户端 ID: {},主题: {},错误: {}]",
93   - clientId, topic, e.getMessage());
94   - String requestId = message != null ? message.getRequestId() : null;
95   - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
96   - } catch (IllegalArgumentException e) {
97   - log.warn("[handleRegister][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
98   - clientId, topic, e.getMessage());
99   - String requestId = message != null ? message.getRequestId() : null;
100   - sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
101   - BAD_REQUEST.getCode(), e.getMessage());
  71 + // 3. 接受连接,并发送成功响应
  72 + endpoint.accept(false);
  73 + sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData());
  74 + log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId);
102 75 } catch (Exception e) {
103   - log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
104   - clientId, topic, e.getMessage(), e);
105   - String requestId = message != null ? message.getRequestId() : null;
106   - sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
107   - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  76 + log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
  77 + // 接受连接,并发送错误响应
  78 + endpoint.accept(false);
  79 + sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage());
108 80 }
109 81 }
110 82  
111   - /**
112   - * 处理设备动态注册请求(一型一密,不需要 deviceSecret)
113   - *
114   - * @param message 消息信息
115   - * @param productKey 产品 Key
116   - * @param deviceName 设备名称
117   - * @param endpoint MQTT 连接端点
118   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
119   - */
120   - @SuppressWarnings("DuplicatedCode")
121   - private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName,
122   - MqttEndpoint endpoint) {
123   - // 1. 解析注册参数
124   - IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
125   - Assert.notNull(params, "注册参数不能为空");
126   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
127   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
128   -
129   - // 2. 调用动态注册 API
130   - CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
131   - result.checkError();
132   -
133   - // 3. 发送成功响应(包含 deviceSecret)
134   - String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
135   - sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData());
136   - log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
137   - params.getDeviceName(), endpoint.clientIdentifier());
138   - }
139   -
140 83 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
... ... @@ -76,6 +76,7 @@ public class IotMqttConnectionManager {
76 76 * @param deviceId 设备 ID
77 77 * @param connectionInfo 连接信息
78 78 */
  79 + // TODO @AI:移除掉 deviceId ???参考别的 tcp 等模块协议
79 80 public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
80 81 // 如果设备已有其他连接,先清理旧连接
81 82 MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
... ... @@ -176,29 +177,16 @@ public class IotMqttConnectionManager {
176 177 * 设备 ID
177 178 */
178 179 private Long deviceId;
179   -
180 180 /**
181 181 * 产品 Key
182 182 */
183 183 private String productKey;
184   -
185 184 /**
186 185 * 设备名称
187 186 */
188 187 private String deviceName;
189 188  
190 189 /**
191   - * 客户端 ID
192   - */
193   - private String clientId;
194   -
195   - // done @AI:保留 authenticated 字段,用于区分已认证连接和待认证连接(如动态注册场景)
196   - /**
197   - * 是否已认证
198   - */
199   - private boolean authenticated;
200   -
201   - /**
202 190 * 连接地址
203 191 */
204 192 private String remoteAddress;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
... ... @@ -21,7 +21,7 @@ import io.vertx.core.net.NetServerOptions;
21 21 import io.vertx.core.net.PemKeyCertOptions;
22 22 import lombok.Getter;
23 23 import lombok.extern.slf4j.Slf4j;
24   -import org.springframework.util.Assert;
  24 +import cn.hutool.core.lang.Assert;
25 25  
26 26 /**
27 27 * IoT TCP 协议实现
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/delimiter/IotTcpDelimiterFrameCodec.java
... ... @@ -8,7 +8,7 @@ import io.vertx.core.Handler;
8 8 import io.vertx.core.buffer.Buffer;
9 9 import io.vertx.core.parsetools.RecordParser;
10 10 import lombok.extern.slf4j.Slf4j;
11   -import org.springframework.util.Assert;
  11 +import cn.hutool.core.lang.Assert;
12 12  
13 13 /**
14 14 * IoT TCP 分隔符帧编解码器
... ... @@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
39 39 private final byte[] delimiterBytes;
40 40  
41 41 public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
42   - Assert.hasText(config.getDelimiter(), "delimiter 不能为空");
  42 + Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
43 43 this.delimiterBytes = parseDelimiter(config.getDelimiter());
44 44 }
45 45  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpFixedLengthFrameCodec.java
... ... @@ -7,7 +7,7 @@ import io.vertx.core.Handler;
7 7 import io.vertx.core.buffer.Buffer;
8 8 import io.vertx.core.parsetools.RecordParser;
9 9 import lombok.extern.slf4j.Slf4j;
10   -import org.springframework.util.Assert;
  10 +import cn.hutool.core.lang.Assert;
11 11  
12 12 /**
13 13 * IoT TCP 定长帧编解码器
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/codec/length/IotTcpLengthFieldFrameCodec.java
... ... @@ -7,7 +7,7 @@ import io.vertx.core.Handler;
7 7 import io.vertx.core.buffer.Buffer;
8 8 import io.vertx.core.parsetools.RecordParser;
9 9 import lombok.extern.slf4j.Slf4j;
10   -import org.springframework.util.Assert;
  10 +import cn.hutool.core.lang.Assert;
11 11  
12 12 import java.util.concurrent.atomic.AtomicReference;
13 13  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/upstream/IotTcpUpstreamHandler.java
... ... @@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer;
24 24 import io.vertx.core.net.NetSocket;
25 25 import io.vertx.core.parsetools.RecordParser;
26 26 import lombok.extern.slf4j.Slf4j;
27   -import org.springframework.util.Assert;
  27 +import cn.hutool.core.lang.Assert;
28 28  
29 29 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
30 30 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
167 167 // 1. 解析认证参数
168 168 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
169 169 Assert.notNull(authParams, "认证参数不能为空");
170   - Assert.hasText(authParams.getUsername(), "username 不能为空");
171   - Assert.hasText(authParams.getPassword(), "password 不能为空");
  170 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  171 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
172 172  
173 173 // 2.1 执行认证
174 174 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
... ... @@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler&lt;NetSocket&gt; {
204 204 // 1. 解析注册参数
205 205 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
206 206 Assert.notNull(params, "注册参数不能为空");
207   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
208   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  207 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  208 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  209 + Assert.notBlank(params.getSign(), "sign 不能为空");
209 210  
210 211 // 2. 调用动态注册
211 212 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
... ... @@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket;
18 18 import io.vertx.core.datagram.DatagramSocketOptions;
19 19 import lombok.Getter;
20 20 import lombok.extern.slf4j.Slf4j;
21   -import org.springframework.util.Assert;
  21 +import cn.hutool.core.lang.Assert;
22 22  
23 23 /**
24 24 * IoT UDP 协议实现
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
... ... @@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer;
27 27 import io.vertx.core.datagram.DatagramPacket;
28 28 import io.vertx.core.datagram.DatagramSocket;
29 29 import lombok.extern.slf4j.Slf4j;
30   -import org.springframework.util.Assert;
  30 +import cn.hutool.core.lang.Assert;
31 31  
32 32 import java.net.InetSocketAddress;
33 33 import java.util.Map;
... ... @@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler {
173 173 // 1. 解析认证参数
174 174 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
175 175 Assert.notNull(authParams, "认证参数不能为空");
176   - Assert.hasText(authParams.getUsername(), "username 不能为空");
177   - Assert.hasText(authParams.getPassword(), "password 不能为空");
  176 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  177 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
178 178  
179 179 // 2.1 执行认证
180 180 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
... ... @@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler {
218 218 // 1. 解析注册参数
219 219 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
220 220 Assert.notNull(params, "注册参数不能为空");
221   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
222   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  221 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  222 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  223 + Assert.notBlank(params.getSign(), "sign 不能为空");
223 224  
224 225 // 2. 调用动态注册
225 226 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java
... ... @@ -20,7 +20,7 @@ import io.vertx.core.http.HttpServerOptions;
20 20 import io.vertx.core.net.PemKeyCertOptions;
21 21 import lombok.Getter;
22 22 import lombok.extern.slf4j.Slf4j;
23   -import org.springframework.util.Assert;
  23 +import cn.hutool.core.lang.Assert;
24 24  
25 25 /**
26 26 * IoT WebSocket 协议实现
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/upstream/IotWebSocketUpstreamHandler.java
... ... @@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa
23 23 import io.vertx.core.Handler;
24 24 import io.vertx.core.http.ServerWebSocket;
25 25 import lombok.extern.slf4j.Slf4j;
26   -import org.springframework.util.Assert;
  26 +import cn.hutool.core.lang.Assert;
27 27  
28 28 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
29 29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
109 109 // 1.2 解码消息
110 110 message = serializer.deserialize(payload);
111 111 Assert.notNull(message, "消息反序列化失败");
112   - Assert.hasText(message.getMethod(), "method 不能为空");
  112 + Assert.notBlank(message.getMethod(), "method 不能为空");
113 113  
114 114 // 2. 根据消息类型路由处理
115 115 if (AUTH_METHOD.equals(message.getMethod())) {
... ... @@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
150 150 // 1. 解析认证参数
151 151 IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
152 152 Assert.notNull(authParams, "认证参数不能为空");
153   - Assert.hasText(authParams.getUsername(), "username 不能为空");
154   - Assert.hasText(authParams.getPassword(), "password 不能为空");
  153 + Assert.notBlank(authParams.getUsername(), "username 不能为空");
  154 + Assert.notBlank(authParams.getPassword(), "password 不能为空");
155 155  
156 156 // 2.1 执行认证
157 157 CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
... ... @@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler&lt;ServerWebSocket&gt; {
187 187 // 1. 解析注册参数
188 188 IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
189 189 Assert.notNull(params, "注册参数不能为空");
190   - Assert.hasText(params.getProductKey(), "productKey 不能为空");
191   - Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
  190 + Assert.notBlank(params.getProductKey(), "productKey 不能为空");
  191 + Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
  192 + Assert.notBlank(params.getSign(), "sign 不能为空");
192 193  
193 194 // 2. 调用动态注册
194 195 CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotDirectDeviceCoapProtocolIntegrationTest.java
... ... @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  12 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
12 13 import lombok.extern.slf4j.Slf4j;
13 14 import org.eclipse.californium.core.CoapClient;
14 15 import org.eclipse.californium.core.CoapResponse;
... ... @@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
203 204 // 1.1 构建请求
204 205 String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
205 206 // 1.2 构建请求参数
206   - IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
207   - reqDTO.setProductKey(PRODUCT_KEY);
208   - reqDTO.setDeviceName("test-" + System.currentTimeMillis());
209   - reqDTO.setProductSecret("test-product-secret");
  207 + String deviceName = "test-" + System.currentTimeMillis();
  208 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  209 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
  210 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
  211 + .setProductKey(PRODUCT_KEY)
  212 + .setDeviceName(deviceName)
  213 + .setSign(sign);
210 214 String payload = JsonUtils.toJsonString(reqDTO);
211 215 // 1.3 输出请求
212 216 log.info("[testDeviceRegister][请求 URI: {}]", uri);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDirectDeviceHttpProtocolIntegrationTest.java
... ... @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
13 14 import lombok.extern.slf4j.Slf4j;
14 15 import org.junit.jupiter.api.Disabled;
15 16 import org.junit.jupiter.api.Test;
... ... @@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
158 159 // 1.1 构建请求
159 160 String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
160 161 // 1.2 构建请求参数
  162 + String deviceName = "test-" + System.currentTimeMillis();
  163 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  164 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
161 165 IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
162 166 .setProductKey(PRODUCT_KEY)
163   - .setDeviceName("test-" + System.currentTimeMillis())
164   - .setProductSecret("test-product-secret");
  167 + .setDeviceName(deviceName)
  168 + .setSign(sign);
165 169 String payload = JsonUtils.toJsonString(reqDTO);
166 170 // 1.3 输出请求
167 171 log.info("[testDeviceRegister][请求 URL: {}]", url);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
... ... @@ -4,10 +4,10 @@ import cn.hutool.core.map.MapUtil;
4 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 7 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 8 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 9 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  10 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
11 11 import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
12 12 import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
13 13 import io.netty.handler.codec.mqtt.MqttQoS;
... ... @@ -173,36 +173,51 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
173 173 /**
174 174 * 直连设备动态注册测试(一型一密)
175 175 * <p>
176   - * 使用产品密钥(productSecret)验证身份,成功后返回设备密钥(deviceSecret)
  176 + * 认证方式:
  177 + * - clientId: 任意值 + "|authType=register|" 后缀
  178 + * - username: {deviceName}&{productKey}(与普通认证相同)
  179 + * - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256)
177 180 * <p>
178   - * 注意:此接口不需要认证
  181 + * 成功后返回设备密钥(deviceSecret),可用于后续一机一密认证
179 182 */
180 183 @Test
181 184 public void testDeviceRegister() throws Exception {
182   - // 1. 连接并认证(使用已有设备连接)
183   - MqttClient client = connectAndAuth();
184   - log.info("[testDeviceRegister][连接认证成功]");
  185 + // 1.1 构建注册参数
  186 + String deviceName = "test-mqtt-" + System.currentTimeMillis();
  187 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  188 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
  189 + // 1.2 构建 MQTT 连接参数(clientId 需要添加 |authType=register| 后缀)
  190 + String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|";
  191 + String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName);
  192 + log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]",
  193 + clientId, username, sign);
  194 + // 1.3 创建客户端并连接(连接时服务端自动处理注册)
  195 + MqttClientOptions options = new MqttClientOptions()
  196 + .setClientId(clientId)
  197 + .setUsername(username)
  198 + .setPassword(sign)
  199 + .setCleanSession(true)
  200 + .setKeepAliveInterval(60);
  201 + MqttClient client = MqttClient.create(vertx, options);
185 202  
186 203 try {
187   - // 2.1 构建注册消息
188   - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
189   - .setProductKey(PRODUCT_KEY)
190   - .setDeviceName("test-mqtt-" + System.currentTimeMillis())
191   - .setProductSecret("test-product-secret");
192   - IotDeviceMessage request = IotDeviceMessage.requestOf(
193   - IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(),
194   - registerReqDTO);
195   -
196   - // 2.2 订阅 _reply 主题
197   - String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply",
198   - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
199   - subscribe(client, replyTopic);
  204 + // 2. 设置消息处理器,接收注册响应
  205 + CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
  206 + client.publishHandler(message -> {
  207 + log.info("[testDeviceRegister][收到响应: topic={}, payload={}]",
  208 + message.topicName(), message.payload().toString());
  209 + IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
  210 + responseFuture.complete(response);
  211 + });
  212 +
  213 + // 3. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
  214 + client.connect(SERVER_PORT, SERVER_HOST)
  215 + .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  216 + log.info("[testDeviceRegister][连接成功,等待注册响应...]");
200 217  
201   - // 3. 发布消息并等待响应
202   - String topic = String.format("/sys/%s/%s/thing/auth/register",
203   - registerReqDTO.getProductKey(), registerReqDTO.getDeviceName());
204   - IotDeviceMessage response = publishAndWaitReply(client, topic, request);
205   - log.info("[testDeviceRegister][响应消息: {}]", response);
  218 + // 4. 等待注册响应
  219 + IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
  220 + log.info("[testDeviceRegister][注册响应: {}]", response);
206 221 log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
207 222 } finally {
208 223 disconnect(client);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotDirectDeviceTcpProtocolIntegrationTest.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
12 13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
13 14 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
... ... @@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
146 147 @Test
147 148 public void testDeviceRegister() throws Exception {
148 149 // 1. 构建注册消息
  150 + String deviceName = "test-tcp-" + System.currentTimeMillis();
  151 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  152 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
149 153 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
150 154 .setProductKey(PRODUCT_KEY)
151   - .setDeviceName("test-tcp-" + System.currentTimeMillis())
152   - .setProductSecret("test-product-secret");
  155 + .setDeviceName(deviceName)
  156 + .setSign(sign);
153 157 IotDeviceMessage request = IotDeviceMessage.requestOf(
154 158 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
155 159  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotDirectDeviceUdpProtocolIntegrationTest.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
9 9 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
11 12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
12 13 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
13 14 import lombok.extern.slf4j.Slf4j;
... ... @@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
100 101 @Test
101 102 public void testDeviceRegister() throws Exception {
102 103 // 1. 构建注册消息
  104 + String deviceName = "test-udp-" + System.currentTimeMillis();
  105 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  106 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
103 107 IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
104 108 .setProductKey(PRODUCT_KEY)
105   - .setDeviceName("test-udp-" + System.currentTimeMillis())
106   - .setProductSecret("test-product-secret");
  109 + .setDeviceName(deviceName)
  110 + .setSign(sign);
107 111 IotDeviceMessage request = IotDeviceMessage.requestOf(
108 112 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);
109 113  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotDirectDeviceWebSocketProtocolIntegrationTest.java
... ... @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
10 10 import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
11 11 import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
12 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
13 14 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
14 15 import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
15 16 import io.vertx.core.Vertx;
... ... @@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
131 132 @Test
132 133 public void testDeviceRegister() throws Exception {
133 134 // 1.1 构建注册消息
134   - IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO();
135   - registerReqDTO.setProductKey(PRODUCT_KEY);
136   - registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis());
137   - registerReqDTO.setProductSecret("test-product-secret");
  135 + String deviceName = "test-ws-" + System.currentTimeMillis();
  136 + String productSecret = "test-product-secret"; // 替换为实际的 productSecret
  137 + String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
  138 + IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
  139 + .setProductKey(PRODUCT_KEY)
  140 + .setDeviceName(deviceName)
  141 + .setSign(sign);
138 142 IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
139 143 IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
140 144 // 1.2 序列化
... ...