Commit 38a21ad59c513c957079c97df99cc596859fd61c
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 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 9 | 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 | 12 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 11 | 13 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 12 | 14 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| ... | ... | @@ -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 | 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 | 27 | @ExcelProperty("产品标识") |
| 28 | 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 | 36 | @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| 31 | 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 | 48 | @NotEmpty(message = "数据格式不能为空") |
| 49 | 49 | private String codecType; |
| 50 | 50 | |
| 51 | + @Schema(description = "是否开启动态注册", example = "false") | |
| 52 | + @NotEmpty(message = "是否开启动态注册不能为空") | |
| 53 | + private Boolean registerEnabled; | |
| 54 | + | |
| 51 | 55 | } |
| 52 | 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 | 33 | */ |
| 34 | 34 | private String productKey; |
| 35 | 35 | /** |
| 36 | + * 产品密钥,用于一型一密动态注册 | |
| 37 | + */ | |
| 38 | + private String productSecret; | |
| 39 | + /** | |
| 40 | + * 是否开启动态注册 | |
| 41 | + */ | |
| 42 | + private Boolean registerEnabled; | |
| 43 | + /** | |
| 36 | 44 | * 产品分类编号 |
| 37 | 45 | * <p> |
| 38 | 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 | 40 | ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效"); |
| 41 | 41 | ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败"); |
| 42 | 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 | 44 | ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效"); |
| 45 | 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 | 50 | // ========== 产品分类 1-050-004-000 ========== |
| 48 | 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 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | 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 | 11 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO; |
| 11 | 13 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| ... | ... | @@ -292,7 +294,7 @@ public interface IotDeviceService { |
| 292 | 294 | */ |
| 293 | 295 | List<IotDeviceDO> getDeviceListByHasLocation(); |
| 294 | 296 | |
| 295 | - // ========== 网关-子设备绑定相关 ========== | |
| 297 | + // ========== 网关-拓扑管理(后台操作) ========== | |
| 296 | 298 | |
| 297 | 299 | /** |
| 298 | 300 | * 绑定子设备到网关 |
| ... | ... | @@ -354,6 +356,8 @@ public interface IotDeviceService { |
| 354 | 356 | */ |
| 355 | 357 | IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice); |
| 356 | 358 | |
| 359 | + // ========== 设备动态注册 ========== | |
| 360 | + | |
| 357 | 361 | /** |
| 358 | 362 | * 处理子设备动态注册消息(网关设备上报) |
| 359 | 363 | * |
| ... | ... | @@ -363,4 +367,12 @@ public interface IotDeviceService { |
| 363 | 367 | */ |
| 364 | 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 | 1 | package cn.iocoder.yudao.module.iot.service.device; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | -import cn.hutool.core.util.IdUtil; | |
| 4 | +import cn.hutool.core.util.BooleanUtil; | |
| 5 | 5 | import cn.hutool.core.util.ObjUtil; |
| 6 | 6 | import cn.hutool.core.util.StrUtil; |
| 7 | 7 | import cn.hutool.extra.spring.SpringUtil; |
| ... | ... | @@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 18 | 18 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 19 | 19 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 20 | 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 | 23 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; |
| 22 | 24 | import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; |
| 23 | 25 | import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; |
| ... | ... | @@ -77,6 +79,10 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 77 | 79 | @Lazy // 延迟加载,解决循环依赖 |
| 78 | 80 | private IotDeviceMessageService deviceMessageService; |
| 79 | 81 | |
| 82 | + private IotDeviceServiceImpl getSelf() { | |
| 83 | + return SpringUtil.getBean(getClass()); | |
| 84 | + } | |
| 85 | + | |
| 80 | 86 | @Override |
| 81 | 87 | public Long createDevice(IotDeviceSaveReqVO createReqVO) { |
| 82 | 88 | return createDevice0(createReqVO).getId(); |
| ... | ... | @@ -138,7 +144,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 138 | 144 | device.setProductId(product.getId()).setProductKey(product.getProductKey()) |
| 139 | 145 | .setDeviceType(product.getDeviceType()); |
| 140 | 146 | // 生成密钥 |
| 141 | - device.setDeviceSecret(generateDeviceSecret()); | |
| 147 | + device.setDeviceSecret(IotDeviceAuthUtils.generateDeviceSecret()); | |
| 142 | 148 | // 设置设备状态为未激活 |
| 143 | 149 | device.setState(IotDeviceStateEnum.INACTIVE.getState()); |
| 144 | 150 | } |
| ... | ... | @@ -368,15 +374,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 368 | 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 | 377 | @Override |
| 381 | 378 | @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 |
| 382 | 379 | public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) { |
| ... | ... | @@ -566,7 +563,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 566 | 563 | return deviceMapper.selectListByHasLocation(); |
| 567 | 564 | } |
| 568 | 565 | |
| 569 | - // ========== 网关-子设备绑定相关 ========== | |
| 566 | + // ========== 网关-拓扑管理(后台操作) ========== | |
| 570 | 567 | |
| 571 | 568 | @Override |
| 572 | 569 | @Transactional(rollbackFor = Exception.class) |
| ... | ... | @@ -766,6 +763,79 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 766 | 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 | 839 | @Override |
| 770 | 840 | public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) { |
| 771 | 841 | // 1.1 校验网关设备类型 |
| ... | ... | @@ -799,6 +869,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 799 | 869 | return results; |
| 800 | 870 | } |
| 801 | 871 | |
| 872 | + // TODO @AI:阿里云的,设备必须存在; | |
| 802 | 873 | private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) { |
| 803 | 874 | // 1.1 校验产品 |
| 804 | 875 | IotProductDO product = productService.getProductByProductKey(params.getProductKey()); |
| ... | ... | @@ -833,46 +904,4 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 833 | 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 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 6 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | |
| 6 | 7 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; |
| 8 | 9 | import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; |
| ... | ... | @@ -53,7 +54,8 @@ public class IotProductServiceImpl implements IotProductService { |
| 53 | 54 | |
| 54 | 55 | // 2. 插入 |
| 55 | 56 | IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) |
| 56 | - .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); | |
| 57 | + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()) | |
| 58 | + .setProductSecret(IotDeviceAuthUtils.generateProductSecret()); | |
| 57 | 59 | productMapper.insert(product); |
| 58 | 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 | 4 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 6 | 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 | 11 | * IoT 设备通用 API |
| ... | ... | @@ -28,4 +30,12 @@ public interface IotDeviceCommonApi { |
| 28 | 30 | */ |
| 29 | 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<String> { |
| 33 | 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 | 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 | 6 | /** |
| 7 | 7 | * IoT 子设备动态注册 Request DTO |
| 8 | 8 | * <p> |
| 9 | - * 用于 thing.sub.register 消息的 params 数组元素 | |
| 9 | + * 用于 thing.auth.register.sub 消息的 params 数组元素 | |
| 10 | + * | |
| 11 | + * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。 | |
| 10 | 12 | * |
| 11 | 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 | 16 | @Data |
| 15 | 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 | 7 | /** |
| 8 | 8 | * IoT 子设备动态注册 Response DTO |
| 9 | 9 | * <p> |
| 10 | - * 用于 thing.sub.register 响应的设备信息 | |
| 10 | + * 用于 thing.auth.register.sub 响应的设备信息 | |
| 11 | 11 | * |
| 12 | 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 | 15 | @Data |
| 16 | 16 | @NoArgsConstructor | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.core.util; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.IdUtil; | |
| 3 | 4 | import cn.hutool.core.util.StrUtil; |
| 4 | 5 | import cn.hutool.crypto.digest.DigestUtil; |
| 5 | 6 | import cn.hutool.crypto.digest.HmacAlgorithm; |
| ... | ... | @@ -87,4 +88,62 @@ public class IotDeviceAuthUtils { |
| 87 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 5 | 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 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; |
| 7 | 9 | import io.vertx.core.AbstractVerticle; |
| 8 | 10 | import io.vertx.core.Vertx; |
| ... | ... | @@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { |
| 47 | 49 | // 创建处理器,添加路由处理器 |
| 48 | 50 | IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); |
| 49 | 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 | 56 | IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); |
| 51 | 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 | 7 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 8 | 8 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 9 | 9 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 10 | +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | |
| 10 | 11 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; |
| 12 | 13 | import io.vertx.core.Handler; |
| ... | ... | @@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> |
| 54 | 55 | private void beforeHandle(RoutingContext context) { |
| 55 | 56 | // 如果不需要认证,则不走前置处理 |
| 56 | 57 | String path = context.request().path(); |
| 57 | - if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { | |
| 58 | + if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) { | |
| 58 | 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 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; |
| 8 | 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 | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 10 | 12 | import jakarta.annotation.PostConstruct; |
| 11 | 13 | import jakarta.annotation.Resource; |
| ... | ... | @@ -54,6 +56,11 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi { |
| 54 | 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 | 64 | private <T, R> CommonResult<R> doPost(String url, T body, |
| 58 | 65 | ParameterizedTypeReference<CommonResult<R>> responseType) { |
| 59 | 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 | 211 | * 子设备动态注册测试 |
| 212 | 212 | * <p> |
| 213 | 213 | * 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret |
| 214 | + * <p> | |
| 215 | + * 注意:此接口需要网关 Token 认证 | |
| 214 | 216 | */ |
| 215 | 217 | @Test |
| 216 | 218 | public void testSubDeviceRegister() { |
| 217 | 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 | 221 | SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME); |
| 220 | 222 | // 1.2 构建请求参数 |
| 221 | 223 | IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO(); | ... | ... |