From 38a21ad59c513c957079c97df99cc596859fd61c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 25 Jan 2026 11:16:07 +0800 Subject: [PATCH] feat(iot):【网关设备:70%】动态注册的初步实现(未测试),基于 stateful-sauteeing-pillow.md 规划 --- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java | 9 +++++++++ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java | 6 ++++++ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java | 4 ++++ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java | 8 ++++++++ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java | 5 ++++- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java | 14 +++++++++++++- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java | 4 +++- yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java | 10 ++++++++++ yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java | 5 +++-- yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java | 35 +++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java | 6 ++++-- yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java | 4 ++-- yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java | 6 ++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java | 3 ++- yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java | 7 +++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDeviceRegisterHttpProtocolIntegrationTest.java | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java | 4 +++- 22 files changed, 634 insertions(+), 65 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDeviceRegisterHttpProtocolIntegrationTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java index eb55b18..71b7796 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +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.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; @@ -57,4 +59,11 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { })); } + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register") + @PermitAll + public CommonResult registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) { + return success(deviceService.registerDevice(reqDTO)); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index ab581d2..ffc92a2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -27,6 +27,12 @@ public class IotProductRespVO { @ExcelProperty("产品标识") private String productKey; + @Schema(description = "产品密钥", requiredMode = Schema.RequiredMode.REQUIRED) + private String productSecret; + + @Schema(description = "是否开启动态注册", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean registerEnabled; + @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long categoryId; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 38f2d24..85a151f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -48,4 +48,8 @@ public class IotProductSaveReqVO { @NotEmpty(message = "数据格式不能为空") private String codecType; + @Schema(description = "是否开启动态注册", example = "false") + @NotEmpty(message = "是否开启动态注册不能为空") + private Boolean registerEnabled; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index 376360e..e296b35 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java @@ -33,6 +33,14 @@ public class IotProductDO extends TenantBaseDO { */ private String productKey; /** + * 产品密钥,用于一型一密动态注册 + */ + private String productSecret; + /** + * 是否开启动态注册 + */ + private Boolean registerEnabled; + /** * 产品分类编号 *

* 关联 {@link IotProductCategoryDO#getId()} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 2e74ebc..44ead7b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -40,9 +40,12 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效"); ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败"); ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关"); - // 子设备注册相关错误码 1-050-003-200 + // 设备注册相关错误码 1-050-003-200 ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效"); ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型"); + ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能"); + ErrorCode DEVICE_REGISTER_SIGN_INVALID = new ErrorCode(1_050_003_211, "动态注册签名验证失败"); + ErrorCode DEVICE_ALREADY_ACTIVATED = new ErrorCode(1_050_003_212, "设备已激活,不允许重复注册"); // ========== 产品分类 1-050-004-000 ========== ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 717376f..fa5dcd3 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 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.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; @@ -292,7 +294,7 @@ public interface IotDeviceService { */ List getDeviceListByHasLocation(); - // ========== 网关-子设备绑定相关 ========== + // ========== 网关-拓扑管理(后台操作) ========== /** * 绑定子设备到网关 @@ -354,6 +356,8 @@ public interface IotDeviceService { */ IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice); + // ========== 设备动态注册 ========== + /** * 处理子设备动态注册消息(网关设备上报) * @@ -363,4 +367,12 @@ public interface IotDeviceService { */ List handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice); + /** + * 设备动态注册(直连设备/网关) + * + * @param reqDTO 动态注册请求 + * @return 注册结果(包含 DeviceSecret) + */ + IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO); + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index fc6b5ee..5602f17 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 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.IotDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; @@ -77,6 +79,10 @@ public class IotDeviceServiceImpl implements IotDeviceService { @Lazy // 延迟加载,解决循环依赖 private IotDeviceMessageService deviceMessageService; + private IotDeviceServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + @Override public Long createDevice(IotDeviceSaveReqVO createReqVO) { return createDevice0(createReqVO).getId(); @@ -138,7 +144,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { device.setProductId(product.getId()).setProductKey(product.getProductKey()) .setDeviceType(product.getDeviceType()); // 生成密钥 - device.setDeviceSecret(generateDeviceSecret()); + device.setDeviceSecret(IotDeviceAuthUtils.generateDeviceSecret()); // 设置设备状态为未激活 device.setState(IotDeviceStateEnum.INACTIVE.getState()); } @@ -368,15 +374,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { return deviceMapper.selectCountByGroupId(groupId); } - /** - * 生成 deviceSecret - * - * @return 生成的 deviceSecret - */ - private String generateDeviceSecret() { - return IdUtil.fastSimpleUUID(); - } - @Override @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 public IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport) { @@ -566,7 +563,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { return deviceMapper.selectListByHasLocation(); } - // ========== 网关-子设备绑定相关 ========== + // ========== 网关-拓扑管理(后台操作) ========== @Override @Transactional(rollbackFor = Exception.class) @@ -766,6 +763,79 @@ public class IotDeviceServiceImpl implements IotDeviceService { return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities); } + /** + * 发送拓扑变更通知给网关设备 + * + * @param gatewayId 网关设备编号 + * @param status 变更状态(0-创建, 1-删除) + * @param subDevices 子设备列表 + * @see 阿里云 - 通知网关拓扑关系变化 + */ + private void sendTopoChangeNotify(Long gatewayId, Integer status, List subDevices) { + if (CollUtil.isEmpty(subDevices)) { + return; + } + // 1. 获取网关设备 + IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId); + if (gatewayDevice == null) { + log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId); + return; + } + + try { + // 2.1 构建拓扑变更通知消息 + List subList = convertList(subDevices, subDevice -> + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName())); + IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList); + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf( + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params); + + // 2.2 发送消息 + deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice); + log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]", + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), + status, subList); + } catch (Exception ex) { + log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]", + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex); + } + } + + // ========== 设备动态注册 ========== + + @Override + public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) { + // 1.1 校验产品 + IotProductDO product = productService.getProductByProductKey(reqDTO.getProductKey()); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + // 1.2 校验产品是否开启动态注册 + if (BooleanUtil.isFalse(product.getRegisterEnabled())) { + throw exception(DEVICE_REGISTER_DISABLED); + } + // 1.3 验证签名 + if (!IotDeviceAuthUtils.verifyRegisterSign(product.getProductSecret(), + reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getRandom(), reqDTO.getSign())) { + throw exception(DEVICE_REGISTER_SIGN_INVALID); + } + + // 4. 查找设备(预注册模式:设备必须已存在) + // TODO @AI:设备不用提前有,这个有问题! + IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName()); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS); + } + + // 5. 校验设备是否已激活(已激活的设备不允许重复注册) + if (!Objects.equals(device.getState(), IotDeviceStateEnum.INACTIVE.getState())) { + throw exception(DEVICE_ALREADY_ACTIVATED); + } + + // 6. 返回设备密钥 + return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret()); + } + @Override public List handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) { // 1.1 校验网关设备类型 @@ -799,6 +869,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { return results; } + // TODO @AI:阿里云的,设备必须存在; private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) { // 1.1 校验产品 IotProductDO product = productService.getProductByProductKey(params.getProductKey()); @@ -833,46 +904,4 @@ public class IotDeviceServiceImpl implements IotDeviceService { return newDevice; } - private IotDeviceServiceImpl getSelf() { - return SpringUtil.getBean(getClass()); - } - - /** - * 发送拓扑变更通知给网关设备 - * - * @param gatewayId 网关设备编号 - * @param status 变更状态(0-创建, 1-删除) - * @param subDevices 子设备列表 - * @see 阿里云 - 通知网关拓扑关系变化 - */ - private void sendTopoChangeNotify(Long gatewayId, Integer status, List subDevices) { - if (CollUtil.isEmpty(subDevices)) { - return; - } - // 1. 获取网关设备 - IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId); - if (gatewayDevice == null) { - log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId); - return; - } - - try { - // 2.1 构建拓扑变更通知消息 - List subList = convertList(subDevices, subDevice -> - new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName())); - IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList); - IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf( - IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params); - - // 2.2 发送消息 - deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice); - log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]", - gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), - status, subList); - } catch (Exception ex) { - log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]", - gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex); - } - } - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index a07d027..f7195f6 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; @@ -53,7 +54,8 @@ public class IotProductServiceImpl implements IotProductService { // 2. 插入 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) - .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()) + .setProductSecret(IotDeviceAuthUtils.generateProductSecret()); productMapper.insert(product); return product.getId(); } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java index 29d540e..c86c429 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; /** * IoT 设备通用 API @@ -28,4 +30,12 @@ public interface IotDeviceCommonApi { */ CommonResult getDevice(IotDeviceGetReqDTO infoReqDTO); + /** + * 设备动态注册(一型一密) + * + * @param reqDTO 动态注册请求 + * @return 注册结果(包含 DeviceSecret) + */ + CommonResult registerDevice(IotDeviceRegisterReqDTO reqDTO); + } diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java index c65c6f4..d980032 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java @@ -33,9 +33,10 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable { TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false), // ========== 设备注册 ========== - // 可参考:https://help.aliyun.com/zh/iot/user-guide/register-devices + // 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification - SUB_DEVICE_REGISTER("thing.sub.register", "子设备动态注册", true), + DEVICE_REGISTER("thing.auth.register", "设备动态注册", true), + SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true), // ========== 设备属性 ========== // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java new file mode 100644 index 0000000..1b2c86f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.core.topic.auth; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * IoT 设备动态注册 Request DTO + *

+ * 用于直连设备/网关的一型一密动态注册:使用 ProductSecret 验证签名,返回 DeviceSecret + * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +@Data +public class IotDeviceRegisterReqDTO { + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + // TODO @AI:可以去掉 random 字段; + /** + * 随机数,用于签名 + */ + @NotEmpty(message = "随机数不能为空") + private String random; + + // TODO @AI:看起来,是直接带 productSecret 阿里云上,你在检查下! + /** + * 签名 + */ + @NotEmpty(message = "签名不能为空") + private String sign; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java new file mode 100644 index 0000000..707f798 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.core.topic.auth; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备动态注册 Response DTO + *

+ * 用于直连设备/网关的一型一密动态注册响应 + * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceRegisterRespDTO { + + /** + * 产品标识 + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 设备密钥 + */ + private String deviceSecret; + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java index 765780a..cf34a1d 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java @@ -6,10 +6,12 @@ import lombok.Data; /** * IoT 子设备动态注册 Request DTO *

- * 用于 thing.sub.register 消息的 params 数组元素 + * 用于 thing.auth.register.sub 消息的 params 数组元素 + * + * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 * * @author 芋道源码 - * @see 阿里云 - 动态注册子设备 + * @see 阿里云 - 动态注册子设备 */ @Data public class IotSubDeviceRegisterReqDTO { diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java index bf054f2..a45f14d 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java @@ -7,10 +7,10 @@ import lombok.NoArgsConstructor; /** * IoT 子设备动态注册 Response DTO *

- * 用于 thing.sub.register 响应的设备信息 + * 用于 thing.auth.register.sub 响应的设备信息 * * @author 芋道源码 - * @see 阿里云 - 动态注册子设备 + * @see 阿里云 - 动态注册子设备 */ @Data @NoArgsConstructor diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java index 9ee953e..df1bbbb 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.core.util; +import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.crypto.digest.HmacAlgorithm; @@ -87,4 +88,62 @@ public class IotDeviceAuthUtils { return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); } + // ========== 动态注册相关方法 ========== + + // TODO @AI:想了下,还是放回到对应的 productService、deviceService 更合适; + + /** + * 生成产品密钥 + * + * @return 产品密钥(UUID) + */ + public static String generateProductSecret() { + return IdUtil.fastSimpleUUID(); + } + + /** + * 生成设备密钥 + * + * @return 设备密钥(UUID) + */ + public static String generateDeviceSecret() { + return IdUtil.fastSimpleUUID(); + } + + // TODO @AI:去掉 random; + /** + * 计算动态注册签名 + *

+ * 参考阿里云规范,参数按字典序排列拼接 + * + * @param productSecret 产品密钥 + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param random 随机数 + * @return 签名 + * @see 一型一密 + */ + public static String buildRegisterSign(String productSecret, String productKey, String deviceName, String random) { + String content = "deviceName" + deviceName + "productKey" + productKey + "random" + random; + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret)) + .digestHex(content); + } + + // TODO @AI:是不是调用方自己验证就好了,不要这里面抽; + /** + * 验证动态注册签名 + * + * @param productSecret 产品密钥 + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param random 随机数 + * @param sign 待验证的签名 + * @return 是否验证通过 + */ + public static boolean verifyRegisterSign(String productSecret, String productKey, + String deviceName, String random, String sign) { + String expectedSign = buildRegisterSign(productSecret, productKey, deviceName, random); + return expectedSign.equals(sign); + } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java index eda59d1..2282606 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; import io.vertx.core.AbstractVerticle; import io.vertx.core.Vertx; @@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { // 创建处理器,添加路由处理器 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); router.post(IotHttpAuthHandler.PATH).handler(authHandler); + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler(); + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler); + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(this); + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler); IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java index f5461c2..883e323 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java @@ -7,6 +7,7 @@ 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.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; import io.vertx.core.Handler; @@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler private void beforeHandle(RoutingContext context) { // 如果不需要认证,则不走前置处理 String path = context.request().path(); - if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { + if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) { return; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java new file mode 100644 index 0000000..56a02d7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 网关 HTTP 协议的【设备动态注册】处理器 + *

+ * 用于直连设备/网关的一型一密动态注册,不需要认证 + * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +public class IotHttpRegisterHandler extends IotHttpAbstractHandler { + + public static final String PATH = "/auth/register/device"; + + private final IotDeviceCommonApi deviceApi; + + public IotHttpRegisterHandler() { + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + public CommonResult handle0(RoutingContext context) { + // 1. 解析参数 + // TODO @AI:参数不太对,看看我写的建议 + JsonObject body = context.body().asJsonObject(); + String productKey = body.getString("productKey"); + if (StrUtil.isEmpty(productKey)) { + throw invalidParamException("productKey 不能为空"); + } + String deviceName = body.getString("deviceName"); + if (StrUtil.isEmpty(deviceName)) { + throw invalidParamException("deviceName 不能为空"); + } + String random = body.getString("random"); + if (StrUtil.isEmpty(random)) { + throw invalidParamException("random 不能为空"); + } + String sign = body.getString("sign"); + if (StrUtil.isEmpty(sign)) { + throw invalidParamException("sign 不能为空"); + } + + // 2. 调用动态注册 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO() + .setProductKey(productKey) + .setDeviceName(deviceName) + .setRandom(random) + .setSign(sign); + CommonResult result = deviceApi.registerDevice(reqDTO); + result.checkError(); + + // 3. 返回结果 + return success(result.getData()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java new file mode 100644 index 0000000..e4ace04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import io.vertx.ext.web.RoutingContext; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 网关 HTTP 协议的【子设备动态注册】处理器 + *

+ * 用于子设备的动态注册,需要网关认证 + * + * @author 芋道源码 + * @see 阿里云 - 动态注册子设备 + */ +public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler { + + /** + * 路径:/auth/register/sub-device/:productKey/:deviceName + *

+ * productKey 和 deviceName 是网关设备的标识 + */ + public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName"; + + private final IotHttpUpstreamProtocol protocol; + + private final IotDeviceMessageService deviceMessageService; + + public IotHttpRegisterSubHandler(IotHttpUpstreamProtocol protocol) { + this.protocol = protocol; + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); + } + + @Override + public CommonResult handle0(RoutingContext context) { + // 1. 解析通用参数 + String productKey = context.pathParam("productKey"); + String deviceName = context.pathParam("deviceName"); + + // 2.1 解析消息 + byte[] bytes = context.body().buffer().getBytes(); + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, productKey, deviceName); + // 2.2 设置方法 + message.setMethod(IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()); + + // TODO @AI:可能还是需要一个新的 deviceApi 接口。因为 register sub 子设备不太一行; + // 2.3 发送消息 + Object responseData = deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId()); + + // 3. 返回结果 + return success(responseData); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java index b325103..271a103 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; +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.gateway.config.IotGatewayProperties; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; @@ -54,6 +56,11 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); } + @Override + public CommonResult registerDevice(IotDeviceRegisterReqDTO reqDTO) { + return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { }); + } + private CommonResult doPost(String url, T body, ParameterizedTypeReference> responseType) { try { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDeviceRegisterHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDeviceRegisterHttpProtocolIntegrationTest.java new file mode 100644 index 0000000..2db36f5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDeviceRegisterHttpProtocolIntegrationTest.java @@ -0,0 +1,203 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +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.topic.auth.IotDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +// TODO @AI:合并到 IotDirectDeviceHttpProtocolIntegrationTest 里呀,没必要拆开;只搞一个直连设备的注册就好了; +/** + * IoT 设备动态注册 HTTP 协议集成测试(手动测试) + * + *

测试场景:一型一密(One Type One Secret)动态注册机制 + * + *

前置条件: + *

    + *
  1. 产品已开启动态注册(registerEnabled = true)
  2. + *
  3. 设备已预先创建(预注册模式)
  4. + *
  5. 设备 deviceSecret 为空(未激活状态)
  6. + *
+ * + *

使用步骤: + *

    + *
  1. 启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)
  2. + *
  3. 运行 {@link #testDeviceRegister()} 测试直连设备/网关动态注册
  4. + *
  5. 运行 {@link #testSubDeviceRegister()} 测试子设备动态注册(需要先获取网关 Token)
  6. + *
+ * + * @author 芋道源码 + * @see 阿里云 - 一型一密 + */ +@Slf4j +@SuppressWarnings("HttpUrlsUsage") +public class IotDeviceRegisterHttpProtocolIntegrationTest { + + private static final String SERVER_HOST = "127.0.0.1"; + private static final int SERVER_PORT = 8092; + + // ===================== 直连设备/网关动态注册配置(根据实际情况修改) ===================== + /** + * 产品 Key(需要开启动态注册) + */ + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT"; + /** + * 产品密钥(从 iot_product 表的 product_secret 字段获取) + */ + private static final String PRODUCT_SECRET = "your_product_secret"; + /** + * 设备名称(需要预先创建,deviceSecret 为空) + */ + private static final String DEVICE_NAME = "test-register-device"; + + // ===================== 网关设备信息(用于子设备动态注册) ===================== + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v"; + private static final String GATEWAY_DEVICE_NAME = "sub-ddd"; + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3"; + + /** + * 网关设备 Token:从网关认证获取后,粘贴到这里 + */ + private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU"; + + // ===================== 子设备信息(用于子设备动态注册) ===================== + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn"; + private static final String SUB_DEVICE_NAME = "test-sub-register-device"; + + // ===================== 直连设备/网关动态注册测试 ===================== + + /** + * 直连设备/网关动态注册测试 + *

+ * 使用产品密钥(productSecret)进行签名验证,成功后返回设备密钥(deviceSecret) + *

+ * 注意:此接口不需要 Token 认证 + */ + @Test + public void testDeviceRegister() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT); + // 1.2 生成签名 + String random = IdUtil.fastSimpleUUID(); + String sign = IotDeviceAuthUtils.buildRegisterSign(PRODUCT_SECRET, PRODUCT_KEY, DEVICE_NAME, random); + // 1.3 构建请求参数 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO(); + reqDTO.setProductKey(PRODUCT_KEY); + reqDTO.setDeviceName(DEVICE_NAME); + reqDTO.setRandom(random); + reqDTO.setSign(sign); + String payload = JsonUtils.toJsonString(reqDTO); + // 1.4 输出请求 + log.info("[testDeviceRegister][请求 URL: {}]", url); + log.info("[testDeviceRegister][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testDeviceRegister][响应体: {}]", response); + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]"); + } + + /** + * 测试动态注册后使用 deviceSecret 进行认证 + *

+ * 此测试需要先执行 testDeviceRegister 获取 deviceSecret + */ + @Test + public void testAuthAfterRegister() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); + // TODO 将 testDeviceRegister 返回的 deviceSecret 填入此处 + String deviceSecret = "返回的deviceSecret"; + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, deviceSecret); + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() + .setClientId(authInfo.getClientId()) + .setUsername(authInfo.getUsername()) + .setPassword(authInfo.getPassword()); + String payload = JsonUtils.toJsonString(authReqDTO); + // 1.2 输出请求 + log.info("[testAuthAfterRegister][请求 URL: {}]", url); + log.info("[testAuthAfterRegister][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testAuthAfterRegister][响应体: {}]", response); + } + + // ===================== 网关认证测试 ===================== + + /** + * 网关设备认证测试:获取网关设备 Token(用于后续子设备动态注册) + */ + @Test + public void testGatewayAuth() { + // 1.1 构建请求 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT); + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo( + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET); + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO() + .setClientId(authInfo.getClientId()) + .setUsername(authInfo.getUsername()) + .setPassword(authInfo.getPassword()); + String payload = JsonUtils.toJsonString(authReqDTO); + // 1.2 输出请求 + log.info("[testGatewayAuth][请求 URL: {}]", url); + log.info("[testGatewayAuth][请求体: {}]", payload); + + // 2.1 发送请求 + String response = HttpUtil.post(url, payload); + // 2.2 输出结果 + log.info("[testGatewayAuth][响应体: {}]", response); + log.info("[testGatewayAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]"); + } + + // ===================== 子设备动态注册测试 ===================== + + /** + * 子设备动态注册测试 + *

+ * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret + *

+ * 注意:此接口需要网关 Token 认证 + */ + @Test + public void testSubDeviceRegister() { + // 1.1 构建请求(需要网关认证) + String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s", + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); + // 1.2 构建请求参数 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY); + subDevice.setDeviceName(SUB_DEVICE_NAME); + String payload = JsonUtils.toJsonString(MapUtil.builder() + .put("id", IdUtil.fastSimpleUUID()) + .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod()) + .put("version", "1.0") + .put("params", Collections.singletonList(subDevice)) + .build()); + // 1.3 输出请求 + log.info("[testSubDeviceRegister][请求 URL: {}]", url); + log.info("[testSubDeviceRegister][请求体: {}]", payload); + + // 2.1 发送请求(需要网关 Token) + try (HttpResponse httpResponse = HttpUtil.createPost(url) + .header("Authorization", GATEWAY_TOKEN) + .body(payload) + .execute()) { + // 2.2 输出结果 + log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body()); + log.info("[testSubDeviceRegister][成功后可使用返回的 deviceSecret 进行子设备认证]"); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java index e431bfb..281ff4f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotGatewayDeviceHttpProtocolIntegrationTest.java @@ -211,11 +211,13 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest { * 子设备动态注册测试 *

* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret + *

+ * 注意:此接口需要网关 Token 认证 */ @Test public void testSubDeviceRegister() { // 1.1 构建请求 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/sub/register", + String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s", SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); // 1.2 构建请求参数 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); -- libgit2 0.21.4