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

This commit is contained in:
YunaiV
2026-02-02 08:34:52 +08:00
parent 9156aef4e3
commit 83990086fa
27 changed files with 239 additions and 210 deletions

View File

@@ -29,6 +29,7 @@ import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
@@ -819,8 +820,9 @@ public class IotDeviceServiceImpl implements IotDeviceService {
if (BooleanUtil.isFalse(product.getRegisterEnabled())) { if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
throw exception(DEVICE_REGISTER_DISABLED); throw exception(DEVICE_REGISTER_DISABLED);
} }
// 1.3 验证 productSecret // 1.3 【重要!!!】验证签名
if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) { if (!IotProductAuthUtils.verifySign(reqDTO.getProductKey(), reqDTO.getDeviceName(),
product.getProductSecret(), reqDTO.getSign())) {
throw exception(DEVICE_REGISTER_SECRET_INVALID); throw exception(DEVICE_REGISTER_SECRET_INVALID);
} }
return TenantUtils.execute(product.getTenantId(), () -> { return TenantUtils.execute(product.getTenantId(), () -> {

View File

@@ -27,9 +27,11 @@ public class IotDeviceRegisterReqDTO {
private String deviceName; private String deviceName;
/** /**
* 产品密钥 * 注册签名
*
* @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
*/ */
@NotEmpty(message = "产品密钥不能为空") @NotEmpty(message = "签名不能为空")
private String productSecret; private String sign;
} }

View File

@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
/**
* IoT 产品【动态注册】认证工具类
* <p>
* 用于一型一密场景,使用 productSecret 生成签名
*
* @author 芋道源码
*/
public class IotProductAuthUtils {
/**
* 生成设备动态注册签名
*
* @param productKey 产品标识
* @param deviceName 设备名称
* @param productSecret 产品密钥
* @return 签名
*/
public static String buildSign(String productKey, String deviceName, String productSecret) {
String content = buildContent(productKey, deviceName);
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
.digestHex(content);
}
/**
* 验证设备动态注册签名
*
* @param productKey 产品标识
* @param deviceName 设备名称
* @param productSecret 产品密钥
* @param sign 待验证的签名
* @return 是否验证通过
*/
public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
String expectedSign = buildSign(productKey, deviceName, productSecret);
return expectedSign.equals(sign);
}
/**
* 构建签名内容
*
* @param productKey 产品标识
* @param deviceName 设备名称
* @return 签名内容
*/
private static String buildContent(String productKey, String deviceName) {
return "deviceName" + deviceName + "productKey" + productKey;
}
}

View File

@@ -21,7 +21,7 @@ import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig; import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.elements.config.Configuration;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View File

@@ -33,7 +33,7 @@ public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
Assert.notNull(request, "请求体不能为空"); Assert.notNull(request, "请求体不能为空");
Assert.notBlank(request.getProductKey(), "productKey 不能为空"); Assert.notBlank(request.getProductKey(), "productKey 不能为空");
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); Assert.notBlank(request.getSign(), "sign 不能为空");
// 2. 调用动态注册 // 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);

View File

@@ -35,7 +35,7 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
Assert.notNull(request, "请求参数不能为空"); Assert.notNull(request, "请求参数不能为空");
Assert.notBlank(request.getProductKey(), "productKey 不能为空"); Assert.notBlank(request.getProductKey(), "productKey 不能为空");
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空"); Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(request.getProductSecret(), "productSecret 不能为空"); Assert.notBlank(request.getSign(), "sign 不能为空");
// 2. 调用动态注册 // 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request); CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);

View File

@@ -4,7 +4,6 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
// done @AIvalidator 参数校验。也看看其他几个配置类有没有类似问题
/** /**
* IoT 网关 MQTT 协议配置 * IoT 网关 MQTT 协议配置
* *

View File

@@ -26,7 +26,8 @@ import io.vertx.mqtt.MqttTopicSubscription;
import io.vertx.mqtt.messages.MqttPublishMessage; import io.vertx.mqtt.messages.MqttPublishMessage;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import java.util.List; import java.util.List;
@@ -40,6 +41,13 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
@Slf4j @Slf4j
public class IotMqttProtocol implements IotProtocol { public class IotMqttProtocol implements IotProtocol {
/**
* 注册连接的 clientId 标识
*
* @see #handleEndpoint(MqttEndpoint)
*/
private static final String AUTH_TYPE_REGISTER = "|authType=register|";
/** /**
* 协议配置 * 协议配置
*/ */
@@ -93,7 +101,7 @@ public class IotMqttProtocol implements IotProtocol {
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService, deviceApi); this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
// 初始化下行消息订阅者 // 初始化下行消息订阅者
@@ -112,7 +120,6 @@ public class IotMqttProtocol implements IotProtocol {
return IotProtocolTypeEnum.MQTT; return IotProtocolTypeEnum.MQTT;
} }
// done @AI这个方法的整体注释风格参考 IotTcpProtocol 的 start 方法。
@Override @Override
public void start() { public void start() {
if (running) { if (running) {
@@ -209,13 +216,18 @@ public class IotMqttProtocol implements IotProtocol {
* @param endpoint MQTT 连接端点 * @param endpoint MQTT 连接端点
*/ */
private void handleEndpoint(MqttEndpoint endpoint) { private void handleEndpoint(MqttEndpoint endpoint) {
// 1. 如果是注册请求,注册待认证连接;否则走正常认证流程
String clientId = endpoint.clientIdentifier(); String clientId = endpoint.clientIdentifier();
if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
// 1. 委托 authHandler 处理连接认证 // 情况一:设备注册请求
// done @AIregister topic 不需要注册,需要判断下;当前逻辑已支持(设备可在未认证状态发送 register 消息registerHandler 会处理) registerHandler.handleRegister(endpoint);
if (!authHandler.handleAuthenticationRequest(endpoint)) {
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
return; return;
} else {
// 情况二:普通认证请求
if (!authHandler.handleAuthenticationRequest(endpoint)) {
endpoint.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD);
return;
}
} }
// 2.1 设置异常和关闭处理器 // 2.1 设置异常和关闭处理器
@@ -224,9 +236,8 @@ public class IotMqttProtocol implements IotProtocol {
clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage()); clientId, connectionManager.getEndpointAddress(endpoint), ex.getMessage());
endpoint.close(); endpoint.close();
}); });
// done @AIcloseHandler 处理底层连接关闭网络中断、异常等disconnectHandler 处理 MQTT DISCONNECT 报文 endpoint.closeHandler(v -> cleanupConnection(endpoint)); // 处理底层连接关闭(网络中断、异常等)
endpoint.closeHandler(v -> cleanupConnection(endpoint)); endpoint.disconnectHandler(v -> { // 处理 MQTT DISCONNECT 报文
endpoint.disconnectHandler(v -> {
log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId); log.debug("[handleEndpoint][设备断开连接,客户端 ID: {}]", clientId);
cleanupConnection(endpoint); cleanupConnection(endpoint);
}); });
@@ -239,7 +250,6 @@ public class IotMqttProtocol implements IotProtocol {
endpoint.publishReleaseHandler(endpoint::publishComplete); endpoint.publishReleaseHandler(endpoint::publishComplete);
// 4.1 设置订阅处理器 // 4.1 设置订阅处理器
// done @AI使用 CollectionUtils.convertList 简化
endpoint.subscribeHandler(subscribe -> { endpoint.subscribeHandler(subscribe -> {
List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName);
log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames);
@@ -265,21 +275,16 @@ public class IotMqttProtocol implements IotProtocol {
private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) { private void processMessage(MqttEndpoint endpoint, MqttPublishMessage message) {
String clientId = endpoint.clientIdentifier(); String clientId = endpoint.clientIdentifier();
try { try {
// 根据 topic 分发到不同 handler // 1. 处理业务消息
String topic = message.topicName(); String topic = message.topicName();
byte[] payload = message.payload().getBytes(); byte[] payload = message.payload().getBytes();
if (registerHandler.isRegisterMessage(topic)) { upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
registerHandler.handleRegister(endpoint, topic, payload);
} else {
upstreamHandler.handleBusinessRequest(endpoint, topic, payload);
}
// 根据 QoS 级别发送相应的确认消息 // 2. 根据 QoS 级别发送相应的确认消息
handleQoSAck(endpoint, message); handleQoSAck(endpoint, message);
} catch (Exception e) { } catch (Exception e) {
log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]", log.error("[processMessage][消息处理失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
cleanupConnection(endpoint);
endpoint.close(); endpoint.close();
} }
} }

View File

@@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor @RequiredArgsConstructor
public abstract class IotMqttAbstractHandler { public abstract class IotMqttAbstractHandler {
// done @AI当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式 // TODO @AI当前使用 Alink 序列化类型,后续可考虑支持更多序列化方式
/** /**
* 默认编解码类型MQTT 使用 Alink 协议) * 默认编解码类型MQTT 使用 Alink 协议)
*/ */
@@ -31,7 +31,6 @@ public abstract class IotMqttAbstractHandler {
protected final IotDeviceMessageService deviceMessageService; protected final IotDeviceMessageService deviceMessageService;
// done @AI基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic // done @AI基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic
// done @AI当前通过 deviceMessageService.encodeDeviceMessage 编码,保持简洁
/** /**
* 发送成功响应到设备 * 发送成功响应到设备
* *
@@ -89,6 +88,6 @@ public abstract class IotMqttAbstractHandler {
} }
} }
// done @AI当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse // TODO @AI当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse(必须抽取!!!)
} }

View File

@@ -14,7 +14,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.mqtt.MqttEndpoint; import io.vertx.mqtt.MqttEndpoint;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -43,13 +43,13 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler {
this.serverId = serverId; this.serverId = serverId;
} }
// 暂时不改TODO @AI【动态注册】在 clientId 包含 |authType=register 时,进行动态注册设备;校验是 clientId、username、password 三者组合;它是拼接 productSecret 的哈希值;所以 IotDeviceAuthUtils 里面的 buildContent 要改造;
/** /**
* 处理 MQTT 连接(认证)请求 * 处理 MQTT 连接(认证)请求
* *
* @param endpoint MQTT 连接端点 * @param endpoint MQTT 连接端点
* @return 认证是否成功 * @return 认证是否成功
*/ */
@SuppressWarnings("DataFlowIssue")
public boolean handleAuthenticationRequest(MqttEndpoint endpoint) { public boolean handleAuthenticationRequest(MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier(); String clientId = endpoint.clientIdentifier();
String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null; String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
@@ -59,9 +59,9 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler {
try { try {
// 1.1 解析认证参数 // 1.1 解析认证参数
Assert.hasText(clientId, "clientId 不能为空"); Assert.notBlank(clientId, "clientId 不能为空");
Assert.hasText(username, "username 不能为空"); Assert.notBlank(username, "username 不能为空");
Assert.hasText(password, "password 不能为空"); Assert.notBlank(password, "password 不能为空");
// 1.2 构建认证参数 // 1.2 构建认证参数
IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO() IotDeviceAuthReqDTO authParams = new IotDeviceAuthReqDTO()
.setClientId(clientId) .setClientId(clientId)
@@ -102,8 +102,6 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler {
.setDeviceId(device.getId()) .setDeviceId(device.getId())
.setProductKey(device.getProductKey()) .setProductKey(device.getProductKey())
.setDeviceName(device.getDeviceName()) .setDeviceName(device.getDeviceName())
.setClientId(clientId)
.setAuthenticated(true)
.setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
} }

View File

@@ -1,23 +1,18 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.mqtt.MqttEndpoint; import io.vertx.mqtt.MqttEndpoint;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
/** /**
* IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密) * IoT 网关 MQTT 设备注册处理器:处理设备动态注册消息(一型一密)
@@ -27,114 +22,62 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
@Slf4j @Slf4j
public class IotMqttRegisterHandler extends IotMqttAbstractHandler { public class IotMqttRegisterHandler extends IotMqttAbstractHandler {
// done @AIIotDeviceMessageMethodEnum.DEVICE_REGISTER 计算出来IotMqttTopicUtils已使用常量保持简洁
/**
* register 请求的 topic 后缀
*/
public static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
private final IotDeviceCommonApi deviceApi; private final IotDeviceCommonApi deviceApi;
// done @AI通过 springutil 处理;构造函数注入更清晰,保持原样
public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager, public IotMqttRegisterHandler(IotMqttConnectionManager connectionManager,
IotDeviceMessageService deviceMessageService, IotDeviceMessageService deviceMessageService) {
IotDeviceCommonApi deviceApi) {
super(connectionManager, deviceMessageService); super(connectionManager, deviceMessageService);
this.deviceApi = deviceApi; this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
} }
/** /**
* 判断是否为注册消息 * 处理注册连接
* * <p>
* @param topic 主题 * 通过 MQTT 连接的 username 解析设备信息password 作为签名,直接处理设备注册
* @return 是否为注册消息
*/
// done @AI是不是搞到 IotMqttTopicUtils 里?当前实现简洁,保持原样
public boolean isRegisterMessage(String topic) {
return topic != null && topic.endsWith(REGISTER_TOPIC_SUFFIX);
}
/**
* 处理注册消息
* *
* @param endpoint MQTT 连接端点 * @param endpoint MQTT 连接端点
* @param topic 主题 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
* @param payload 消息内容
*/ */
public void handleRegister(MqttEndpoint endpoint, String topic, byte[] payload) { @SuppressWarnings("DataFlowIssue")
public void handleRegister(MqttEndpoint endpoint) {
String clientId = endpoint.clientIdentifier(); String clientId = endpoint.clientIdentifier();
IotDeviceMessage message = null; String username = endpoint.auth() != null ? endpoint.auth().getUsername() : null;
String password = endpoint.auth() != null ? endpoint.auth().getPassword() : null;
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
String productKey = null; String productKey = null;
String deviceName = null; String deviceName = null;
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
try { try {
// 1.1 基础检查 // 1.1 校验参数
if (ArrayUtil.isEmpty(payload)) { Assert.notBlank(clientId, "clientId 不能为空");
return; Assert.notBlank(username, "username 不能为空");
} Assert.notBlank(password, "password 不能为空");
// 1.2 解析主题,获取 productKey 和 deviceName IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
String[] topicParts = topic.split("/"); Assert.notNull(deviceInfo, "解析设备信息失败");
Assert.isTrue(topicParts.length >= 4 && !StrUtil.hasBlank(topicParts[2], topicParts[3]), productKey = deviceInfo.getProductKey();
"topic 格式不正确,无法解析 productKey 和 deviceName"); deviceName = deviceInfo.getDeviceName();
productKey = topicParts[2]; log.info("[handleRegister][设备注册连接,客户端 ID: {},设备: {}.{}]",
deviceName = topicParts[3]; clientId, productKey, deviceName);
// 1.2 构建注册参数
IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
.setProductKey(productKey)
.setDeviceName(deviceName)
.setSign(password);
// 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType // 2. 调用动态注册 API
message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE); CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
Assert.notNull(message, "消息解码失败"); result.checkError();
// 3. 处理设备动态注册请求 // 3. 接受连接,并发送成功响应
log.info("[handleRegister][收到设备注册消息,设备: {}.{}, 方法: {}]", endpoint.accept(false);
productKey, deviceName, message.getMethod()); sendSuccessResponse(endpoint, productKey, deviceName, null, method, result.getData());
processRegisterRequest(message, productKey, deviceName, endpoint); log.info("[handleRegister][注册成功,设备: {}.{},客户端 ID: {}]", productKey, deviceName, clientId);
} catch (ServiceException e) {
log.warn("[handleRegister][业务异常,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage());
String requestId = message != null ? message.getRequestId() : null;
sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("[handleRegister][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
clientId, topic, e.getMessage());
String requestId = message != null ? message.getRequestId() : null;
sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
BAD_REQUEST.getCode(), e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("[handleRegister][消息处理异常,客户端 ID: {},主题: {},错误: {}]", log.warn("[handleRegister][注册失败,客户端 ID: {},错误: {}]", clientId, e.getMessage());
clientId, topic, e.getMessage(), e); // 接受连接,并发送错误响应
String requestId = message != null ? message.getRequestId() : null; endpoint.accept(false);
sendErrorResponse(endpoint, productKey, deviceName, requestId, method, sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage());
INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
} }
} }
/**
* 处理设备动态注册请求(一型一密,不需要 deviceSecret
*
* @param message 消息信息
* @param productKey 产品 Key
* @param deviceName 设备名称
* @param endpoint MQTT 连接端点
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@SuppressWarnings("DuplicatedCode")
private void processRegisterRequest(IotDeviceMessage message, String productKey, String deviceName,
MqttEndpoint endpoint) {
// 1. 解析注册参数
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
Assert.notNull(params, "注册参数不能为空");
Assert.hasText(params.getProductKey(), "productKey 不能为空");
Assert.hasText(params.getDeviceName(), "deviceName 不能为空");
// 2. 调用动态注册 API
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
result.checkError();
// 3. 发送成功响应(包含 deviceSecret
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
sendSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), method, result.getData());
log.info("[processRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
params.getDeviceName(), endpoint.clientIdentifier());
}
} }

View File

@@ -76,6 +76,7 @@ public class IotMqttConnectionManager {
* @param deviceId 设备 ID * @param deviceId 设备 ID
* @param connectionInfo 连接信息 * @param connectionInfo 连接信息
*/ */
// TODO @AI移除掉 deviceId ???参考别的 tcp 等模块协议
public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) {
// 如果设备已有其他连接,先清理旧连接 // 如果设备已有其他连接,先清理旧连接
MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
@@ -176,28 +177,15 @@ public class IotMqttConnectionManager {
* 设备 ID * 设备 ID
*/ */
private Long deviceId; private Long deviceId;
/** /**
* 产品 Key * 产品 Key
*/ */
private String productKey; private String productKey;
/** /**
* 设备名称 * 设备名称
*/ */
private String deviceName; private String deviceName;
/**
* 客户端 ID
*/
private String clientId;
// done @AI保留 authenticated 字段,用于区分已认证连接和待认证连接(如动态注册场景)
/**
* 是否已认证
*/
private boolean authenticated;
/** /**
* 连接地址 * 连接地址
*/ */

View File

@@ -21,7 +21,7 @@ import io.vertx.core.net.NetServerOptions;
import io.vertx.core.net.PemKeyCertOptions; import io.vertx.core.net.PemKeyCertOptions;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
/** /**
* IoT TCP 协议实现 * IoT TCP 协议实现

View File

@@ -8,7 +8,7 @@ import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser; import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
/** /**
* IoT TCP 分隔符帧编解码器 * IoT TCP 分隔符帧编解码器
@@ -39,7 +39,7 @@ public class IotTcpDelimiterFrameCodec implements IotTcpFrameCodec {
private final byte[] delimiterBytes; private final byte[] delimiterBytes;
public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) { public IotTcpDelimiterFrameCodec(IotTcpConfig.CodecConfig config) {
Assert.hasText(config.getDelimiter(), "delimiter 不能为空"); Assert.notBlank(config.getDelimiter(), "delimiter 不能为空");
this.delimiterBytes = parseDelimiter(config.getDelimiter()); this.delimiterBytes = parseDelimiter(config.getDelimiter());
} }

View File

@@ -7,7 +7,7 @@ import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser; import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
/** /**
* IoT TCP 定长帧编解码器 * IoT TCP 定长帧编解码器

View File

@@ -7,7 +7,7 @@ import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser; import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;

View File

@@ -24,7 +24,7 @@ import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket; import io.vertx.core.net.NetSocket;
import io.vertx.core.parsetools.RecordParser; import io.vertx.core.parsetools.RecordParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -167,8 +167,8 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
// 1. 解析认证参数 // 1. 解析认证参数
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
Assert.notNull(authParams, "认证参数不能为空"); Assert.notNull(authParams, "认证参数不能为空");
Assert.hasText(authParams.getUsername(), "username 不能为空"); Assert.notBlank(authParams.getUsername(), "username 不能为空");
Assert.hasText(authParams.getPassword(), "password 不能为空"); Assert.notBlank(authParams.getPassword(), "password 不能为空");
// 2.1 执行认证 // 2.1 执行认证
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
@@ -204,8 +204,9 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
// 1. 解析注册参数 // 1. 解析注册参数
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
Assert.notNull(params, "注册参数不能为空"); Assert.notNull(params, "注册参数不能为空");
Assert.hasText(params.getProductKey(), "productKey 不能为空"); Assert.notBlank(params.getProductKey(), "productKey 不能为空");
Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(params.getSign(), "sign 不能为空");
// 2. 调用动态注册 // 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);

View File

@@ -18,7 +18,7 @@ import io.vertx.core.datagram.DatagramSocket;
import io.vertx.core.datagram.DatagramSocketOptions; import io.vertx.core.datagram.DatagramSocketOptions;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
/** /**
* IoT UDP 协议实现 * IoT UDP 协议实现

View File

@@ -27,7 +27,7 @@ import io.vertx.core.buffer.Buffer;
import io.vertx.core.datagram.DatagramPacket; import io.vertx.core.datagram.DatagramPacket;
import io.vertx.core.datagram.DatagramSocket; import io.vertx.core.datagram.DatagramSocket;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Map; import java.util.Map;
@@ -173,8 +173,8 @@ public class IotUdpUpstreamHandler {
// 1. 解析认证参数 // 1. 解析认证参数
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
Assert.notNull(authParams, "认证参数不能为空"); Assert.notNull(authParams, "认证参数不能为空");
Assert.hasText(authParams.getUsername(), "username 不能为空"); Assert.notBlank(authParams.getUsername(), "username 不能为空");
Assert.hasText(authParams.getPassword(), "password 不能为空"); Assert.notBlank(authParams.getPassword(), "password 不能为空");
// 2.1 执行认证 // 2.1 执行认证
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
@@ -218,8 +218,9 @@ public class IotUdpUpstreamHandler {
// 1. 解析注册参数 // 1. 解析注册参数
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
Assert.notNull(params, "注册参数不能为空"); Assert.notNull(params, "注册参数不能为空");
Assert.hasText(params.getProductKey(), "productKey 不能为空"); Assert.notBlank(params.getProductKey(), "productKey 不能为空");
Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(params.getSign(), "sign 不能为空");
// 2. 调用动态注册 // 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);

View File

@@ -20,7 +20,7 @@ import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PemKeyCertOptions; import io.vertx.core.net.PemKeyCertOptions;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
/** /**
* IoT WebSocket 协议实现 * IoT WebSocket 协议实现

View File

@@ -23,7 +23,7 @@ import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessa
import io.vertx.core.Handler; import io.vertx.core.Handler;
import io.vertx.core.http.ServerWebSocket; import io.vertx.core.http.ServerWebSocket;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert; import cn.hutool.core.lang.Assert;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -109,7 +109,7 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
// 1.2 解码消息 // 1.2 解码消息
message = serializer.deserialize(payload); message = serializer.deserialize(payload);
Assert.notNull(message, "消息反序列化失败"); Assert.notNull(message, "消息反序列化失败");
Assert.hasText(message.getMethod(), "method 不能为空"); Assert.notBlank(message.getMethod(), "method 不能为空");
// 2. 根据消息类型路由处理 // 2. 根据消息类型路由处理
if (AUTH_METHOD.equals(message.getMethod())) { if (AUTH_METHOD.equals(message.getMethod())) {
@@ -150,8 +150,8 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
// 1. 解析认证参数 // 1. 解析认证参数
IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class); IotDeviceAuthReqDTO authParams = JsonUtils.convertObject(message.getParams(), IotDeviceAuthReqDTO.class);
Assert.notNull(authParams, "认证参数不能为空"); Assert.notNull(authParams, "认证参数不能为空");
Assert.hasText(authParams.getUsername(), "username 不能为空"); Assert.notBlank(authParams.getUsername(), "username 不能为空");
Assert.hasText(authParams.getPassword(), "password 不能为空"); Assert.notBlank(authParams.getPassword(), "password 不能为空");
// 2.1 执行认证 // 2.1 执行认证
CommonResult<Boolean> authResult = deviceApi.authDevice(authParams); CommonResult<Boolean> authResult = deviceApi.authDevice(authParams);
@@ -187,8 +187,9 @@ public class IotWebSocketUpstreamHandler implements Handler<ServerWebSocket> {
// 1. 解析注册参数 // 1. 解析注册参数
IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class); IotDeviceRegisterReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceRegisterReqDTO.class);
Assert.notNull(params, "注册参数不能为空"); Assert.notNull(params, "注册参数不能为空");
Assert.hasText(params.getProductKey(), "productKey 不能为空"); Assert.notBlank(params.getProductKey(), "productKey 不能为空");
Assert.hasText(params.getDeviceName(), "deviceName 不能为空"); Assert.notBlank(params.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(params.getSign(), "sign 不能为空");
// 2. 调用动态注册 // 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params); CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);

View File

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.CoapResponse;
@@ -203,10 +204,13 @@ public class IotDirectDeviceCoapProtocolIntegrationTest {
// 1.1 构建请求 // 1.1 构建请求
String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); String uri = String.format("coap://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
// 1.2 构建请求参数 // 1.2 构建请求参数
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); String deviceName = "test-" + System.currentTimeMillis();
reqDTO.setProductKey(PRODUCT_KEY); String productSecret = "test-product-secret"; // 替换为实际的 productSecret
reqDTO.setDeviceName("test-" + System.currentTimeMillis()); String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
reqDTO.setProductSecret("test-product-secret"); IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(PRODUCT_KEY)
.setDeviceName(deviceName)
.setSign(sign);
String payload = JsonUtils.toJsonString(reqDTO); String payload = JsonUtils.toJsonString(reqDTO);
// 1.3 输出请求 // 1.3 输出请求
log.info("[testDeviceRegister][请求 URI: {}]", uri); log.info("[testDeviceRegister][请求 URI: {}]", uri);

View File

@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -158,10 +159,13 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
// 1.1 构建请求 // 1.1 构建请求
String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
// 1.2 构建请求参数 // 1.2 构建请求参数
String deviceName = "test-" + System.currentTimeMillis();
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(PRODUCT_KEY) .setProductKey(PRODUCT_KEY)
.setDeviceName("test-" + System.currentTimeMillis()) .setDeviceName(deviceName)
.setProductSecret("test-product-secret"); .setSign(sign);
String payload = JsonUtils.toJsonString(reqDTO); String payload = JsonUtils.toJsonString(reqDTO);
// 1.3 输出请求 // 1.3 输出请求
log.info("[testDeviceRegister][请求 URL: {}]", url); log.info("[testDeviceRegister][请求 URL: {}]", url);

View File

@@ -4,10 +4,10 @@ import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec;
import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttQoS;
@@ -173,36 +173,51 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
/** /**
* 直连设备动态注册测试(一型一密) * 直连设备动态注册测试(一型一密)
* <p> * <p>
* 使用产品密钥productSecret验证身份成功后返回设备密钥deviceSecret * 认证方式:
* - clientId: 任意值 + "|authType=register|" 后缀
* - username: {deviceName}&{productKey}(与普通认证相同)
* - password: 签名(使用 productSecret 对 "deviceName" + deviceName + "productKey" + productKey 进行 HMAC-SHA256
* <p> * <p>
* 注意:此接口不需要认证 * 成功后返回设备密钥deviceSecret可用于后续一机一密认证
*/ */
@Test @Test
public void testDeviceRegister() throws Exception { public void testDeviceRegister() throws Exception {
// 1. 连接并认证(使用已有设备连接) // 1.1 构建注册参数
MqttClient client = connectAndAuth(); String deviceName = "test-mqtt-" + System.currentTimeMillis();
log.info("[testDeviceRegister][连接认证成功]"); String productSecret = "test-product-secret"; // 替换为实际的 productSecret
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
// 1.2 构建 MQTT 连接参数clientId 需要添加 |authType=register| 后缀)
String clientId = IotDeviceAuthUtils.buildClientId(PRODUCT_KEY, deviceName) + "|authType=register|";
String username = IotDeviceAuthUtils.buildUsername(PRODUCT_KEY, deviceName);
log.info("[testDeviceRegister][注册参数: clientId={}, username={}, sign={}]",
clientId, username, sign);
// 1.3 创建客户端并连接(连接时服务端自动处理注册)
MqttClientOptions options = new MqttClientOptions()
.setClientId(clientId)
.setUsername(username)
.setPassword(sign)
.setCleanSession(true)
.setKeepAliveInterval(60);
MqttClient client = MqttClient.create(vertx, options);
try { try {
// 2.1 构建注册消息 // 2. 设置消息处理器,接收注册响应
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() CompletableFuture<IotDeviceMessage> responseFuture = new CompletableFuture<>();
.setProductKey(PRODUCT_KEY) client.publishHandler(message -> {
.setDeviceName("test-mqtt-" + System.currentTimeMillis()) log.info("[testDeviceRegister][收到响应: topic={}, payload={}]",
.setProductSecret("test-product-secret"); message.topicName(), message.payload().toString());
IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessage response = CODEC.decode(message.payload().getBytes());
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), responseFuture.complete(response);
registerReqDTO); });
// 2.2 订阅 _reply 主题 // 3. 连接服务器(连接成功后服务端会自动处理注册并发送响应)
String replyTopic = String.format("/sys/%s/%s/thing/auth/register_reply", client.connect(SERVER_PORT, SERVER_HOST)
registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); .toCompletionStage().toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
subscribe(client, replyTopic); log.info("[testDeviceRegister][连接成功,等待注册响应...]");
// 3. 发布消息并等待响应 // 4. 等待注册响应
String topic = String.format("/sys/%s/%s/thing/auth/register", IotDeviceMessage response = responseFuture.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); log.info("[testDeviceRegister][注册响应: {}]", response);
IotDeviceMessage response = publishAndWaitReply(client, topic, request);
log.info("[testDeviceRegister][响应消息: {}]", response);
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
} finally { } finally {
disconnect(client); disconnect(client);

View File

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpCodecTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodecFactory;
@@ -146,10 +147,13 @@ public class IotDirectDeviceTcpProtocolIntegrationTest {
@Test @Test
public void testDeviceRegister() throws Exception { public void testDeviceRegister() throws Exception {
// 1. 构建注册消息 // 1. 构建注册消息
String deviceName = "test-tcp-" + System.currentTimeMillis();
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(PRODUCT_KEY) .setProductKey(PRODUCT_KEY)
.setDeviceName("test-tcp-" + System.currentTimeMillis()) .setDeviceName(deviceName)
.setProductSecret("test-product-secret"); .setSign(sign);
IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessage request = IotDeviceMessage.requestOf(
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);

View File

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -100,10 +101,13 @@ public class IotDirectDeviceUdpProtocolIntegrationTest {
@Test @Test
public void testDeviceRegister() throws Exception { public void testDeviceRegister() throws Exception {
// 1. 构建注册消息 // 1. 构建注册消息
String deviceName = "test-udp-" + System.currentTimeMillis();
String productSecret = "test-product-secret"; // 替换为实际的 productSecret
String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO() IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(PRODUCT_KEY) .setProductKey(PRODUCT_KEY)
.setDeviceName("test-udp-" + System.currentTimeMillis()) .setDeviceName(deviceName)
.setProductSecret("test-product-secret"); .setSign(sign);
IotDeviceMessage request = IotDeviceMessage.requestOf( IotDeviceMessage request = IotDeviceMessage.requestOf(
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO); IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO);

View File

@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO; import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer; import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
@@ -131,10 +132,13 @@ public class IotDirectDeviceWebSocketProtocolIntegrationTest {
@Test @Test
public void testDeviceRegister() throws Exception { public void testDeviceRegister() throws Exception {
// 1.1 构建注册消息 // 1.1 构建注册消息
IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO(); String deviceName = "test-ws-" + System.currentTimeMillis();
registerReqDTO.setProductKey(PRODUCT_KEY); String productSecret = "test-product-secret"; // 替换为实际的 productSecret
registerReqDTO.setDeviceName("test-ws-" + System.currentTimeMillis()); String sign = IotProductAuthUtils.buildSign(PRODUCT_KEY, deviceName, productSecret);
registerReqDTO.setProductSecret("test-product-secret"); IotDeviceRegisterReqDTO registerReqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(PRODUCT_KEY)
.setDeviceName(deviceName)
.setSign(sign);
IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(), IotDeviceMessage request = IotDeviceMessage.of(IdUtil.fastSimpleUUID(),
IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null); IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerReqDTO, null, null, null);
// 1.2 序列化 // 1.2 序列化