From 643cc4cfd2fa1717842966ee7b97a927ba6e8f89 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 3 Jun 2025 13:22:55 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90IoT=20=E7=89=A9=E8=81=94?= =?UTF-8?q?=E7=BD=91=E3=80=91=E5=A2=9E=E5=8A=A0=E7=BD=91=E5=85=B3=20HTTP?= =?UTF-8?q?=20=E5=8D=8F=E8=AE=AE=E7=9A=84=E9=89=B4=E6=9D=83=EF=BC=8C?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=20JWT=20=E8=BD=BB=E9=87=8F=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/date/LocalDateTimeUtils.java | 13 ++ .../iot/api/device/IotDeviceUpstreamApi.java | 30 ---- .../iot/api/device/dto/package-info.java | 4 - .../yudao/module/iot/api/package-info.java | 6 - .../module/iot/enums/ErrorCodeConstants.java | 18 --- .../iot/api/device/IoTDeviceApiImpl.java | 37 +++++ .../api/device/IoTDeviceUpstreamApiImpl.java | 30 ---- .../yudao/module/iot/api/package-info.java | 4 +- .../dal/dataobject/device/IotDeviceDO.java | 15 +- .../iot/dal/redis/RedisKeyConstants.java | 23 +-- .../config/SecurityConfiguration.java | 29 ---- .../framework/security/core/package-info.java | 4 - .../iot/service/device/IotDeviceService.java | 9 ++ .../service/device/IotDeviceServiceImpl.java | 46 ++++-- .../control/IotDeviceUpstreamService.java | 8 - .../control/IotDeviceUpstreamServiceImpl.java | 36 ----- .../yudao/module/iot/util/MqttSignUtils.java | 69 --------- .../iot/core/biz/IotDeviceCommonApi.java | 12 +- .../core/biz/dto/IotDeviceAuthReqDTO.java} | 8 +- .../iot/core/util/IotDeviceAuthUtils.java | 85 +++++++++++ .../iot/core/util/IotDeviceMessageUtils.java | 2 +- .../yudao-module-iot-gateway/pom.xml | 5 + .../config/IotGatewayConfiguration.java | 5 +- .../gateway/config/IotGatewayProperties.java | 32 +++- .../iot/gateway/enums/ErrorCodeConstants.java | 16 ++ .../http/IotHttpUpstreamProtocol.java | 9 +- .../http/router/IotHttpAbstractHandler.java | 98 ++++++++++++ .../http/router/IotHttpAuthHandler.java | 84 +++++++++++ .../http/router/IotHttpUpstreamHandler.java | 81 ++++------ .../service/auth/IotDeviceTokenService.java | 37 +++++ .../auth/IotDeviceTokenServiceImpl.java | 79 ++++++++++ .../device/IotDeviceClientServiceImpl.java | 58 ++++++++ .../src/main/resources/application.yaml | 4 + .../auth/IotDeviceTokenServiceImplTest.java | 139 ++++++++++++++++++ ...otNetComponentCommonAutoConfiguration.java | 25 ---- .../upstream/IotDeviceUpstreamClient.java | 26 ---- .../core/util/IotNetComponentCommonUtils.java | 56 ------- ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../router/IotDeviceAuthVertxHandler.java | 2 +- .../upstream/IotComponentUpstreamClient.java | 46 ------ 40 files changed, 793 insertions(+), 498 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java delete mode 100644 yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java delete mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java rename yudao-module-iot/{yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java => yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java} (62%) create mode 100644 yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceClientServiceImpl.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImplTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/config/IotNetComponentCommonAutoConfiguration.java delete mode 100644 yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java delete mode 100644 yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/util/IotNetComponentCommonUtils.java delete mode 100644 yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-server/src/main/java/cn/iocoder/yudao/module/iot/net/component/server/upstream/IotComponentUpstreamClient.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java index 6d19a3cc75..cc2d4e204d 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.date; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; @@ -312,4 +313,16 @@ public class LocalDateTimeUtils { } } + /** + * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。 + * + * @param sourceDateTime 需要转换的本地日期时间,不能为空 + * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second) + * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null} + * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常 + */ + public static Long toEpochSecond(LocalDateTime sourceDateTime) { + return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond(); + } + } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java deleted file mode 100644 index 0dde58a5be..0000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.enums.ApiConstants; -import jakarta.validation.Valid; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; - -/** - * 设备数据 Upstream 上行 API - * - * 目的:设备 -> 插件 -> 服务端 - * - * @author haohao - */ -public interface IotDeviceUpstreamApi { - - String PREFIX = ApiConstants.PREFIX + "/device/upstream"; - - // TODO @芋艿:考虑 http 认证 - /** - * 认证 Emqx 连接 - * - * @param authReqDTO 认证 Emqx 连接 DTO - */ - @PostMapping(PREFIX + "/authenticate-emqx-connection") - CommonResult authenticateEmqxConnection(@Valid @RequestBody IotDeviceEmqxAuthReqDTO authReqDTO); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java deleted file mode 100644 index cb946cd894..0000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * TODO 芋艿:占位 - */ -package cn.iocoder.yudao.module.iot.api.device.dto; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java deleted file mode 100644 index 7da0c665ba..0000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 占位 - * - * TODO 芋艿:后续删除 - */ -package cn.iocoder.yudao.module.iot.api; diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index e51c24b6ff..e12b3640e7 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -39,18 +39,6 @@ public interface ErrorCodeConstants { ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); - // ========== 插件配置 1-050-006-000 ========== - ErrorCode PLUGIN_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "插件配置不存在"); - ErrorCode PLUGIN_INSTALL_FAILED = new ErrorCode(1_050_006_001, "插件安装失败"); - ErrorCode PLUGIN_INSTALL_FAILED_FILE_NAME_NOT_MATCH = new ErrorCode(1_050_006_002, "插件安装失败,文件名与原插件id不匹配"); - ErrorCode PLUGIN_CONFIG_DELETE_FAILED_RUNNING = new ErrorCode(1_050_006_003, "请先停止插件"); - ErrorCode PLUGIN_STATUS_INVALID = new ErrorCode(1_050_006_004, "插件状态无效"); - ErrorCode PLUGIN_CONFIG_KEY_DUPLICATE = new ErrorCode(1_050_006_005, "插件标识已存在"); - ErrorCode PLUGIN_START_FAILED = new ErrorCode(1_050_006_006, "插件启动失败"); - ErrorCode PLUGIN_STOP_FAILED = new ErrorCode(1_050_006_007, "插件停止失败"); - - // ========== 插件实例 1-050-007-000 ========== - // ========== 固件相关 1-050-008-000 ========== ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); @@ -66,16 +54,10 @@ public interface ErrorCodeConstants { ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_202, "升级记录不能重试"); - // ========== MQTT 通信相关 1-050-009-000 ========== - ErrorCode MQTT_TOPIC_ILLEGAL = new ErrorCode(1_050_009_000, "topic illegal"); - // ========== IoT 数据桥梁 1-050-010-000 ========== ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); // ========== IoT 场景联动 1-050-011-000 ========== ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_011_000, "IoT 场景联动不存在"); - // ========== IoT 产品脚本信息 1-050-012-000 ========== - ErrorCode PRODUCT_SCRIPT_NOT_EXISTS = new ErrorCode(1_050_012_000, "IoT 产品脚本信息不存在"); - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java new file mode 100644 index 0000000000..d4e5b3774f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.api.device; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import org.springframework.context.annotation.Primary; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * IoT 设备 API 实现类 + * + * @author haohao + */ +@RestController +@Validated +@Primary // 保证优先匹配,因为 yudao-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入 +public class IoTDeviceApiImpl implements IotDeviceCommonApi { + + @Resource + private IotDeviceService deviceService; + + @Override + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") + @PermitAll + public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { + return success(deviceService.authDevice(authReqDTO)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java deleted file mode 100644 index 31c4b69ae1..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.iot.api.device; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; -import jakarta.annotation.Resource; -import org.springframework.context.annotation.Primary; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RestController; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -/** - * * 设备数据 Upstream 上行 API 实现类 - */ -@RestController -@Validated -@Primary // 保证优先匹配,因为 yudao-module-iot-net-component-core 也有 IotDeviceUpstreamApi 的实现,并且也可能会被 biz 引入 -public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { - - @Resource - private IotDeviceUpstreamService deviceUpstreamService; - - @Override - public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { - boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); - return success(result); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java index 07852180d4..63bca16371 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java @@ -1,6 +1,4 @@ /** - * 占位 - * - * TODO 芋艿:后续删除 + * iot API 包,定义并实现提供给其它模块的 API */ package cn.iocoder.yudao.module.iot.api; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index 3dd2dd8eb6..8317b0e7ff 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -121,23 +121,10 @@ public class IotDeviceDO extends TenantBaseDO { */ private String firmwareId; - // TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有 /** - * 设备密钥,用于设备认证,需安全存储 + * 设备密钥,用于设备认证 */ private String deviceSecret; - /** - * MQTT 客户端 ID - */ - private String mqttClientId; - /** - * MQTT 用户名 - */ - private String mqttUsername; - /** - * MQTT 密码 - */ - private String mqttPassword; /** * 认证类型(如一机一密、动态注册) */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java index f281c5878b..836a2ed1c9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java @@ -9,14 +9,15 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; */ public interface RedisKeyConstants { + // TODO @芋艿:弱化 deviceKey;使用 product_key + device_name 替代 /** * 设备属性的数据缓存,采用 HASH 结构 *

- * KEY 格式:device_property:{productKey},${deviceName} + * KEY 格式:device_property:{deviceKey} * HASH KEY:identifier 属性标识 * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} */ - String DEVICE_PROPERTY = "iot:device_property:%s,%s"; + String DEVICE_PROPERTY = "iot:device_property:%s"; /** * 设备的最后上报时间,采用 ZSET 结构 @@ -26,6 +27,15 @@ public interface RedisKeyConstants { */ String DEVICE_REPORT_TIMES = "iot:device_report_times"; + /** + * 设备关联的网关 serverId 缓存,采用 HASH 结构 + * + * KEY 格式:device_server_id + * HASH KEY:{productKey},{deviceName} + * VALUE 数据类型:String serverId + */ + String DEVICE_SERVER_ID = "iot:device_server_id"; + /** * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) * @@ -42,13 +52,4 @@ public interface RedisKeyConstants { */ String THING_MODEL_LIST = "iot:thing_model_list"; - /** - * 设备关联的网关 serverId 缓存,采用 HASH 结构 - * - * KEY 格式:device_server_id - * HASH KEY:{productKey},{deviceName} - * VALUE 数据类型:String serverId - */ - String DEVICE_SERVER_ID = "iot:device_server_id"; - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java deleted file mode 100644 index 9cf00cc104..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.yudao.module.iot.framework.security.config; - -import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; -import cn.iocoder.yudao.module.iot.enums.ApiConstants; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; - -/** - * IoT 模块的 Security 配置 - */ -@Configuration(proxyBeanMethods = false, value = "iotSecurityConfiguration") -public class SecurityConfiguration { - - @Bean("iotAuthorizeRequestsCustomizer") - public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { - return new AuthorizeRequestsCustomizer() { - - @Override - public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { - // RPC 服务的安全配置 - registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); - } - - }; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java deleted file mode 100644 index c714d10274..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 占位 - */ -package cn.iocoder.yudao.module.iot.framework.security.core; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index f722e8e033..8971820194 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import jakarta.validation.Valid; @@ -228,4 +229,12 @@ public interface IotDeviceService { */ List getDeviceListByProductKeyAndNames(String productKey, List deviceNames); + /** + * 认证设备 + * + * @param authReqDTO 认证信息 + * @return 是否认证成功 + */ + boolean authDevice(IotDeviceAuthReqDTO authReqDTO); + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java index e29ec59355..48a367675b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.device; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -12,16 +13,16 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import cn.iocoder.yudao.module.iot.service.product.IotProductService; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; @@ -397,15 +398,17 @@ public class IotDeviceServiceImpl implements IotDeviceService { return respVO; } + // TODO @芋艿:改成通用的; @Override public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { IotDeviceDO device = validateDeviceExists(deviceId); - MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), - device.getDeviceSecret()); - return new IotDeviceMqttConnectionParamsRespVO() - .setMqttClientId(mqttSignResult.getClientId()) - .setMqttUsername(mqttSignResult.getUsername()) - .setMqttPassword(mqttSignResult.getPassword()); +// MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), +// device.getDeviceSecret()); +// return new IotDeviceMqttConnectionParamsRespVO() +// .setMqttClientId(mqttSignResult.getClientId()) +// .setMqttUsername(mqttSignResult.getUsername()) +// .setMqttPassword(mqttSignResult.getPassword()); + return null; } private void deleteDeviceCache(IotDeviceDO device) { @@ -459,4 +462,29 @@ public class IotDeviceServiceImpl implements IotDeviceService { return deviceMapper.selectByProductKeyAndDeviceNames(productKey, deviceNames); } + @Override + public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) { + // 1. 校验设备是否存在 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername()); + if (deviceInfo == null) { + log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername()); + return false; + } + String deviceName = deviceInfo.getDeviceName(); + String productKey = deviceInfo.getProductKey(); + IotDeviceDO device = getSelf().getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); + if (device == null) { + log.warn("[authDevice][设备({}/{}) 不存在]", productKey, deviceName); + return false; + } + + // 2. 校验密码 + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret()); + if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) { + log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); + return false; + } + return true; + } + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java index 727a0f92ed..b82b331491 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.service.device.control; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; import jakarta.validation.Valid; @@ -48,11 +47,4 @@ public interface IotDeviceUpstreamService { // */ // void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO); - /** - * Emqx 连接认证 - * - * @param authReqDTO Emqx 连接认证 DTO - */ - boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java index f36887905c..0eb82280f7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.service.device.control; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; @@ -12,8 +11,6 @@ import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils; -import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -222,37 +219,4 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { // sendDeviceMessage(message, device); } - // TODO @芋艿:后续需要考虑,http 的认证 - @Override - public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { - log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); - // 1.1 校验设备是否存在。username 格式:${DeviceName}&${ProductKey} - String[] usernameParts = authReqDTO.getUsername().split("&"); - if (usernameParts.length != 2) { - log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); - return false; - } - String deviceName = usernameParts[0]; - String productKey = usernameParts[1]; - // 1.2 获得设备 - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); - if (device == null) { - log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", productKey, deviceName); - return false; - } - // TODO @haohao:需要记录,记录设备的最后时间 - - // 2. 校验密码 - String deviceSecret = device.getDeviceSecret(); - String clientId = authReqDTO.getClientId(); - MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); - // TODO 建议,先失败,return false; - if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { - log.info("[authenticateEmqxConnection][认证成功]"); - return true; - } - log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); - return false; - } - } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java deleted file mode 100644 index 01a6dba932..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package cn.iocoder.yudao.module.iot.util; - -import cn.hutool.crypto.digest.HMac; -import cn.hutool.crypto.digest.HmacAlgorithm; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.nio.charset.StandardCharsets; - -/** - * MQTT 签名工具类 - * - * 提供静态方法来计算 MQTT 连接参数 - */ -public class MqttSignUtils { - - /** - * 计算 MQTT 连接参数 - * - * @param productKey 产品密钥 - * @param deviceName 设备名称 - * @param deviceSecret 设备密钥 - * @return 包含 clientId, username, password 的结果对象 - */ - public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { - return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName); - } - - /** - * 计算 MQTT 连接参数 - * - * @param productKey 产品密钥 - * @param deviceName 设备名称 - * @param deviceSecret 设备密钥 - * @param clientId 客户端 ID - * @return 包含 clientId, username, password 的结果对象 - */ - public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { - String username = deviceName + "&" + productKey; - // 构建签名内容 - StringBuilder signContentBuilder = new StringBuilder() - .append("clientId").append(clientId) - .append("deviceName").append(deviceName) - .append("deviceSecret").append(deviceSecret) - .append("productKey").append(productKey); - - // 使用 HMac 计算签名 - byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8); - String signContent = signContentBuilder.toString(); - HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key); - String password = mac.digestHex(signContent); - - return new MqttSignResult(clientId, username, password); - } - - /** - * MQTT 签名结果类 - */ - @Getter - @AllArgsConstructor - public static class MqttSignResult { - - private final String clientId; - private final String username; - private final String password; - - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java index c3a57e5a0c..70f986e51e 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java @@ -1,5 +1,15 @@ package cn.iocoder.yudao.module.iot.core.biz; -// TODO @芋艿:待实现 +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; + +/** + * IoT 设备通用 API + * + * @author haohao + */ public interface IotDeviceCommonApi { + + CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO); + } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java similarity index 62% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java rename to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java index 8762aae5bc..9e62a2fc0c 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java @@ -1,17 +1,15 @@ -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; +package cn.iocoder.yudao.module.iot.core.biz.dto; import jakarta.validation.constraints.NotEmpty; import lombok.Data; -// TODO @芋艿:要不要继承 IotDeviceUpstreamAbstractReqDTO -// TODO @芋艿:@haohao:后续其它认证的设计 /** - * IoT 认证 Emqx 连接 Request DTO + * IoT 设备认证 Request DTO * * @author 芋道源码 */ @Data -public class IotDeviceEmqxAuthReqDTO { +public class IotDeviceAuthReqDTO { /** * 客户端 ID diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java new file mode 100644 index 0000000000..2bc4880070 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.core.util; + +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备【认证】的工具类,参考阿里云 + * + * @see 如何计算 MQTT 签名参数 + */ +public class IotDeviceAuthUtils { + + /** + * 认证信息 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class AuthInfo { + + /** + * 客户端 ID + */ + private String clientId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + } + + /** + * 设备信息 + */ + @Data + public static class DeviceInfo { + + private String productKey; + + private String deviceName; + + } + + public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) { + String clientId = buildClientId(productKey, deviceName); + String username = buildUsername(productKey, deviceName); + String content = "clientId" + clientId + + "deviceName" + deviceName + + "deviceSecret" + deviceSecret + + "productKey" + productKey; + String password = buildPassword(deviceSecret, content); + return new AuthInfo(clientId, username, password); + } + + private static String buildClientId(String productKey, String deviceName) { + return String.format("%s.%s", productKey, deviceName); + } + + private static String buildUsername(String productKey, String deviceName) { + return String.format("%s&%s", deviceName, productKey); + } + + private static String buildPassword(String deviceSecret, String content) { + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes()) + .digestHex(content); + } + + public static DeviceInfo parseUsername(String username) { + String[] usernameParts = username.split("&"); + if (usernameParts.length != 2) { + return null; + } + return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java index 434e66bf12..d1c5ffce3b 100644 --- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java +++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java @@ -6,7 +6,7 @@ import cn.hutool.system.SystemUtil; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; /** - * IoT 设备消息的工具类 + * IoT 设备【消息】的工具类 * * @author 芋道源码 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index 83eae1f603..2871738014 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -23,6 +23,11 @@ ${revision} + + org.springframework + spring-web + + org.apache.rocketmq diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index cd0e6ac8a8..9a2e99dea0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -24,9 +24,8 @@ public class IotGatewayConfiguration { public static class HttpProtocolConfiguration { @Bean - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties, - IotDeviceMessageProducer deviceMessageProducer) { - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), deviceMessageProducer); + public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) { + return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp()); } @Bean diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java index 9e83a36024..46170d5c04 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java @@ -1,9 +1,12 @@ package cn.iocoder.yudao.module.iot.gateway.config; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; +import java.time.Duration; import java.util.List; @ConfigurationProperties(prefix = "yudao.iot.gateway") @@ -15,6 +18,10 @@ public class IotGatewayProperties { * 设备 RPC 服务配置 */ private RpcProperties rpc; + /** + * Token 配置 + */ + private TokenProperties token; /** * 协议配置 @@ -27,15 +34,34 @@ public class IotGatewayProperties { /** * 主程序 API 地址 */ + @NotEmpty(message = "主程序 API 地址不能为空") private String url; /** * 连接超时时间 */ - private String connectTimeout; + @NotNull(message = "连接超时时间不能为空") + private Duration connectTimeout; /** * 读取超时时间 */ - private String readTimeout; + @NotNull(message = "读取超时时间不能为空") + private Duration readTimeout; + + } + + @Data + public static class TokenProperties { + + /** + * 密钥 + */ + @NotEmpty(message = "密钥不能为空") + private String secret; + /** + * 令牌有效期 + */ + @NotNull(message = "令牌有效期不能为空") + private Duration expiration; } @@ -60,6 +86,7 @@ public class IotGatewayProperties { /** * 是否开启 */ + @NotNull(message = "是否开启不能为空") private Boolean enabled; /** * 服务端口 @@ -74,6 +101,7 @@ public class IotGatewayProperties { /** * 是否开启 */ + @NotNull(message = "是否开启不能为空") private Boolean enabled; /** * MQTT 服务器地址 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java new file mode 100644 index 0000000000..bdf264fd89 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.iot.gateway.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * iot gateway 错误码枚举类 + *

+ * iot 系统,使用 1-051-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 设备认证 1-050-001-000 ============ + ErrorCode DEVICE_AUTH_FAIL = new ErrorCode(1_051_001_000, "设备鉴权失败"); // 对应阿里云 20000 + ErrorCode DEVICE_TOKEN_EXPIRED = new ErrorCode(1_051_001_002, "token 失效。需重新调用 auth 进行鉴权,获取token"); // 对应阿里云 20001 + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java index ef88f1f656..52b11217da 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; -import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; import io.vertx.core.AbstractVerticle; import io.vertx.core.Vertx; @@ -25,8 +25,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { private final IotGatewayProperties.HttpProperties httpProperties; - private final IotDeviceMessageProducer deviceMessageProducer; - private HttpServer httpServer; @Override @@ -38,10 +36,11 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { router.route().handler(BodyHandler.create()); // 创建处理器,添加路由处理器 - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler( - this, deviceMessageProducer); + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); router.post(IotHttpUpstreamHandler.PROPERTY_PATH).handler(upstreamHandler); router.post(IotHttpUpstreamHandler.EVENT_PATH).handler(upstreamHandler); + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); + router.post(IotHttpAuthHandler.PATH).handler(authHandler); // 启动 HTTP 服务器 try { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java new file mode 100644 index 0000000000..d56661ddd8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; + +/** + * IoT 网关 HTTP 协议的处理器抽象基类:提供通用的前置处理(认证)、全局的异常捕获等 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class IotHttpAbstractHandler implements Handler { + + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + + @Override + public void handle(RoutingContext context) { + try { + // 1. 前置处理 + CommonResult result = beforeHandle(context); + if (result != null) { + writeResponse(context, result); + return; + } + + // 2. 执行逻辑 + result = handle0(context); + writeResponse(context, result); + } catch (ServiceException e) { + writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); + } catch (Exception e) { + log.error("[handle][path({}) 处理异常]", context.request().path(), e); + writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); + } + } + + protected abstract CommonResult handle0(RoutingContext context); + + private CommonResult beforeHandle(RoutingContext context) { + // 如果不需要认证,则不走前置处理 + String path = context.request().path(); + if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { + return null; + } + + // 解析参数 + String token = context.request().getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isEmpty(token)) { + throw invalidParamException("token 不能为空"); + } + String productKey = context.pathParam("productKey"); + if (StrUtil.isEmpty(productKey)) { + throw invalidParamException("productKey 不能为空"); + } + String deviceName = context.pathParam("deviceName"); + if (StrUtil.isEmpty(deviceName)) { + throw invalidParamException("deviceName 不能为空"); + } + + // 校验 token + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); + Assert.notNull(deviceInfo, "设备信息不能为空"); + // 校验设备信息是否匹配 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { + throw exception(FORBIDDEN); + } + return null; + } + + @SuppressWarnings("deprecation") + public static void writeResponse(RoutingContext context, Object data) { + context.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) + .end(JsonUtils.toJsonString(data)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java new file mode 100644 index 0000000000..1e65d645ce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; + +/** + * IoT 网关 HTTP 协议的【认证】处理器 + * + * 参考 https://help.aliyun.com/zh/iot/user-guide/establish-connections-over-https + * + * @author 芋道源码 + */ +public class IotHttpAuthHandler extends IotHttpAbstractHandler { + + public static final String PATH = "/auth"; + + private final IotHttpUpstreamProtocol protocol; + + private final IotDeviceMessageProducer deviceMessageProducer; + + private final IotDeviceTokenService deviceTokenService; + + private final IotDeviceCommonApi deviceClientService; + + public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { + this.protocol = protocol; + this.deviceMessageProducer = SpringUtil.getBean(IotDeviceMessageProducer.class); + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceClientService = SpringUtil.getBean(IotDeviceCommonApi.class); + } + + @Override + public CommonResult handle0(RoutingContext context) { + // 解析参数 + JsonObject body = context.body().asJsonObject(); + String clientId = body.getString("clientId"); + if (StrUtil.isEmpty(clientId)) { + throw invalidParamException("clientId 不能为空"); + } + String username = body.getString("username"); + if (StrUtil.isEmpty(username)) { + throw invalidParamException("username 不能为空"); + } + String password = body.getString("password"); + if (StrUtil.isEmpty(password)) { + throw invalidParamException("password 不能为空"); + } + + // 执行认证 + CommonResult result = deviceClientService.authDevice(new IotDeviceAuthReqDTO() + .setClientId(clientId).setUsername(username).setPassword(password)); + if (result == null || !result.isSuccess()) { + throw exception(DEVICE_AUTH_FAIL); + } + + // 生成 Token + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username); + Assert.notNull(deviceInfo, "设备信息不能为空"); + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + Assert.notBlank(token, "生成 token 不能为空位"); + + // TODO @芋艿:发送上线消息; + + // 构建响应数据 + return success(MapUtil.of("token", token)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java index 2625bdc7c8..aff8c0d3af 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; @@ -16,11 +17,8 @@ import lombok.extern.slf4j.Slf4j; import java.util.Map; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - /** - * IoT 网关 HTTP 协议的处理器 + * IoT 网关 HTTP 协议的【上行】处理器 * * @author 芋道源码 */ @@ -54,47 +52,35 @@ public class IotHttpUpstreamHandler implements Handler { private static final String EVENT_METHOD_SUFFIX = ".post"; private final IotHttpUpstreamProtocol protocol; -// /** -// * 设备上行 API -// */ -// private final IotDeviceUpstreamApi deviceUpstreamApi; - /** - * 设备消息生产者 - */ + private final IotDeviceMessageProducer deviceMessageProducer; + public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { + this.protocol = protocol; + this.deviceMessageProducer = SpringUtil.getBean(IotDeviceMessageProducer.class); + } + @Override - public void handle(RoutingContext routingContext) { - String path = routingContext.request().path(); + public void handle(RoutingContext context) { + String path = context.request().path(); + // 1. 解析通用参数 + Map params = parseCommonParams(context); + String productKey = params.get("productKey"); + String deviceName = params.get("deviceName"); + JsonObject body = context.body().asJsonObject(); - try { - // 1. 解析通用参数 - Map params = parseCommonParams(routingContext); - String productKey = params.get("productKey"); - String deviceName = params.get("deviceName"); - JsonObject body = routingContext.body().asJsonObject(); + // 2. 根据路径模式处理不同类型的请求 + if (isPropertyPostPath(path)) { + // 处理属性上报 + handlePropertyPost(context, productKey, deviceName, body); + return; + } - // 2. 根据路径模式处理不同类型的请求 - if (isPropertyPostPath(path)) { - // 处理属性上报 - handlePropertyPost(routingContext, productKey, deviceName, body); - return; - } - - if (isEventPostPath(path)) { - // 处理事件上报 - String identifier = routingContext.pathParam("identifier"); - handleEventPost(routingContext, productKey, deviceName, identifier, body); - return; - } - - // 不支持的请求路径 - sendErrorResponse(routingContext, "unknown", BAD_REQUEST.getCode(), "不支持的请求路径"); - } catch (Exception e) { - log.error("[handle][处理上行请求异常] path={}", path, e); - String method = determineMethodFromPath(path, routingContext); - sendErrorResponse(routingContext, method, INTERNAL_SERVER_ERROR.getCode(), - INTERNAL_SERVER_ERROR.getMsg()); + if (isEventPostPath(path)) { + // 处理事件上报 + String identifier = context.pathParam("identifier"); + handleEventPost(context, productKey, deviceName, identifier, body); + return; } } @@ -169,7 +155,6 @@ public class IotHttpUpstreamHandler implements Handler { // // // 事件上报 // CommonResult result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); -// String method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX; // // // 返回响应 // sendResponse(routingContext, requestId, method, result); @@ -195,20 +180,6 @@ public class IotHttpUpstreamHandler implements Handler { // IotNetComponentCommonUtils.writeJsonResponse(routingContext, response); } - /** - * 发送错误响应 - * - * @param routingContext 路由上下文 - * @param method 方法名 - * @param code 错误代码 - * @param message 错误消息 - */ - private void sendErrorResponse(RoutingContext routingContext, String method, Integer code, - String message) { -// IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message); -// IotNetComponentCommonUtils.writeJsonResponse(routingContext, errorResponse); - } - /** * 从路径确定方法名 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java new file mode 100644 index 0000000000..b44c23e8b4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.gateway.service.auth; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; + +/** + * IoT 设备 Token 服务 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceTokenService { + + /** + * 创建设备 Token + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 设备 Token + */ + String createToken(String productKey, String deviceName); + + /** + * 验证设备 Token + * + * @param token 设备 Token + * @return 设备信息 + */ + IotDeviceAuthUtils.DeviceInfo verifyToken(String token); + + /** + * 解析用户名 + * + * @param username 用户名 + * @return 设备信息 + */ + IotDeviceAuthUtils.DeviceInfo parseUsername(String username); + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java new file mode 100644 index 0000000000..e6fe2fb816 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.gateway.service.auth; + +import cn.hutool.core.lang.Assert; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTUtil; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_TOKEN_EXPIRED; + +/** + * IoT 设备 Token Service 实现类:调用远程的 device http 接口,进行设备 Token 生成、解析等逻辑 + * + * 注意:目前仅 HTTP 协议使用 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { + + @Resource + private IotGatewayProperties gatewayProperties; + + @Override + public String createToken(String productKey, String deviceName) { + Assert.notBlank(productKey, "productKey 不能为空"); + Assert.notBlank(deviceName, "deviceName 不能为空"); + // 构建 JWT payload + Map payload = new HashMap<>(); + payload.put("productKey", productKey); + payload.put("deviceName", deviceName); + LocalDateTime expireTime = LocalDateTimeUtils.addTime(gatewayProperties.getToken().getExpiration()); + payload.put("exp", LocalDateTimeUtils.toEpochSecond(expireTime)); // 过期时间(exp 是 JWT 规范推荐) + + // 生成 JWT Token + return JWTUtil.createToken(payload, gatewayProperties.getToken().getSecret().getBytes()); + } + + @Override + public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) { + Assert.notBlank(token, "token 不能为空"); + // 校验 JWT Token + boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes()); + if (!verify) { + throw exception(DEVICE_TOKEN_EXPIRED); + } + + // 解析 Token + JWT jwt = JWTUtil.parseToken(token); + JSONObject payload = jwt.getPayloads(); + // 检查过期时间 + Long exp = payload.getLong("exp"); + if (exp == null || exp > System.currentTimeMillis() / 1000) { + throw exception(DEVICE_TOKEN_EXPIRED); + } + String productKey = payload.getStr("productKey"); + String deviceName = payload.getStr("deviceName"); + Assert.notBlank(productKey, "productKey 不能为空"); + Assert.notBlank(deviceName, "deviceName 不能为空"); + return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName); + } + + @Override + public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) { + return IotDeviceAuthUtils.parseUsername(username); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceClientServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceClientServiceImpl.java new file mode 100644 index 0000000000..f61bf3df90 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceClientServiceImpl.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.gateway.service.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDeviceClientServiceImpl implements IotDeviceCommonApi { + + @Resource + private IotGatewayProperties gatewayProperties; + + private RestTemplate restTemplate; + + @PostConstruct + public void init() { + IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc(); + restTemplate = new RestTemplateBuilder() + .rootUri(rpc.getUrl() + "/rpc-api/iot/device/") + .readTimeout(rpc.getReadTimeout()) + .connectTimeout(rpc.getConnectTimeout()) + .build(); + } + + @Override + public CommonResult authDevice(IotDeviceAuthReqDTO authReqDTO) { + return doPost("auth", authReqDTO); + } + + @SuppressWarnings("unchecked") + private CommonResult doPost(String url, T requestBody) { + try { + CommonResult result = restTemplate.postForObject(url, requestBody, + (Class>) (Class) CommonResult.class); + log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); + return result; + } catch (Exception e) { + log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); + return CommonResult.error(INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 9cc438720e..0f52fda62d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -22,6 +22,10 @@ yudao: url: http://127.0.0.1:48080 # 主程序 API 地址 connect-timeout: 30s read-timeout: 30s + # 设备 Token 配置 + token: + secret: 1234567890123456789012345678901 + expiration: 7d # 协议配置 protocol: diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImplTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImplTest.java new file mode 100644 index 0000000000..a898c7ca42 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImplTest.java @@ -0,0 +1,139 @@ +package cn.iocoder.yudao.module.iot.gateway.service.auth; + +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * {@link IotDeviceTokenServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@ExtendWith(MockitoExtension.class) +class IotDeviceTokenServiceImplTest { + + @Mock + private IotGatewayProperties gatewayProperties; + + @InjectMocks + private IotDeviceTokenServiceImpl tokenService; + + private IotGatewayProperties.TokenProperties tokenProperties; + + @BeforeEach + void setUp() { + // 初始化 Token 配置 + tokenProperties = new IotGatewayProperties.TokenProperties(); + tokenProperties.setSecret("1234567890123456789012345678901"); + tokenProperties.setExpiration(Duration.ofDays(7)); + + when(gatewayProperties.getToken()).thenReturn(tokenProperties); + } + + @Test + void testCreateToken_Success() { + // 准备参数 + String productKey = "testProduct"; + String deviceName = "testDevice"; + + // 调用方法 + String token = tokenService.createToken(productKey, deviceName); + + // 验证结果 + assertNotNull(token); + assertFalse(token.isEmpty()); + } + + @Test + void testCreateToken_WithBlankParameters() { + // 测试空白参数 + assertNull(tokenService.createToken("", "deviceName")); + assertNull(tokenService.createToken("productKey", "")); + assertNull(tokenService.createToken(null, "deviceName")); + assertNull(tokenService.createToken("productKey", null)); + } + + @Test + void testCreateToken_WithoutConfig() { + // 模拟配置为空 + when(gatewayProperties.getToken()).thenReturn(null); + + // 调用方法 + String token = tokenService.createToken("productKey", "deviceName"); + + // 验证结果 + assertNull(token); + } + + @Test + void testVerifyToken_Success() { + // 准备参数 + String productKey = "testProduct"; + String deviceName = "testDevice"; + + // 创建 Token + String token = tokenService.createToken(productKey, deviceName); + assertNotNull(token); + + // 验证 Token + IotDeviceAuthUtils.DeviceInfo deviceInfo = tokenService.verifyToken(token); + + // 验证结果 + assertNotNull(deviceInfo); + assertEquals(productKey, deviceInfo.getProductKey()); + assertEquals(deviceName, deviceInfo.getDeviceName()); + } + + @Test + void testVerifyToken_WithBlankToken() { + // 测试空白 Token + assertNull(tokenService.verifyToken("")); + assertNull(tokenService.verifyToken(null)); + } + + @Test + void testVerifyToken_WithInvalidToken() { + // 测试无效 Token + assertNull(tokenService.verifyToken("invalid.token.here")); + } + + @Test + void testVerifyToken_WithoutConfig() { + // 模拟配置为空 + when(gatewayProperties.getToken()).thenReturn(null); + + // 调用方法 + IotDeviceAuthUtils.DeviceInfo deviceInfo = tokenService.verifyToken("any.token.here"); + + // 验证结果 + assertNull(deviceInfo); + } + + @Test + void testTokenRoundTrip() { + // 测试完整的 Token 创建和验证流程 + String productKey = "myProduct"; + String deviceName = "myDevice"; + + // 1. 创建 Token + String token = tokenService.createToken(productKey, deviceName); + assertNotNull(token); + + // 2. 验证 Token + IotDeviceAuthUtils.DeviceInfo deviceInfo = tokenService.verifyToken(token); + assertNotNull(deviceInfo); + assertEquals(productKey, deviceInfo.getProductKey()); + assertEquals(deviceName, deviceInfo.getDeviceName()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/config/IotNetComponentCommonAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/config/IotNetComponentCommonAutoConfiguration.java deleted file mode 100644 index 7c28ee65fc..0000000000 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/config/IotNetComponentCommonAutoConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.module.iot.net.component.core.config; - -import cn.iocoder.yudao.module.iot.net.component.core.upstream.IotDeviceUpstreamClient; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.annotation.EnableScheduling; - -/** - * IoT 网络组件的通用自动配置类 - * - * @author haohao - */ -@EnableConfigurationProperties(IotNetComponentCommonProperties.class) -public class IotNetComponentCommonAutoConfiguration { - - /** - * 创建设备上行客户端 - */ - @Bean - public IotDeviceUpstreamClient deviceUpstreamClient() { - return new IotDeviceUpstreamClient(); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java deleted file mode 100644 index 211e074993..0000000000 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/upstream/IotDeviceUpstreamClient.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.iot.net.component.core.upstream; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -/** - * 设备数据 Upstream 上行客户端 - *

- * 直接调用 IotDeviceUpstreamApi 接口 - * - * @author haohao - */ -@Slf4j -public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { - - @Resource - private IotDeviceUpstreamApi deviceUpstreamApi; - - @Override - public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { - return deviceUpstreamApi.authenticateEmqxConnection(authReqDTO); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/util/IotNetComponentCommonUtils.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/util/IotNetComponentCommonUtils.java deleted file mode 100644 index 5598c29d6e..0000000000 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/java/cn/iocoder/yudao/module/iot/net/component/core/util/IotNetComponentCommonUtils.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.iocoder.yudao.module.iot.net.component.core.util; - -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.module.iot.net.component.core.pojo.IotStandardResponse; -import io.vertx.core.http.HttpHeaders; -import io.vertx.ext.web.RoutingContext; -import org.springframework.http.MediaType; - -/** - * IoT 网络组件的通用工具类 - * - * @author 芋道源码 - */ -public class IotNetComponentCommonUtils { - - /** - * 将对象转换为JSON字符串后写入HTTP响应 - * - * @param routingContext 路由上下文 - * @param data 数据对象 - */ - @SuppressWarnings("deprecation") - public static void writeJsonResponse(RoutingContext routingContext, Object data) { - routingContext.response() - .setStatusCode(200) - .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(data)); - } - - /** - * 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse) - *

- * 推荐使用此方法,统一 MQTT 和 HTTP 的响应格式。使用方式: - * - *

-     * // 成功响应
-     * IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
-     * IotNetComponentCommonUtils.writeJsonResponse(routingContext, response);
-     *
-     * // 错误响应
-     * IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
-     * IotNetComponentCommonUtils.writeJsonResponse(routingContext, errorResponse);
-     * 
- * - * @param routingContext 路由上下文 - * @param response IotStandardResponse 响应对象 - */ - @SuppressWarnings("deprecation") - public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) { - routingContext.response() - .setStatusCode(200) - .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) - .end(JsonUtils.toJsonString(response)); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 57f1b43109..0000000000 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonAutoConfiguration \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/net/component/emqx/upstream/router/IotDeviceAuthVertxHandler.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/net/component/emqx/upstream/router/IotDeviceAuthVertxHandler.java index 7ca1592e81..72d4b4c161 100644 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/net/component/emqx/upstream/router/IotDeviceAuthVertxHandler.java +++ b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/net/component/emqx/upstream/router/IotDeviceAuthVertxHandler.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceEmqxAuthReqDTO; import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils; import io.vertx.core.Handler; import io.vertx.core.json.JsonObject; diff --git a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-server/src/main/java/cn/iocoder/yudao/module/iot/net/component/server/upstream/IotComponentUpstreamClient.java b/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-server/src/main/java/cn/iocoder/yudao/module/iot/net/component/server/upstream/IotComponentUpstreamClient.java deleted file mode 100644 index 7959c5b670..0000000000 --- a/yudao-module-iot/yudao-module-iot-net-components/yudao-module-iot-net-component-server/src/main/java/cn/iocoder/yudao/module/iot/net/component/server/upstream/IotComponentUpstreamClient.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.iocoder.yudao.module.iot.net.component.server.upstream; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.iot.net.component.server.config.IotNetComponentServerProperties; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.client.RestTemplate; - -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; - -/** - * 组件上行客户端,用于向主程序上报设备数据 - *

- * 通过 HTTP 调用远程的 IotDeviceUpstreamApi 接口 - * - * @author haohao - */ -@RequiredArgsConstructor -@Slf4j -public class IotComponentUpstreamClient { - - public static final String URL_PREFIX = "/rpc-api/iot/device/upstream"; - - private final IotNetComponentServerProperties properties; - - private final RestTemplate restTemplate; - -// @Override -// public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { -// String url = properties.getUpstreamUrl() + URL_PREFIX + "/update-state"; -// return doPost(url, updateReqDTO); -// } - - @SuppressWarnings("unchecked") - private CommonResult doPost(String url, T requestBody) { - try { - CommonResult result = restTemplate.postForObject(url, requestBody, - (Class>) (Class) CommonResult.class); - log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); - return result; - } catch (Exception e) { - log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); - return CommonResult.error(INTERNAL_SERVER_ERROR); - } - } -} \ No newline at end of file