Commit 38a21ad59c513c957079c97df99cc596859fd61c

Authored by YunaiV
1 parent 1309be39

feat(iot):【网关设备:70%】动态注册的初步实现(未测试),基于 stateful-sauteeing-pillow.md 规划

Showing 22 changed files with 634 additions and 65 deletions
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; @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
10 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
11 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 13 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
12 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 14 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -57,4 +59,11 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @@ -57,4 +59,11 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
57 })); 59 }));
58 } 60 }
59 61
  62 + @Override
  63 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
  64 + @PermitAll
  65 + public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) {
  66 + return success(deviceService.registerDevice(reqDTO));
  67 + }
  68 +
60 } 69 }
61 \ No newline at end of file 70 \ No newline at end of file
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 { @@ -27,6 +27,12 @@ public class IotProductRespVO {
27 @ExcelProperty("产品标识") 27 @ExcelProperty("产品标识")
28 private String productKey; 28 private String productKey;
29 29
  30 + @Schema(description = "产品密钥", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private String productSecret;
  32 +
  33 + @Schema(description = "是否开启动态注册", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  34 + private Boolean registerEnabled;
  35 +
30 @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 36 @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
31 private Long categoryId; 37 private Long categoryId;
32 38
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 { @@ -48,4 +48,8 @@ public class IotProductSaveReqVO {
48 @NotEmpty(message = "数据格式不能为空") 48 @NotEmpty(message = "数据格式不能为空")
49 private String codecType; 49 private String codecType;
50 50
  51 + @Schema(description = "是否开启动态注册", example = "false")
  52 + @NotEmpty(message = "是否开启动态注册不能为空")
  53 + private Boolean registerEnabled;
  54 +
51 } 55 }
52 \ No newline at end of file 56 \ No newline at end of file
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 { @@ -33,6 +33,14 @@ public class IotProductDO extends TenantBaseDO {
33 */ 33 */
34 private String productKey; 34 private String productKey;
35 /** 35 /**
  36 + * 产品密钥,用于一型一密动态注册
  37 + */
  38 + private String productSecret;
  39 + /**
  40 + * 是否开启动态注册
  41 + */
  42 + private Boolean registerEnabled;
  43 + /**
36 * 产品分类编号 44 * 产品分类编号
37 * <p> 45 * <p>
38 * 关联 {@link IotProductCategoryDO#getId()} 46 * 关联 {@link IotProductCategoryDO#getId()}
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 { @@ -40,9 +40,12 @@ public interface ErrorCodeConstants {
40 ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效"); 40 ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
41 ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败"); 41 ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
42 ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关"); 42 ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
43 - // 子设备注册相关错误码 1-050-003-200 43 + // 设备注册相关错误码 1-050-003-200
44 ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效"); 44 ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
45 ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型"); 45 ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
  46 + ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
  47 + ErrorCode DEVICE_REGISTER_SIGN_INVALID = new ErrorCode(1_050_003_211, "动态注册签名验证失败");
  48 + ErrorCode DEVICE_ALREADY_ACTIVATED = new ErrorCode(1_050_003_212, "设备已激活,不允许重复注册");
46 49
47 // ========== 产品分类 1-050-004-000 ========== 50 // ========== 产品分类 1-050-004-000 ==========
48 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); 51 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
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; @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; 8 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
9 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; 11 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
10 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; 12 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
11 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 13 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
@@ -292,7 +294,7 @@ public interface IotDeviceService { @@ -292,7 +294,7 @@ public interface IotDeviceService {
292 */ 294 */
293 List<IotDeviceDO> getDeviceListByHasLocation(); 295 List<IotDeviceDO> getDeviceListByHasLocation();
294 296
295 - // ========== 网关-子设备绑定相关 ========== 297 + // ========== 网关-拓扑管理(后台操作) ==========
296 298
297 /** 299 /**
298 * 绑定子设备到网关 300 * 绑定子设备到网关
@@ -354,6 +356,8 @@ public interface IotDeviceService { @@ -354,6 +356,8 @@ public interface IotDeviceService {
354 */ 356 */
355 IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice); 357 IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice);
356 358
  359 + // ========== 设备动态注册 ==========
  360 +
357 /** 361 /**
358 * 处理子设备动态注册消息(网关设备上报) 362 * 处理子设备动态注册消息(网关设备上报)
359 * 363 *
@@ -363,4 +367,12 @@ public interface IotDeviceService { @@ -363,4 +367,12 @@ public interface IotDeviceService {
363 */ 367 */
364 List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice); 368 List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
365 369
  370 + /**
  371 + * 设备动态注册(直连设备/网关)
  372 + *
  373 + * @param reqDTO 动态注册请求
  374 + * @return 注册结果(包含 DeviceSecret)
  375 + */
  376 + IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
  377 +
366 } 378 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
1 package cn.iocoder.yudao.module.iot.service.device; 1 package cn.iocoder.yudao.module.iot.service.device;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 -import cn.hutool.core.util.IdUtil; 4 +import cn.hutool.core.util.BooleanUtil;
5 import cn.hutool.core.util.ObjUtil; 5 import cn.hutool.core.util.ObjUtil;
6 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
7 import cn.hutool.extra.spring.SpringUtil; 7 import cn.hutool.extra.spring.SpringUtil;
@@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; @@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
18 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 18 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; 20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  21 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  22 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
21 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; 23 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
22 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; 24 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
23 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; 25 import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
@@ -77,6 +79,10 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -77,6 +79,10 @@ public class IotDeviceServiceImpl implements IotDeviceService {
77 @Lazy // 延迟加载,解决循环依赖 79 @Lazy // 延迟加载,解决循环依赖
78 private IotDeviceMessageService deviceMessageService; 80 private IotDeviceMessageService deviceMessageService;
79 81
  82 + private IotDeviceServiceImpl getSelf() {
  83 + return SpringUtil.getBean(getClass());
  84 + }
  85 +
80 @Override 86 @Override
81 public Long createDevice(IotDeviceSaveReqVO createReqVO) { 87 public Long createDevice(IotDeviceSaveReqVO createReqVO) {
82 return createDevice0(createReqVO).getId(); 88 return createDevice0(createReqVO).getId();
@@ -138,7 +144,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -138,7 +144,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
138 device.setProductId(product.getId()).setProductKey(product.getProductKey()) 144 device.setProductId(product.getId()).setProductKey(product.getProductKey())
139 .setDeviceType(product.getDeviceType()); 145 .setDeviceType(product.getDeviceType());
140 // 生成密钥 146 // 生成密钥
141 - device.setDeviceSecret(generateDeviceSecret()); 147 + device.setDeviceSecret(IotDeviceAuthUtils.generateDeviceSecret());
142 // 设置设备状态为未激活 148 // 设置设备状态为未激活
143 device.setState(IotDeviceStateEnum.INACTIVE.getState()); 149 device.setState(IotDeviceStateEnum.INACTIVE.getState());
144 } 150 }
@@ -368,15 +374,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -368,15 +374,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
368 return deviceMapper.selectCountByGroupId(groupId); 374 return deviceMapper.selectCountByGroupId(groupId);
369 } 375 }
370 376
371 - /**  
372 - * 生成 deviceSecret  
373 - *  
374 - * @return 生成的 deviceSecret  
375 - */  
376 - private String generateDeviceSecret() {  
377 - return IdUtil.fastSimpleUUID();  
378 - }  
379 -  
380 @Override 377 @Override
381 @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 378 @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
382 public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) { 379 public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
@@ -566,7 +563,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -566,7 +563,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
566 return deviceMapper.selectListByHasLocation(); 563 return deviceMapper.selectListByHasLocation();
567 } 564 }
568 565
569 - // ========== 网关-子设备绑定相关 ========== 566 + // ========== 网关-拓扑管理(后台操作) ==========
570 567
571 @Override 568 @Override
572 @Transactional(rollbackFor = Exception.class) 569 @Transactional(rollbackFor = Exception.class)
@@ -766,6 +763,79 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -766,6 +763,79 @@ public class IotDeviceServiceImpl implements IotDeviceService {
766 return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities); 763 return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities);
767 } 764 }
768 765
  766 + /**
  767 + * 发送拓扑变更通知给网关设备
  768 + *
  769 + * @param gatewayId 网关设备编号
  770 + * @param status 变更状态(0-创建, 1-删除)
  771 + * @param subDevices 子设备列表
  772 + * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
  773 + */
  774 + private void sendTopoChangeNotify(Long gatewayId, Integer status, List<IotDeviceDO> subDevices) {
  775 + if (CollUtil.isEmpty(subDevices)) {
  776 + return;
  777 + }
  778 + // 1. 获取网关设备
  779 + IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId);
  780 + if (gatewayDevice == null) {
  781 + log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId);
  782 + return;
  783 + }
  784 +
  785 + try {
  786 + // 2.1 构建拓扑变更通知消息
  787 + List<IotDeviceIdentity> subList = convertList(subDevices, subDevice ->
  788 + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  789 + IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList);
  790 + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf(
  791 + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params);
  792 +
  793 + // 2.2 发送消息
  794 + deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice);
  795 + log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]",
  796 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  797 + status, subList);
  798 + } catch (Exception ex) {
  799 + log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]",
  800 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex);
  801 + }
  802 + }
  803 +
  804 + // ========== 设备动态注册 ==========
  805 +
  806 + @Override
  807 + public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) {
  808 + // 1.1 校验产品
  809 + IotProductDO product = productService.getProductByProductKey(reqDTO.getProductKey());
  810 + if (product == null) {
  811 + throw exception(PRODUCT_NOT_EXISTS);
  812 + }
  813 + // 1.2 校验产品是否开启动态注册
  814 + if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
  815 + throw exception(DEVICE_REGISTER_DISABLED);
  816 + }
  817 + // 1.3 验证签名
  818 + if (!IotDeviceAuthUtils.verifyRegisterSign(product.getProductSecret(),
  819 + reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getRandom(), reqDTO.getSign())) {
  820 + throw exception(DEVICE_REGISTER_SIGN_INVALID);
  821 + }
  822 +
  823 + // 4. 查找设备(预注册模式:设备必须已存在)
  824 + // TODO @AI:设备不用提前有,这个有问题!
  825 + IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName());
  826 + if (device == null) {
  827 + throw exception(DEVICE_NOT_EXISTS);
  828 + }
  829 +
  830 + // 5. 校验设备是否已激活(已激活的设备不允许重复注册)
  831 + if (!Objects.equals(device.getState(), IotDeviceStateEnum.INACTIVE.getState())) {
  832 + throw exception(DEVICE_ALREADY_ACTIVATED);
  833 + }
  834 +
  835 + // 6. 返回设备密钥
  836 + return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
  837 + }
  838 +
769 @Override 839 @Override
770 public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) { 840 public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
771 // 1.1 校验网关设备类型 841 // 1.1 校验网关设备类型
@@ -799,6 +869,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -799,6 +869,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
799 return results; 869 return results;
800 } 870 }
801 871
  872 + // TODO @AI:阿里云的,设备必须存在;
802 private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) { 873 private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
803 // 1.1 校验产品 874 // 1.1 校验产品
804 IotProductDO product = productService.getProductByProductKey(params.getProductKey()); 875 IotProductDO product = productService.getProductByProductKey(params.getProductKey());
@@ -833,46 +904,4 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -833,46 +904,4 @@ public class IotDeviceServiceImpl implements IotDeviceService {
833 return newDevice; 904 return newDevice;
834 } 905 }
835 906
836 - private IotDeviceServiceImpl getSelf() {  
837 - return SpringUtil.getBean(getClass());  
838 - }  
839 -  
840 - /**  
841 - * 发送拓扑变更通知给网关设备  
842 - *  
843 - * @param gatewayId 网关设备编号  
844 - * @param status 变更状态(0-创建, 1-删除)  
845 - * @param subDevices 子设备列表  
846 - * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>  
847 - */  
848 - private void sendTopoChangeNotify(Long gatewayId, Integer status, List<IotDeviceDO> subDevices) {  
849 - if (CollUtil.isEmpty(subDevices)) {  
850 - return;  
851 - }  
852 - // 1. 获取网关设备  
853 - IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId);  
854 - if (gatewayDevice == null) {  
855 - log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId);  
856 - return;  
857 - }  
858 -  
859 - try {  
860 - // 2.1 构建拓扑变更通知消息  
861 - List<IotDeviceIdentity> subList = convertList(subDevices, subDevice ->  
862 - new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));  
863 - IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList);  
864 - IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf(  
865 - IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params);  
866 -  
867 - // 2.2 发送消息  
868 - deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice);  
869 - log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]",  
870 - gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),  
871 - status, subList);  
872 - } catch (Exception ex) {  
873 - log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]",  
874 - gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex);  
875 - }  
876 - }  
877 -  
878 } 907 }
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; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
6 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 7 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
7 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; 8 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
8 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; 9 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO;
@@ -53,7 +54,8 @@ public class IotProductServiceImpl implements IotProductService { @@ -53,7 +54,8 @@ public class IotProductServiceImpl implements IotProductService {
53 54
54 // 2. 插入 55 // 2. 插入
55 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) 56 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
56 - .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); 57 + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
  58 + .setProductSecret(IotDeviceAuthUtils.generateProductSecret());
57 productMapper.insert(product); 59 productMapper.insert(product);
58 return product.getId(); 60 return product.getId();
59 } 61 }
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; @@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
7 9
8 /** 10 /**
9 * IoT 设备通用 API 11 * IoT 设备通用 API
@@ -28,4 +30,12 @@ public interface IotDeviceCommonApi { @@ -28,4 +30,12 @@ public interface IotDeviceCommonApi {
28 */ 30 */
29 CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO); 31 CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
30 32
  33 + /**
  34 + * 设备动态注册(一型一密)
  35 + *
  36 + * @param reqDTO 动态注册请求
  37 + * @return 注册结果(包含 DeviceSecret)
  38 + */
  39 + CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
  40 +
31 } 41 }
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&lt;String&gt; { @@ -33,9 +33,10 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
33 TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false), 33 TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
34 34
35 // ========== 设备注册 ========== 35 // ========== 设备注册 ==========
36 - // 可参考:https://help.aliyun.com/zh/iot/user-guide/register-devices 36 + // 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
37 37
38 - SUB_DEVICE_REGISTER("thing.sub.register", "子设备动态注册", true), 38 + DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
  39 + SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
39 40
40 // ========== 设备属性 ========== 41 // ========== 设备属性 ==========
41 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 42 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * IoT 设备动态注册 Request DTO
  8 + * <p>
  9 + * 用于直连设备/网关的一型一密动态注册:使用 ProductSecret 验证签名,返回 DeviceSecret
  10 + *
  11 + * @author 芋道源码
  12 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  13 + */
  14 +@Data
  15 +public class IotDeviceRegisterReqDTO {
  16 +
  17 + /**
  18 + * 产品标识
  19 + */
  20 + @NotEmpty(message = "产品标识不能为空")
  21 + private String productKey;
  22 +
  23 + /**
  24 + * 设备名称
  25 + */
  26 + @NotEmpty(message = "设备名称不能为空")
  27 + private String deviceName;
  28 +
  29 + // TODO @AI:可以去掉 random 字段;
  30 + /**
  31 + * 随机数,用于签名
  32 + */
  33 + @NotEmpty(message = "随机数不能为空")
  34 + private String random;
  35 +
  36 + // TODO @AI:看起来,是直接带 productSecret 阿里云上,你在检查下!
  37 + /**
  38 + * 签名
  39 + */
  40 + @NotEmpty(message = "签名不能为空")
  41 + private String sign;
  42 +
  43 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * IoT 设备动态注册 Response DTO
  9 + * <p>
  10 + * 用于直连设备/网关的一型一密动态注册响应
  11 + *
  12 + * @author 芋道源码
  13 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotDeviceRegisterRespDTO {
  19 +
  20 + /**
  21 + * 产品标识
  22 + */
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 设备名称
  27 + */
  28 + private String deviceName;
  29 +
  30 + /**
  31 + * 设备密钥
  32 + */
  33 + private String deviceSecret;
  34 +
  35 +}
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; @@ -6,10 +6,12 @@ import lombok.Data;
6 /** 6 /**
7 * IoT 子设备动态注册 Request DTO 7 * IoT 子设备动态注册 Request DTO
8 * <p> 8 * <p>
9 - * 用于 thing.sub.register 消息的 params 数组元素 9 + * 用于 thing.auth.register.sub 消息的 params 数组元素
  10 + *
  11 + * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
10 * 12 *
11 * @author 芋道源码 13 * @author 芋道源码
12 - * @see <a href="http://help.aliyun.com/zh/marketplace/dynamic-registration-of-sub-devices">阿里云 - 动态注册子设备</a> 14 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
13 */ 15 */
14 @Data 16 @Data
15 public class IotSubDeviceRegisterReqDTO { 17 public class IotSubDeviceRegisterReqDTO {
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; @@ -7,10 +7,10 @@ import lombok.NoArgsConstructor;
7 /** 7 /**
8 * IoT 子设备动态注册 Response DTO 8 * IoT 子设备动态注册 Response DTO
9 * <p> 9 * <p>
10 - * 用于 thing.sub.register 响应的设备信息 10 + * 用于 thing.auth.register.sub 响应的设备信息
11 * 11 *
12 * @author 芋道源码 12 * @author 芋道源码
13 - * @see <a href="http://help.aliyun.com/zh/marketplace/dynamic-registration-of-sub-devices">阿里云 - 动态注册子设备</a> 13 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
14 */ 14 */
15 @Data 15 @Data
16 @NoArgsConstructor 16 @NoArgsConstructor
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
1 package cn.iocoder.yudao.module.iot.core.util; 1 package cn.iocoder.yudao.module.iot.core.util;
2 2
  3 +import cn.hutool.core.util.IdUtil;
3 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
4 import cn.hutool.crypto.digest.DigestUtil; 5 import cn.hutool.crypto.digest.DigestUtil;
5 import cn.hutool.crypto.digest.HmacAlgorithm; 6 import cn.hutool.crypto.digest.HmacAlgorithm;
@@ -87,4 +88,62 @@ public class IotDeviceAuthUtils { @@ -87,4 +88,62 @@ public class IotDeviceAuthUtils {
87 return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); 88 return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]);
88 } 89 }
89 90
  91 + // ========== 动态注册相关方法 ==========
  92 +
  93 + // TODO @AI:想了下,还是放回到对应的 productService、deviceService 更合适;
  94 +
  95 + /**
  96 + * 生成产品密钥
  97 + *
  98 + * @return 产品密钥(UUID)
  99 + */
  100 + public static String generateProductSecret() {
  101 + return IdUtil.fastSimpleUUID();
  102 + }
  103 +
  104 + /**
  105 + * 生成设备密钥
  106 + *
  107 + * @return 设备密钥(UUID)
  108 + */
  109 + public static String generateDeviceSecret() {
  110 + return IdUtil.fastSimpleUUID();
  111 + }
  112 +
  113 + // TODO @AI:去掉 random;
  114 + /**
  115 + * 计算动态注册签名
  116 + * <p>
  117 + * 参考阿里云规范,参数按字典序排列拼接
  118 + *
  119 + * @param productSecret 产品密钥
  120 + * @param productKey 产品标识
  121 + * @param deviceName 设备名称
  122 + * @param random 随机数
  123 + * @return 签名
  124 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">一型一密</a>
  125 + */
  126 + public static String buildRegisterSign(String productSecret, String productKey, String deviceName, String random) {
  127 + String content = "deviceName" + deviceName + "productKey" + productKey + "random" + random;
  128 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
  129 + .digestHex(content);
  130 + }
  131 +
  132 + // TODO @AI:是不是调用方自己验证就好了,不要这里面抽;
  133 + /**
  134 + * 验证动态注册签名
  135 + *
  136 + * @param productSecret 产品密钥
  137 + * @param productKey 产品标识
  138 + * @param deviceName 设备名称
  139 + * @param random 随机数
  140 + * @param sign 待验证的签名
  141 + * @return 是否验证通过
  142 + */
  143 + public static boolean verifyRegisterSign(String productSecret, String productKey,
  144 + String deviceName, String random, String sign) {
  145 + String expectedSign = buildRegisterSign(productSecret, productKey, deviceName, random);
  146 + return expectedSign.equals(sign);
  147 + }
  148 +
90 } 149 }
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; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; 5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
7 import io.vertx.core.AbstractVerticle; 9 import io.vertx.core.AbstractVerticle;
8 import io.vertx.core.Vertx; 10 import io.vertx.core.Vertx;
@@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { @@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
47 // 创建处理器,添加路由处理器 49 // 创建处理器,添加路由处理器
48 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); 50 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
49 router.post(IotHttpAuthHandler.PATH).handler(authHandler); 51 router.post(IotHttpAuthHandler.PATH).handler(authHandler);
  52 + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
  53 + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
  54 + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(this);
  55 + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
50 IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); 56 IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
51 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); 57 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
52 58
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; @@ -7,6 +7,7 @@ import cn.hutool.extra.spring.SpringUtil;
7 import cn.iocoder.yudao.framework.common.exception.ServiceException; 7 import cn.iocoder.yudao.framework.common.exception.ServiceException;
8 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  10 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 12 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
12 import io.vertx.core.Handler; 13 import io.vertx.core.Handler;
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt; @@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
54 private void beforeHandle(RoutingContext context) { 55 private void beforeHandle(RoutingContext context) {
55 // 如果不需要认证,则不走前置处理 56 // 如果不需要认证,则不走前置处理
56 String path = context.request().path(); 57 String path = context.request().path();
57 - if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { 58 + if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
58 return; 59 return;
59 } 60 }
60 61
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  9 +import io.vertx.core.json.JsonObject;
  10 +import io.vertx.ext.web.RoutingContext;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  13 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  14 +
  15 +/**
  16 + * IoT 网关 HTTP 协议的【设备动态注册】处理器
  17 + * <p>
  18 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  19 + *
  20 + * @author 芋道源码
  21 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  22 + */
  23 +public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
  24 +
  25 + public static final String PATH = "/auth/register/device";
  26 +
  27 + private final IotDeviceCommonApi deviceApi;
  28 +
  29 + public IotHttpRegisterHandler() {
  30 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  31 + }
  32 +
  33 + @Override
  34 + public CommonResult<Object> handle0(RoutingContext context) {
  35 + // 1. 解析参数
  36 + // TODO @AI:参数不太对,看看我写的建议
  37 + JsonObject body = context.body().asJsonObject();
  38 + String productKey = body.getString("productKey");
  39 + if (StrUtil.isEmpty(productKey)) {
  40 + throw invalidParamException("productKey 不能为空");
  41 + }
  42 + String deviceName = body.getString("deviceName");
  43 + if (StrUtil.isEmpty(deviceName)) {
  44 + throw invalidParamException("deviceName 不能为空");
  45 + }
  46 + String random = body.getString("random");
  47 + if (StrUtil.isEmpty(random)) {
  48 + throw invalidParamException("random 不能为空");
  49 + }
  50 + String sign = body.getString("sign");
  51 + if (StrUtil.isEmpty(sign)) {
  52 + throw invalidParamException("sign 不能为空");
  53 + }
  54 +
  55 + // 2. 调用动态注册
  56 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
  57 + .setProductKey(productKey)
  58 + .setDeviceName(deviceName)
  59 + .setRandom(random)
  60 + .setSign(sign);
  61 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
  62 + result.checkError();
  63 +
  64 + // 3. 返回结果
  65 + return success(result.getData());
  66 + }
  67 +
  68 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
  8 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  9 +import io.vertx.ext.web.RoutingContext;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  12 +
  13 +/**
  14 + * IoT 网关 HTTP 协议的【子设备动态注册】处理器
  15 + * <p>
  16 + * 用于子设备的动态注册,需要网关认证
  17 + *
  18 + * @author 芋道源码
  19 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  20 + */
  21 +public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
  22 +
  23 + /**
  24 + * 路径:/auth/register/sub-device/:productKey/:deviceName
  25 + * <p>
  26 + * productKey 和 deviceName 是网关设备的标识
  27 + */
  28 + public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
  29 +
  30 + private final IotHttpUpstreamProtocol protocol;
  31 +
  32 + private final IotDeviceMessageService deviceMessageService;
  33 +
  34 + public IotHttpRegisterSubHandler(IotHttpUpstreamProtocol protocol) {
  35 + this.protocol = protocol;
  36 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  37 + }
  38 +
  39 + @Override
  40 + public CommonResult<Object> handle0(RoutingContext context) {
  41 + // 1. 解析通用参数
  42 + String productKey = context.pathParam("productKey");
  43 + String deviceName = context.pathParam("deviceName");
  44 +
  45 + // 2.1 解析消息
  46 + byte[] bytes = context.body().buffer().getBytes();
  47 + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, productKey, deviceName);
  48 + // 2.2 设置方法
  49 + message.setMethod(IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod());
  50 +
  51 + // TODO @AI:可能还是需要一个新的 deviceApi 接口。因为 register sub 子设备不太一行;
  52 + // 2.3 发送消息
  53 + Object responseData = deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
  54 +
  55 + // 3. 返回结果
  56 + return success(responseData);
  57 + }
  58 +
  59 +}
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; @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
9 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 11 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
10 import jakarta.annotation.PostConstruct; 12 import jakarta.annotation.PostConstruct;
11 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
@@ -54,6 +56,11 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { @@ -54,6 +56,11 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
54 return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { }); 56 return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { });
55 } 57 }
56 58
  59 + @Override
  60 + public CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO) {
  61 + return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { });
  62 + }
  63 +
57 private <T, R> CommonResult<R> doPost(String url, T body, 64 private <T, R> CommonResult<R> doPost(String url, T body,
58 ParameterizedTypeReference<CommonResult<R>> responseType) { 65 ParameterizedTypeReference<CommonResult<R>> responseType) {
59 try { 66 try {
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotDeviceRegisterHttpProtocolIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.http.HttpResponse;
  6 +import cn.hutool.http.HttpUtil;
  7 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.junit.jupiter.api.Test;
  15 +
  16 +import java.util.Collections;
  17 +
  18 +// TODO @AI:合并到 IotDirectDeviceHttpProtocolIntegrationTest 里呀,没必要拆开;只搞一个直连设备的注册就好了;
  19 +/**
  20 + * IoT 设备动态注册 HTTP 协议集成测试(手动测试)
  21 + *
  22 + * <p>测试场景:一型一密(One Type One Secret)动态注册机制
  23 + *
  24 + * <p><b>前置条件:</b>
  25 + * <ol>
  26 + * <li>产品已开启动态注册(registerEnabled = true)</li>
  27 + * <li>设备已预先创建(预注册模式)</li>
  28 + * <li>设备 deviceSecret 为空(未激活状态)</li>
  29 + * </ol>
  30 + *
  31 + * <p>使用步骤:
  32 + * <ol>
  33 + * <li>启动 yudao-module-iot-gateway 服务(HTTP 端口 8092)</li>
  34 + * <li>运行 {@link #testDeviceRegister()} 测试直连设备/网关动态注册</li>
  35 + * <li>运行 {@link #testSubDeviceRegister()} 测试子设备动态注册(需要先获取网关 Token)</li>
  36 + * </ol>
  37 + *
  38 + * @author 芋道源码
  39 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  40 + */
  41 +@Slf4j
  42 +@SuppressWarnings("HttpUrlsUsage")
  43 +public class IotDeviceRegisterHttpProtocolIntegrationTest {
  44 +
  45 + private static final String SERVER_HOST = "127.0.0.1";
  46 + private static final int SERVER_PORT = 8092;
  47 +
  48 + // ===================== 直连设备/网关动态注册配置(根据实际情况修改) =====================
  49 + /**
  50 + * 产品 Key(需要开启动态注册)
  51 + */
  52 + private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
  53 + /**
  54 + * 产品密钥(从 iot_product 表的 product_secret 字段获取)
  55 + */
  56 + private static final String PRODUCT_SECRET = "your_product_secret";
  57 + /**
  58 + * 设备名称(需要预先创建,deviceSecret 为空)
  59 + */
  60 + private static final String DEVICE_NAME = "test-register-device";
  61 +
  62 + // ===================== 网关设备信息(用于子设备动态注册) =====================
  63 + private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
  64 + private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
  65 + private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
  66 +
  67 + /**
  68 + * 网关设备 Token:从网关认证获取后,粘贴到这里
  69 + */
  70 + private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
  71 +
  72 + // ===================== 子设备信息(用于子设备动态注册) =====================
  73 + private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
  74 + private static final String SUB_DEVICE_NAME = "test-sub-register-device";
  75 +
  76 + // ===================== 直连设备/网关动态注册测试 =====================
  77 +
  78 + /**
  79 + * 直连设备/网关动态注册测试
  80 + * <p>
  81 + * 使用产品密钥(productSecret)进行签名验证,成功后返回设备密钥(deviceSecret)
  82 + * <p>
  83 + * 注意:此接口不需要 Token 认证
  84 + */
  85 + @Test
  86 + public void testDeviceRegister() {
  87 + // 1.1 构建请求
  88 + String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
  89 + // 1.2 生成签名
  90 + String random = IdUtil.fastSimpleUUID();
  91 + String sign = IotDeviceAuthUtils.buildRegisterSign(PRODUCT_SECRET, PRODUCT_KEY, DEVICE_NAME, random);
  92 + // 1.3 构建请求参数
  93 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
  94 + reqDTO.setProductKey(PRODUCT_KEY);
  95 + reqDTO.setDeviceName(DEVICE_NAME);
  96 + reqDTO.setRandom(random);
  97 + reqDTO.setSign(sign);
  98 + String payload = JsonUtils.toJsonString(reqDTO);
  99 + // 1.4 输出请求
  100 + log.info("[testDeviceRegister][请求 URL: {}]", url);
  101 + log.info("[testDeviceRegister][请求体: {}]", payload);
  102 +
  103 + // 2.1 发送请求
  104 + String response = HttpUtil.post(url, payload);
  105 + // 2.2 输出结果
  106 + log.info("[testDeviceRegister][响应体: {}]", response);
  107 + log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
  108 + }
  109 +
  110 + /**
  111 + * 测试动态注册后使用 deviceSecret 进行认证
  112 + * <p>
  113 + * 此测试需要先执行 testDeviceRegister 获取 deviceSecret
  114 + */
  115 + @Test
  116 + public void testAuthAfterRegister() {
  117 + // 1.1 构建请求
  118 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
  119 + // TODO 将 testDeviceRegister 返回的 deviceSecret 填入此处
  120 + String deviceSecret = "返回的deviceSecret";
  121 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, deviceSecret);
  122 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  123 + .setClientId(authInfo.getClientId())
  124 + .setUsername(authInfo.getUsername())
  125 + .setPassword(authInfo.getPassword());
  126 + String payload = JsonUtils.toJsonString(authReqDTO);
  127 + // 1.2 输出请求
  128 + log.info("[testAuthAfterRegister][请求 URL: {}]", url);
  129 + log.info("[testAuthAfterRegister][请求体: {}]", payload);
  130 +
  131 + // 2.1 发送请求
  132 + String response = HttpUtil.post(url, payload);
  133 + // 2.2 输出结果
  134 + log.info("[testAuthAfterRegister][响应体: {}]", response);
  135 + }
  136 +
  137 + // ===================== 网关认证测试 =====================
  138 +
  139 + /**
  140 + * 网关设备认证测试:获取网关设备 Token(用于后续子设备动态注册)
  141 + */
  142 + @Test
  143 + public void testGatewayAuth() {
  144 + // 1.1 构建请求
  145 + String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
  146 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
  147 + GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
  148 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
  149 + .setClientId(authInfo.getClientId())
  150 + .setUsername(authInfo.getUsername())
  151 + .setPassword(authInfo.getPassword());
  152 + String payload = JsonUtils.toJsonString(authReqDTO);
  153 + // 1.2 输出请求
  154 + log.info("[testGatewayAuth][请求 URL: {}]", url);
  155 + log.info("[testGatewayAuth][请求体: {}]", payload);
  156 +
  157 + // 2.1 发送请求
  158 + String response = HttpUtil.post(url, payload);
  159 + // 2.2 输出结果
  160 + log.info("[testGatewayAuth][响应体: {}]", response);
  161 + log.info("[testGatewayAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
  162 + }
  163 +
  164 + // ===================== 子设备动态注册测试 =====================
  165 +
  166 + /**
  167 + * 子设备动态注册测试
  168 + * <p>
  169 + * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
  170 + * <p>
  171 + * 注意:此接口需要网关 Token 认证
  172 + */
  173 + @Test
  174 + public void testSubDeviceRegister() {
  175 + // 1.1 构建请求(需要网关认证)
  176 + String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
  177 + SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
  178 + // 1.2 构建请求参数
  179 + IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
  180 + subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
  181 + subDevice.setDeviceName(SUB_DEVICE_NAME);
  182 + String payload = JsonUtils.toJsonString(MapUtil.builder()
  183 + .put("id", IdUtil.fastSimpleUUID())
  184 + .put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
  185 + .put("version", "1.0")
  186 + .put("params", Collections.singletonList(subDevice))
  187 + .build());
  188 + // 1.3 输出请求
  189 + log.info("[testSubDeviceRegister][请求 URL: {}]", url);
  190 + log.info("[testSubDeviceRegister][请求体: {}]", payload);
  191 +
  192 + // 2.1 发送请求(需要网关 Token)
  193 + try (HttpResponse httpResponse = HttpUtil.createPost(url)
  194 + .header("Authorization", GATEWAY_TOKEN)
  195 + .body(payload)
  196 + .execute()) {
  197 + // 2.2 输出结果
  198 + log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body());
  199 + log.info("[testSubDeviceRegister][成功后可使用返回的 deviceSecret 进行子设备认证]");
  200 + }
  201 + }
  202 +
  203 +}
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 { @@ -211,11 +211,13 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
211 * 子设备动态注册测试 211 * 子设备动态注册测试
212 * <p> 212 * <p>
213 * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret 213 * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
  214 + * <p>
  215 + * 注意:此接口需要网关 Token 认证
214 */ 216 */
215 @Test 217 @Test
216 public void testSubDeviceRegister() { 218 public void testSubDeviceRegister() {
217 // 1.1 构建请求 219 // 1.1 构建请求
218 - String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/sub/register", 220 + String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
219 SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); 221 SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
220 // 1.2 构建请求参数 222 // 1.2 构建请求参数
221 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); 223 IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();