Commit 643cc4cfd2fa1717842966ee7b97a927ba6e8f89
1 parent
1498389d
feat:【IoT 物联网】增加网关 HTTP 协议的鉴权,基于 JWT 轻量级
Showing
39 changed files
with
772 additions
and
477 deletions
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; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.date; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | import cn.hutool.core.date.DatePattern; | 4 | import cn.hutool.core.date.DatePattern; |
| 5 | import cn.hutool.core.date.LocalDateTimeUtil; | 5 | import cn.hutool.core.date.LocalDateTimeUtil; |
| 6 | +import cn.hutool.core.date.TemporalAccessorUtil; | ||
| 6 | import cn.hutool.core.lang.Assert; | 7 | import cn.hutool.core.lang.Assert; |
| 7 | import cn.hutool.core.util.StrUtil; | 8 | import cn.hutool.core.util.StrUtil; |
| 8 | import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; | 9 | import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; |
| @@ -312,4 +313,16 @@ public class LocalDateTimeUtils { | @@ -312,4 +313,16 @@ public class LocalDateTimeUtils { | ||
| 312 | } | 313 | } |
| 313 | } | 314 | } |
| 314 | 315 | ||
| 316 | + /** | ||
| 317 | + * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。 | ||
| 318 | + * | ||
| 319 | + * @param sourceDateTime 需要转换的本地日期时间,不能为空 | ||
| 320 | + * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second) | ||
| 321 | + * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null} | ||
| 322 | + * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常 | ||
| 323 | + */ | ||
| 324 | + public static Long toEpochSecond(LocalDateTime sourceDateTime) { | ||
| 325 | + return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond(); | ||
| 326 | + } | ||
| 327 | + | ||
| 315 | } | 328 | } |
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.api.device; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; | ||
| 5 | -import cn.iocoder.yudao.module.iot.enums.ApiConstants; | ||
| 6 | -import jakarta.validation.Valid; | ||
| 7 | -import org.springframework.web.bind.annotation.PostMapping; | ||
| 8 | -import org.springframework.web.bind.annotation.RequestBody; | ||
| 9 | - | ||
| 10 | -/** | ||
| 11 | - * 设备数据 Upstream 上行 API | ||
| 12 | - * | ||
| 13 | - * 目的:设备 -> 插件 -> 服务端 | ||
| 14 | - * | ||
| 15 | - * @author haohao | ||
| 16 | - */ | ||
| 17 | -public interface IotDeviceUpstreamApi { | ||
| 18 | - | ||
| 19 | - String PREFIX = ApiConstants.PREFIX + "/device/upstream"; | ||
| 20 | - | ||
| 21 | - // TODO @芋艿:考虑 http 认证 | ||
| 22 | - /** | ||
| 23 | - * 认证 Emqx 连接 | ||
| 24 | - * | ||
| 25 | - * @param authReqDTO 认证 Emqx 连接 DTO | ||
| 26 | - */ | ||
| 27 | - @PostMapping(PREFIX + "/authenticate-emqx-connection") | ||
| 28 | - CommonResult<Boolean> authenticateEmqxConnection(@Valid @RequestBody IotDeviceEmqxAuthReqDTO authReqDTO); | ||
| 29 | - | ||
| 30 | -} | ||
| 31 | \ No newline at end of file | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java deleted
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java deleted
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 { | @@ -39,18 +39,6 @@ public interface ErrorCodeConstants { | ||
| 39 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); | 39 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); |
| 40 | ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); | 40 | ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); |
| 41 | 41 | ||
| 42 | - // ========== 插件配置 1-050-006-000 ========== | ||
| 43 | - ErrorCode PLUGIN_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "插件配置不存在"); | ||
| 44 | - ErrorCode PLUGIN_INSTALL_FAILED = new ErrorCode(1_050_006_001, "插件安装失败"); | ||
| 45 | - ErrorCode PLUGIN_INSTALL_FAILED_FILE_NAME_NOT_MATCH = new ErrorCode(1_050_006_002, "插件安装失败,文件名与原插件id不匹配"); | ||
| 46 | - ErrorCode PLUGIN_CONFIG_DELETE_FAILED_RUNNING = new ErrorCode(1_050_006_003, "请先停止插件"); | ||
| 47 | - ErrorCode PLUGIN_STATUS_INVALID = new ErrorCode(1_050_006_004, "插件状态无效"); | ||
| 48 | - ErrorCode PLUGIN_CONFIG_KEY_DUPLICATE = new ErrorCode(1_050_006_005, "插件标识已存在"); | ||
| 49 | - ErrorCode PLUGIN_START_FAILED = new ErrorCode(1_050_006_006, "插件启动失败"); | ||
| 50 | - ErrorCode PLUGIN_STOP_FAILED = new ErrorCode(1_050_006_007, "插件停止失败"); | ||
| 51 | - | ||
| 52 | - // ========== 插件实例 1-050-007-000 ========== | ||
| 53 | - | ||
| 54 | // ========== 固件相关 1-050-008-000 ========== | 42 | // ========== 固件相关 1-050-008-000 ========== |
| 55 | 43 | ||
| 56 | ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); | 44 | ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); |
| @@ -66,16 +54,10 @@ public interface ErrorCodeConstants { | @@ -66,16 +54,10 @@ public interface ErrorCodeConstants { | ||
| 66 | ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); | 54 | ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); |
| 67 | ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_202, "升级记录不能重试"); | 55 | ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_202, "升级记录不能重试"); |
| 68 | 56 | ||
| 69 | - // ========== MQTT 通信相关 1-050-009-000 ========== | ||
| 70 | - ErrorCode MQTT_TOPIC_ILLEGAL = new ErrorCode(1_050_009_000, "topic illegal"); | ||
| 71 | - | ||
| 72 | // ========== IoT 数据桥梁 1-050-010-000 ========== | 57 | // ========== IoT 数据桥梁 1-050-010-000 ========== |
| 73 | ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); | 58 | ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); |
| 74 | 59 | ||
| 75 | // ========== IoT 场景联动 1-050-011-000 ========== | 60 | // ========== IoT 场景联动 1-050-011-000 ========== |
| 76 | ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_011_000, "IoT 场景联动不存在"); | 61 | ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_011_000, "IoT 场景联动不存在"); |
| 77 | 62 | ||
| 78 | - // ========== IoT 产品脚本信息 1-050-012-000 ========== | ||
| 79 | - ErrorCode PRODUCT_SCRIPT_NOT_EXISTS = new ErrorCode(1_050_012_000, "IoT 产品脚本信息不存在"); | ||
| 80 | - | ||
| 81 | } | 63 | } |
| 82 | \ No newline at end of file | 64 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| 1 | package cn.iocoder.yudao.module.iot.api.device; | 1 | package cn.iocoder.yudao.module.iot.api.device; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.RpcConstants; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 4 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; | ||
| 5 | -import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; | 5 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 7 | +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | ||
| 6 | import jakarta.annotation.Resource; | 8 | import jakarta.annotation.Resource; |
| 9 | +import jakarta.annotation.security.PermitAll; | ||
| 7 | import org.springframework.context.annotation.Primary; | 10 | import org.springframework.context.annotation.Primary; |
| 8 | import org.springframework.validation.annotation.Validated; | 11 | import org.springframework.validation.annotation.Validated; |
| 12 | +import org.springframework.web.bind.annotation.PostMapping; | ||
| 9 | import org.springframework.web.bind.annotation.RestController; | 13 | import org.springframework.web.bind.annotation.RestController; |
| 10 | 14 | ||
| 11 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 15 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 12 | 16 | ||
| 13 | /** | 17 | /** |
| 14 | - * * 设备数据 Upstream 上行 API 实现类 | 18 | + * IoT 设备 API 实现类 |
| 19 | + * | ||
| 20 | + * @author haohao | ||
| 15 | */ | 21 | */ |
| 16 | @RestController | 22 | @RestController |
| 17 | @Validated | 23 | @Validated |
| 18 | -@Primary // 保证优先匹配,因为 yudao-module-iot-net-component-core 也有 IotDeviceUpstreamApi 的实现,并且也可能会被 biz 引入 | ||
| 19 | -public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { | 24 | +@Primary // 保证优先匹配,因为 yudao-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入 |
| 25 | +public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 20 | 26 | ||
| 21 | @Resource | 27 | @Resource |
| 22 | - private IotDeviceUpstreamService deviceUpstreamService; | 28 | + private IotDeviceService deviceService; |
| 23 | 29 | ||
| 24 | @Override | 30 | @Override |
| 25 | - public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { | ||
| 26 | - boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); | ||
| 27 | - return success(result); | 31 | + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth") |
| 32 | + @PermitAll | ||
| 33 | + public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) { | ||
| 34 | + return success(deviceService.authDevice(authReqDTO)); | ||
| 28 | } | 35 | } |
| 29 | 36 | ||
| 30 | } | 37 | } |
| 31 | \ No newline at end of file | 38 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
| @@ -121,24 +121,11 @@ public class IotDeviceDO extends TenantBaseDO { | @@ -121,24 +121,11 @@ public class IotDeviceDO extends TenantBaseDO { | ||
| 121 | */ | 121 | */ |
| 122 | private String firmwareId; | 122 | private String firmwareId; |
| 123 | 123 | ||
| 124 | - // TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有 | ||
| 125 | /** | 124 | /** |
| 126 | - * 设备密钥,用于设备认证,需安全存储 | 125 | + * 设备密钥,用于设备认证 |
| 127 | */ | 126 | */ |
| 128 | private String deviceSecret; | 127 | private String deviceSecret; |
| 129 | /** | 128 | /** |
| 130 | - * MQTT 客户端 ID | ||
| 131 | - */ | ||
| 132 | - private String mqttClientId; | ||
| 133 | - /** | ||
| 134 | - * MQTT 用户名 | ||
| 135 | - */ | ||
| 136 | - private String mqttUsername; | ||
| 137 | - /** | ||
| 138 | - * MQTT 密码 | ||
| 139 | - */ | ||
| 140 | - private String mqttPassword; | ||
| 141 | - /** | ||
| 142 | * 认证类型(如一机一密、动态注册) | 129 | * 认证类型(如一机一密、动态注册) |
| 143 | */ | 130 | */ |
| 144 | // TODO @haohao:是不是要枚举哈 | 131 | // TODO @haohao:是不是要枚举哈 |
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; | @@ -9,14 +9,15 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; | ||
| 9 | */ | 9 | */ |
| 10 | public interface RedisKeyConstants { | 10 | public interface RedisKeyConstants { |
| 11 | 11 | ||
| 12 | + // TODO @芋艿:弱化 deviceKey;使用 product_key + device_name 替代 | ||
| 12 | /** | 13 | /** |
| 13 | * 设备属性的数据缓存,采用 HASH 结构 | 14 | * 设备属性的数据缓存,采用 HASH 结构 |
| 14 | * <p> | 15 | * <p> |
| 15 | - * KEY 格式:device_property:{productKey},${deviceName} | 16 | + * KEY 格式:device_property:{deviceKey} |
| 16 | * HASH KEY:identifier 属性标识 | 17 | * HASH KEY:identifier 属性标识 |
| 17 | * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} | 18 | * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} |
| 18 | */ | 19 | */ |
| 19 | - String DEVICE_PROPERTY = "iot:device_property:%s,%s"; | 20 | + String DEVICE_PROPERTY = "iot:device_property:%s"; |
| 20 | 21 | ||
| 21 | /** | 22 | /** |
| 22 | * 设备的最后上报时间,采用 ZSET 结构 | 23 | * 设备的最后上报时间,采用 ZSET 结构 |
| @@ -27,6 +28,15 @@ public interface RedisKeyConstants { | @@ -27,6 +28,15 @@ public interface RedisKeyConstants { | ||
| 27 | String DEVICE_REPORT_TIMES = "iot:device_report_times"; | 28 | String DEVICE_REPORT_TIMES = "iot:device_report_times"; |
| 28 | 29 | ||
| 29 | /** | 30 | /** |
| 31 | + * 设备关联的网关 serverId 缓存,采用 HASH 结构 | ||
| 32 | + * | ||
| 33 | + * KEY 格式:device_server_id | ||
| 34 | + * HASH KEY:{productKey},{deviceName} | ||
| 35 | + * VALUE 数据类型:String serverId | ||
| 36 | + */ | ||
| 37 | + String DEVICE_SERVER_ID = "iot:device_server_id"; | ||
| 38 | + | ||
| 39 | + /** | ||
| 30 | * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) | 40 | * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) |
| 31 | * | 41 | * |
| 32 | * KEY 格式:device_${productKey}_${deviceName} | 42 | * KEY 格式:device_${productKey}_${deviceName} |
| @@ -42,13 +52,4 @@ public interface RedisKeyConstants { | @@ -42,13 +52,4 @@ public interface RedisKeyConstants { | ||
| 42 | */ | 52 | */ |
| 43 | String THING_MODEL_LIST = "iot:thing_model_list"; | 53 | String THING_MODEL_LIST = "iot:thing_model_list"; |
| 44 | 54 | ||
| 45 | - /** | ||
| 46 | - * 设备关联的网关 serverId 缓存,采用 HASH 结构 | ||
| 47 | - * | ||
| 48 | - * KEY 格式:device_server_id | ||
| 49 | - * HASH KEY:{productKey},{deviceName} | ||
| 50 | - * VALUE 数据类型:String serverId | ||
| 51 | - */ | ||
| 52 | - String DEVICE_SERVER_ID = "iot:device_server_id"; | ||
| 53 | - | ||
| 54 | } | 55 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.framework.security.config; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; | ||
| 4 | -import cn.iocoder.yudao.module.iot.enums.ApiConstants; | ||
| 5 | -import org.springframework.context.annotation.Bean; | ||
| 6 | -import org.springframework.context.annotation.Configuration; | ||
| 7 | -import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| 8 | -import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; | ||
| 9 | - | ||
| 10 | -/** | ||
| 11 | - * IoT 模块的 Security 配置 | ||
| 12 | - */ | ||
| 13 | -@Configuration(proxyBeanMethods = false, value = "iotSecurityConfiguration") | ||
| 14 | -public class SecurityConfiguration { | ||
| 15 | - | ||
| 16 | - @Bean("iotAuthorizeRequestsCustomizer") | ||
| 17 | - public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { | ||
| 18 | - return new AuthorizeRequestsCustomizer() { | ||
| 19 | - | ||
| 20 | - @Override | ||
| 21 | - public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) { | ||
| 22 | - // RPC 服务的安全配置 | ||
| 23 | - registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); | ||
| 24 | - } | ||
| 25 | - | ||
| 26 | - }; | ||
| 27 | - } | ||
| 28 | - | ||
| 29 | -} |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java deleted
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; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.device; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; | 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 5 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 6 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 7 | import jakarta.validation.Valid; | 8 | import jakarta.validation.Valid; |
| @@ -228,4 +229,12 @@ public interface IotDeviceService { | @@ -228,4 +229,12 @@ public interface IotDeviceService { | ||
| 228 | */ | 229 | */ |
| 229 | List<IotDeviceDO> getDeviceListByProductKeyAndNames(String productKey, List<String> deviceNames); | 230 | List<IotDeviceDO> getDeviceListByProductKeyAndNames(String productKey, List<String> deviceNames); |
| 230 | 231 | ||
| 232 | + /** | ||
| 233 | + * 认证设备 | ||
| 234 | + * | ||
| 235 | + * @param authReqDTO 认证信息 | ||
| 236 | + * @return 是否认证成功 | ||
| 237 | + */ | ||
| 238 | + boolean authDevice(IotDeviceAuthReqDTO authReqDTO); | ||
| 239 | + | ||
| 231 | } | 240 | } |
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; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.device; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.ObjUtil; | ||
| 5 | import cn.hutool.core.util.RandomUtil; | 6 | import cn.hutool.core.util.RandomUtil; |
| 6 | import cn.hutool.core.util.StrUtil; | 7 | import cn.hutool.core.util.StrUtil; |
| 7 | import cn.hutool.extra.spring.SpringUtil; | 8 | import cn.hutool.extra.spring.SpringUtil; |
| @@ -12,16 +13,16 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; | @@ -12,16 +13,16 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; | ||
| 12 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; | 13 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; |
| 13 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | 14 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 14 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; | 15 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 16 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 17 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | ||
| 15 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 18 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 16 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; | 19 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; |
| 17 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | 20 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 18 | import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; | 21 | import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; |
| 19 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; | 22 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; |
| 20 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | ||
| 21 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; | 23 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; | 24 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 23 | -import cn.iocoder.yudao.module.iot.util.MqttSignUtils; | ||
| 24 | -import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; | 25 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; |
| 25 | import jakarta.annotation.Resource; | 26 | import jakarta.annotation.Resource; |
| 26 | import jakarta.validation.ConstraintViolationException; | 27 | import jakarta.validation.ConstraintViolationException; |
| 27 | import lombok.extern.slf4j.Slf4j; | 28 | import lombok.extern.slf4j.Slf4j; |
| @@ -397,15 +398,17 @@ public class IotDeviceServiceImpl implements IotDeviceService { | @@ -397,15 +398,17 @@ public class IotDeviceServiceImpl implements IotDeviceService { | ||
| 397 | return respVO; | 398 | return respVO; |
| 398 | } | 399 | } |
| 399 | 400 | ||
| 401 | + // TODO @芋艿:改成通用的; | ||
| 400 | @Override | 402 | @Override |
| 401 | public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { | 403 | public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { |
| 402 | IotDeviceDO device = validateDeviceExists(deviceId); | 404 | IotDeviceDO device = validateDeviceExists(deviceId); |
| 403 | - MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), | ||
| 404 | - device.getDeviceSecret()); | ||
| 405 | - return new IotDeviceMqttConnectionParamsRespVO() | ||
| 406 | - .setMqttClientId(mqttSignResult.getClientId()) | ||
| 407 | - .setMqttUsername(mqttSignResult.getUsername()) | ||
| 408 | - .setMqttPassword(mqttSignResult.getPassword()); | 405 | +// MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), |
| 406 | +// device.getDeviceSecret()); | ||
| 407 | +// return new IotDeviceMqttConnectionParamsRespVO() | ||
| 408 | +// .setMqttClientId(mqttSignResult.getClientId()) | ||
| 409 | +// .setMqttUsername(mqttSignResult.getUsername()) | ||
| 410 | +// .setMqttPassword(mqttSignResult.getPassword()); | ||
| 411 | + return null; | ||
| 409 | } | 412 | } |
| 410 | 413 | ||
| 411 | private void deleteDeviceCache(IotDeviceDO device) { | 414 | private void deleteDeviceCache(IotDeviceDO device) { |
| @@ -459,4 +462,29 @@ public class IotDeviceServiceImpl implements IotDeviceService { | @@ -459,4 +462,29 @@ public class IotDeviceServiceImpl implements IotDeviceService { | ||
| 459 | return deviceMapper.selectByProductKeyAndDeviceNames(productKey, deviceNames); | 462 | return deviceMapper.selectByProductKeyAndDeviceNames(productKey, deviceNames); |
| 460 | } | 463 | } |
| 461 | 464 | ||
| 465 | + @Override | ||
| 466 | + public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) { | ||
| 467 | + // 1. 校验设备是否存在 | ||
| 468 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername()); | ||
| 469 | + if (deviceInfo == null) { | ||
| 470 | + log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername()); | ||
| 471 | + return false; | ||
| 472 | + } | ||
| 473 | + String deviceName = deviceInfo.getDeviceName(); | ||
| 474 | + String productKey = deviceInfo.getProductKey(); | ||
| 475 | + IotDeviceDO device = getSelf().getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); | ||
| 476 | + if (device == null) { | ||
| 477 | + log.warn("[authDevice][设备({}/{}) 不存在]", productKey, deviceName); | ||
| 478 | + return false; | ||
| 479 | + } | ||
| 480 | + | ||
| 481 | + // 2. 校验密码 | ||
| 482 | + IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret()); | ||
| 483 | + if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) { | ||
| 484 | + log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); | ||
| 485 | + return false; | ||
| 486 | + } | ||
| 487 | + return true; | ||
| 488 | + } | ||
| 489 | + | ||
| 462 | } | 490 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java
| 1 | package cn.iocoder.yudao.module.iot.service.device.control; | 1 | package cn.iocoder.yudao.module.iot.service.device.control; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; | ||
| 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; | 3 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; |
| 5 | import jakarta.validation.Valid; | 4 | import jakarta.validation.Valid; |
| 6 | 5 | ||
| @@ -48,11 +47,4 @@ public interface IotDeviceUpstreamService { | @@ -48,11 +47,4 @@ public interface IotDeviceUpstreamService { | ||
| 48 | // */ | 47 | // */ |
| 49 | // void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO); | 48 | // void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO); |
| 50 | 49 | ||
| 51 | - /** | ||
| 52 | - * Emqx 连接认证 | ||
| 53 | - * | ||
| 54 | - * @param authReqDTO Emqx 连接认证 DTO | ||
| 55 | - */ | ||
| 56 | - boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); | ||
| 57 | - | ||
| 58 | } | 50 | } |
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; | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.service.device.control; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | import cn.hutool.core.util.ObjUtil; | 5 | import cn.hutool.core.util.ObjUtil; |
| 6 | -import cn.hutool.core.util.StrUtil; | ||
| 7 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | 6 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 8 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; | 7 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; |
| 9 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; | 8 | 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; | @@ -12,8 +11,6 @@ import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; | ||
| 12 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; | 11 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 13 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | 12 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 14 | import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; | 13 | import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; |
| 15 | -import cn.iocoder.yudao.module.iot.util.MqttSignUtils; | ||
| 16 | -import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; | ||
| 17 | import jakarta.annotation.Resource; | 14 | import jakarta.annotation.Resource; |
| 18 | import lombok.extern.slf4j.Slf4j; | 15 | import lombok.extern.slf4j.Slf4j; |
| 19 | import org.springframework.stereotype.Service; | 16 | import org.springframework.stereotype.Service; |
| @@ -222,37 +219,4 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | @@ -222,37 +219,4 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { | ||
| 222 | // sendDeviceMessage(message, device); | 219 | // sendDeviceMessage(message, device); |
| 223 | } | 220 | } |
| 224 | 221 | ||
| 225 | - // TODO @芋艿:后续需要考虑,http 的认证 | ||
| 226 | - @Override | ||
| 227 | - public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { | ||
| 228 | - log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); | ||
| 229 | - // 1.1 校验设备是否存在。username 格式:${DeviceName}&${ProductKey} | ||
| 230 | - String[] usernameParts = authReqDTO.getUsername().split("&"); | ||
| 231 | - if (usernameParts.length != 2) { | ||
| 232 | - log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); | ||
| 233 | - return false; | ||
| 234 | - } | ||
| 235 | - String deviceName = usernameParts[0]; | ||
| 236 | - String productKey = usernameParts[1]; | ||
| 237 | - // 1.2 获得设备 | ||
| 238 | - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); | ||
| 239 | - if (device == null) { | ||
| 240 | - log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", productKey, deviceName); | ||
| 241 | - return false; | ||
| 242 | - } | ||
| 243 | - // TODO @haohao:需要记录,记录设备的最后时间 | ||
| 244 | - | ||
| 245 | - // 2. 校验密码 | ||
| 246 | - String deviceSecret = device.getDeviceSecret(); | ||
| 247 | - String clientId = authReqDTO.getClientId(); | ||
| 248 | - MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); | ||
| 249 | - // TODO 建议,先失败,return false; | ||
| 250 | - if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { | ||
| 251 | - log.info("[authenticateEmqxConnection][认证成功]"); | ||
| 252 | - return true; | ||
| 253 | - } | ||
| 254 | - log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); | ||
| 255 | - return false; | ||
| 256 | - } | ||
| 257 | - | ||
| 258 | } | 222 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.util; | ||
| 2 | - | ||
| 3 | -import cn.hutool.crypto.digest.HMac; | ||
| 4 | -import cn.hutool.crypto.digest.HmacAlgorithm; | ||
| 5 | -import lombok.AllArgsConstructor; | ||
| 6 | -import lombok.Getter; | ||
| 7 | - | ||
| 8 | -import java.nio.charset.StandardCharsets; | ||
| 9 | - | ||
| 10 | -/** | ||
| 11 | - * MQTT 签名工具类 | ||
| 12 | - * | ||
| 13 | - * 提供静态方法来计算 MQTT 连接参数 | ||
| 14 | - */ | ||
| 15 | -public class MqttSignUtils { | ||
| 16 | - | ||
| 17 | - /** | ||
| 18 | - * 计算 MQTT 连接参数 | ||
| 19 | - * | ||
| 20 | - * @param productKey 产品密钥 | ||
| 21 | - * @param deviceName 设备名称 | ||
| 22 | - * @param deviceSecret 设备密钥 | ||
| 23 | - * @return 包含 clientId, username, password 的结果对象 | ||
| 24 | - */ | ||
| 25 | - public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { | ||
| 26 | - return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName); | ||
| 27 | - } | ||
| 28 | - | ||
| 29 | - /** | ||
| 30 | - * 计算 MQTT 连接参数 | ||
| 31 | - * | ||
| 32 | - * @param productKey 产品密钥 | ||
| 33 | - * @param deviceName 设备名称 | ||
| 34 | - * @param deviceSecret 设备密钥 | ||
| 35 | - * @param clientId 客户端 ID | ||
| 36 | - * @return 包含 clientId, username, password 的结果对象 | ||
| 37 | - */ | ||
| 38 | - public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { | ||
| 39 | - String username = deviceName + "&" + productKey; | ||
| 40 | - // 构建签名内容 | ||
| 41 | - StringBuilder signContentBuilder = new StringBuilder() | ||
| 42 | - .append("clientId").append(clientId) | ||
| 43 | - .append("deviceName").append(deviceName) | ||
| 44 | - .append("deviceSecret").append(deviceSecret) | ||
| 45 | - .append("productKey").append(productKey); | ||
| 46 | - | ||
| 47 | - // 使用 HMac 计算签名 | ||
| 48 | - byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8); | ||
| 49 | - String signContent = signContentBuilder.toString(); | ||
| 50 | - HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key); | ||
| 51 | - String password = mac.digestHex(signContent); | ||
| 52 | - | ||
| 53 | - return new MqttSignResult(clientId, username, password); | ||
| 54 | - } | ||
| 55 | - | ||
| 56 | - /** | ||
| 57 | - * MQTT 签名结果类 | ||
| 58 | - */ | ||
| 59 | - @Getter | ||
| 60 | - @AllArgsConstructor | ||
| 61 | - public static class MqttSignResult { | ||
| 62 | - | ||
| 63 | - private final String clientId; | ||
| 64 | - private final String username; | ||
| 65 | - private final String password; | ||
| 66 | - | ||
| 67 | - } | ||
| 68 | - | ||
| 69 | -} | ||
| 70 | \ No newline at end of file | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
| 1 | package cn.iocoder.yudao.module.iot.core.biz; | 1 | package cn.iocoder.yudao.module.iot.core.biz; |
| 2 | 2 | ||
| 3 | -// TODO @芋艿:待实现 | 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * IoT 设备通用 API | ||
| 8 | + * | ||
| 9 | + * @author haohao | ||
| 10 | + */ | ||
| 4 | public interface IotDeviceCommonApi { | 11 | public interface IotDeviceCommonApi { |
| 12 | + | ||
| 13 | + CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO); | ||
| 14 | + | ||
| 5 | } | 15 | } |
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
| 1 | -package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; | 1 | +package cn.iocoder.yudao.module.iot.core.biz.dto; |
| 2 | 2 | ||
| 3 | import jakarta.validation.constraints.NotEmpty; | 3 | import jakarta.validation.constraints.NotEmpty; |
| 4 | import lombok.Data; | 4 | import lombok.Data; |
| 5 | 5 | ||
| 6 | -// TODO @芋艿:要不要继承 IotDeviceUpstreamAbstractReqDTO | ||
| 7 | -// TODO @芋艿:@haohao:后续其它认证的设计 | ||
| 8 | /** | 6 | /** |
| 9 | - * IoT 认证 Emqx 连接 Request DTO | 7 | + * IoT 设备认证 Request DTO |
| 10 | * | 8 | * |
| 11 | * @author 芋道源码 | 9 | * @author 芋道源码 |
| 12 | */ | 10 | */ |
| 13 | @Data | 11 | @Data |
| 14 | -public class IotDeviceEmqxAuthReqDTO { | 12 | +public class IotDeviceAuthReqDTO { |
| 15 | 13 | ||
| 16 | /** | 14 | /** |
| 17 | * 客户端 ID | 15 | * 客户端 ID |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.util; | ||
| 2 | + | ||
| 3 | +import cn.hutool.crypto.digest.DigestUtil; | ||
| 4 | +import cn.hutool.crypto.digest.HmacAlgorithm; | ||
| 5 | +import lombok.AllArgsConstructor; | ||
| 6 | +import lombok.Data; | ||
| 7 | +import lombok.NoArgsConstructor; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IoT 设备【认证】的工具类,参考阿里云 | ||
| 11 | + * | ||
| 12 | + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/how-do-i-obtain-mqtt-parameters-for-authentication">如何计算 MQTT 签名参数</a> | ||
| 13 | + */ | ||
| 14 | +public class IotDeviceAuthUtils { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 认证信息 | ||
| 18 | + */ | ||
| 19 | + @Data | ||
| 20 | + @NoArgsConstructor | ||
| 21 | + @AllArgsConstructor | ||
| 22 | + public static class AuthInfo { | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 客户端 ID | ||
| 26 | + */ | ||
| 27 | + private String clientId; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 用户名 | ||
| 31 | + */ | ||
| 32 | + private String username; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 密码 | ||
| 36 | + */ | ||
| 37 | + private String password; | ||
| 38 | + | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 设备信息 | ||
| 43 | + */ | ||
| 44 | + @Data | ||
| 45 | + public static class DeviceInfo { | ||
| 46 | + | ||
| 47 | + private String productKey; | ||
| 48 | + | ||
| 49 | + private String deviceName; | ||
| 50 | + | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) { | ||
| 54 | + String clientId = buildClientId(productKey, deviceName); | ||
| 55 | + String username = buildUsername(productKey, deviceName); | ||
| 56 | + String content = "clientId" + clientId + | ||
| 57 | + "deviceName" + deviceName + | ||
| 58 | + "deviceSecret" + deviceSecret + | ||
| 59 | + "productKey" + productKey; | ||
| 60 | + String password = buildPassword(deviceSecret, content); | ||
| 61 | + return new AuthInfo(clientId, username, password); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + private static String buildClientId(String productKey, String deviceName) { | ||
| 65 | + return String.format("%s.%s", productKey, deviceName); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + private static String buildUsername(String productKey, String deviceName) { | ||
| 69 | + return String.format("%s&%s", deviceName, productKey); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + private static String buildPassword(String deviceSecret, String content) { | ||
| 73 | + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes()) | ||
| 74 | + .digestHex(content); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + public static DeviceInfo parseUsername(String username) { | ||
| 78 | + String[] usernameParts = username.split("&"); | ||
| 79 | + if (usernameParts.length != 2) { | ||
| 80 | + return null; | ||
| 81 | + } | ||
| 82 | + return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | +} |
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; | @@ -6,7 +6,7 @@ import cn.hutool.system.SystemUtil; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 7 | ||
| 8 | /** | 8 | /** |
| 9 | - * IoT 设备消息的工具类 | 9 | + * IoT 设备【消息】的工具类 |
| 10 | * | 10 | * |
| 11 | * @author 芋道源码 | 11 | * @author 芋道源码 |
| 12 | */ | 12 | */ |
yudao-module-iot/yudao-module-iot-gateway/pom.xml
| @@ -23,6 +23,11 @@ | @@ -23,6 +23,11 @@ | ||
| 23 | <version>${revision}</version> | 23 | <version>${revision}</version> |
| 24 | </dependency> | 24 | </dependency> |
| 25 | 25 | ||
| 26 | + <dependency> | ||
| 27 | + <groupId>org.springframework</groupId> | ||
| 28 | + <artifactId>spring-web</artifactId> | ||
| 29 | + </dependency> | ||
| 30 | + | ||
| 26 | <!-- 消息队列相关 --> | 31 | <!-- 消息队列相关 --> |
| 27 | <dependency> | 32 | <dependency> |
| 28 | <groupId>org.apache.rocketmq</groupId> | 33 | <groupId>org.apache.rocketmq</groupId> |
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 { | @@ -24,9 +24,8 @@ public class IotGatewayConfiguration { | ||
| 24 | public static class HttpProtocolConfiguration { | 24 | public static class HttpProtocolConfiguration { |
| 25 | 25 | ||
| 26 | @Bean | 26 | @Bean |
| 27 | - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties, | ||
| 28 | - IotDeviceMessageProducer deviceMessageProducer) { | ||
| 29 | - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), deviceMessageProducer); | 27 | + public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) { |
| 28 | + return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp()); | ||
| 30 | } | 29 | } |
| 31 | 30 | ||
| 32 | @Bean | 31 | @Bean |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.config; | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | ||
| 3 | +import jakarta.validation.constraints.NotEmpty; | ||
| 4 | +import jakarta.validation.constraints.NotNull; | ||
| 3 | import lombok.Data; | 5 | import lombok.Data; |
| 4 | import org.springframework.boot.context.properties.ConfigurationProperties; | 6 | import org.springframework.boot.context.properties.ConfigurationProperties; |
| 5 | import org.springframework.validation.annotation.Validated; | 7 | import org.springframework.validation.annotation.Validated; |
| 6 | 8 | ||
| 9 | +import java.time.Duration; | ||
| 7 | import java.util.List; | 10 | import java.util.List; |
| 8 | 11 | ||
| 9 | @ConfigurationProperties(prefix = "yudao.iot.gateway") | 12 | @ConfigurationProperties(prefix = "yudao.iot.gateway") |
| @@ -15,6 +18,10 @@ public class IotGatewayProperties { | @@ -15,6 +18,10 @@ public class IotGatewayProperties { | ||
| 15 | * 设备 RPC 服务配置 | 18 | * 设备 RPC 服务配置 |
| 16 | */ | 19 | */ |
| 17 | private RpcProperties rpc; | 20 | private RpcProperties rpc; |
| 21 | + /** | ||
| 22 | + * Token 配置 | ||
| 23 | + */ | ||
| 24 | + private TokenProperties token; | ||
| 18 | 25 | ||
| 19 | /** | 26 | /** |
| 20 | * 协议配置 | 27 | * 协议配置 |
| @@ -27,15 +34,34 @@ public class IotGatewayProperties { | @@ -27,15 +34,34 @@ public class IotGatewayProperties { | ||
| 27 | /** | 34 | /** |
| 28 | * 主程序 API 地址 | 35 | * 主程序 API 地址 |
| 29 | */ | 36 | */ |
| 37 | + @NotEmpty(message = "主程序 API 地址不能为空") | ||
| 30 | private String url; | 38 | private String url; |
| 31 | /** | 39 | /** |
| 32 | * 连接超时时间 | 40 | * 连接超时时间 |
| 33 | */ | 41 | */ |
| 34 | - private String connectTimeout; | 42 | + @NotNull(message = "连接超时时间不能为空") |
| 43 | + private Duration connectTimeout; | ||
| 35 | /** | 44 | /** |
| 36 | * 读取超时时间 | 45 | * 读取超时时间 |
| 37 | */ | 46 | */ |
| 38 | - private String readTimeout; | 47 | + @NotNull(message = "读取超时时间不能为空") |
| 48 | + private Duration readTimeout; | ||
| 49 | + | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + @Data | ||
| 53 | + public static class TokenProperties { | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 密钥 | ||
| 57 | + */ | ||
| 58 | + @NotEmpty(message = "密钥不能为空") | ||
| 59 | + private String secret; | ||
| 60 | + /** | ||
| 61 | + * 令牌有效期 | ||
| 62 | + */ | ||
| 63 | + @NotNull(message = "令牌有效期不能为空") | ||
| 64 | + private Duration expiration; | ||
| 39 | 65 | ||
| 40 | } | 66 | } |
| 41 | 67 | ||
| @@ -60,6 +86,7 @@ public class IotGatewayProperties { | @@ -60,6 +86,7 @@ public class IotGatewayProperties { | ||
| 60 | /** | 86 | /** |
| 61 | * 是否开启 | 87 | * 是否开启 |
| 62 | */ | 88 | */ |
| 89 | + @NotNull(message = "是否开启不能为空") | ||
| 63 | private Boolean enabled; | 90 | private Boolean enabled; |
| 64 | /** | 91 | /** |
| 65 | * 服务端口 | 92 | * 服务端口 |
| @@ -74,6 +101,7 @@ public class IotGatewayProperties { | @@ -74,6 +101,7 @@ public class IotGatewayProperties { | ||
| 74 | /** | 101 | /** |
| 75 | * 是否开启 | 102 | * 是否开启 |
| 76 | */ | 103 | */ |
| 104 | + @NotNull(message = "是否开启不能为空") | ||
| 77 | private Boolean enabled; | 105 | private Boolean enabled; |
| 78 | /** | 106 | /** |
| 79 | * MQTT 服务器地址 | 107 | * MQTT 服务器地址 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/enums/ErrorCodeConstants.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.enums; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.exception.ErrorCode; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * iot gateway 错误码枚举类 | ||
| 7 | + * <p> | ||
| 8 | + * iot 系统,使用 1-051-000-000 段 | ||
| 9 | + */ | ||
| 10 | +public interface ErrorCodeConstants { | ||
| 11 | + | ||
| 12 | + // ========== 设备认证 1-050-001-000 ============ | ||
| 13 | + ErrorCode DEVICE_AUTH_FAIL = new ErrorCode(1_051_001_000, "设备鉴权失败"); // 对应阿里云 20000 | ||
| 14 | + ErrorCode DEVICE_TOKEN_EXPIRED = new ErrorCode(1_051_001_002, "token 失效。需重新调用 auth 进行鉴权,获取token"); // 对应阿里云 20001 | ||
| 15 | + | ||
| 16 | +} | ||
| 0 | \ No newline at end of file | 17 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | ||
| 4 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 5 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; | ||
| 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; |
| 7 | import io.vertx.core.AbstractVerticle; | 7 | import io.vertx.core.AbstractVerticle; |
| 8 | import io.vertx.core.Vertx; | 8 | import io.vertx.core.Vertx; |
| @@ -25,8 +25,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { | @@ -25,8 +25,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { | ||
| 25 | 25 | ||
| 26 | private final IotGatewayProperties.HttpProperties httpProperties; | 26 | private final IotGatewayProperties.HttpProperties httpProperties; |
| 27 | 27 | ||
| 28 | - private final IotDeviceMessageProducer deviceMessageProducer; | ||
| 29 | - | ||
| 30 | private HttpServer httpServer; | 28 | private HttpServer httpServer; |
| 31 | 29 | ||
| 32 | @Override | 30 | @Override |
| @@ -38,10 +36,11 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { | @@ -38,10 +36,11 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { | ||
| 38 | router.route().handler(BodyHandler.create()); | 36 | router.route().handler(BodyHandler.create()); |
| 39 | 37 | ||
| 40 | // 创建处理器,添加路由处理器 | 38 | // 创建处理器,添加路由处理器 |
| 41 | - IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler( | ||
| 42 | - this, deviceMessageProducer); | 39 | + IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); |
| 43 | router.post(IotHttpUpstreamHandler.PROPERTY_PATH).handler(upstreamHandler); | 40 | router.post(IotHttpUpstreamHandler.PROPERTY_PATH).handler(upstreamHandler); |
| 44 | router.post(IotHttpUpstreamHandler.EVENT_PATH).handler(upstreamHandler); | 41 | router.post(IotHttpUpstreamHandler.EVENT_PATH).handler(upstreamHandler); |
| 42 | + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); | ||
| 43 | + router.post(IotHttpAuthHandler.PATH).handler(authHandler); | ||
| 45 | 44 | ||
| 46 | // 启动 HTTP 服务器 | 45 | // 启动 HTTP 服务器 |
| 47 | try { | 46 | try { |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 4 | +import cn.hutool.core.util.ObjUtil; | ||
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 6 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 7 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 8 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 9 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 10 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | ||
| 12 | +import io.vertx.core.Handler; | ||
| 13 | +import io.vertx.core.http.HttpHeaders; | ||
| 14 | +import io.vertx.ext.web.RoutingContext; | ||
| 15 | +import lombok.RequiredArgsConstructor; | ||
| 16 | +import lombok.extern.slf4j.Slf4j; | ||
| 17 | +import org.springframework.http.MediaType; | ||
| 18 | + | ||
| 19 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; | ||
| 20 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 21 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 22 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * IoT 网关 HTTP 协议的处理器抽象基类:提供通用的前置处理(认证)、全局的异常捕获等 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +@RequiredArgsConstructor | ||
| 30 | +@Slf4j | ||
| 31 | +public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> { | ||
| 32 | + | ||
| 33 | + private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | ||
| 34 | + | ||
| 35 | + @Override | ||
| 36 | + public void handle(RoutingContext context) { | ||
| 37 | + try { | ||
| 38 | + // 1. 前置处理 | ||
| 39 | + CommonResult<Object> result = beforeHandle(context); | ||
| 40 | + if (result != null) { | ||
| 41 | + writeResponse(context, result); | ||
| 42 | + return; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + // 2. 执行逻辑 | ||
| 46 | + result = handle0(context); | ||
| 47 | + writeResponse(context, result); | ||
| 48 | + } catch (ServiceException e) { | ||
| 49 | + writeResponse(context, CommonResult.error(e.getCode(), e.getMessage())); | ||
| 50 | + } catch (Exception e) { | ||
| 51 | + log.error("[handle][path({}) 处理异常]", context.request().path(), e); | ||
| 52 | + writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR)); | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + protected abstract CommonResult<Object> handle0(RoutingContext context); | ||
| 57 | + | ||
| 58 | + private CommonResult<Object> beforeHandle(RoutingContext context) { | ||
| 59 | + // 如果不需要认证,则不走前置处理 | ||
| 60 | + String path = context.request().path(); | ||
| 61 | + if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { | ||
| 62 | + return null; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + // 解析参数 | ||
| 66 | + String token = context.request().getHeader(HttpHeaders.AUTHORIZATION); | ||
| 67 | + if (StrUtil.isEmpty(token)) { | ||
| 68 | + throw invalidParamException("token 不能为空"); | ||
| 69 | + } | ||
| 70 | + String productKey = context.pathParam("productKey"); | ||
| 71 | + if (StrUtil.isEmpty(productKey)) { | ||
| 72 | + throw invalidParamException("productKey 不能为空"); | ||
| 73 | + } | ||
| 74 | + String deviceName = context.pathParam("deviceName"); | ||
| 75 | + if (StrUtil.isEmpty(deviceName)) { | ||
| 76 | + throw invalidParamException("deviceName 不能为空"); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + // 校验 token | ||
| 80 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); | ||
| 81 | + Assert.notNull(deviceInfo, "设备信息不能为空"); | ||
| 82 | + // 校验设备信息是否匹配 | ||
| 83 | + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) | ||
| 84 | + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) { | ||
| 85 | + throw exception(FORBIDDEN); | ||
| 86 | + } | ||
| 87 | + return null; | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + @SuppressWarnings("deprecation") | ||
| 91 | + public static void writeResponse(RoutingContext context, Object data) { | ||
| 92 | + context.response() | ||
| 93 | + .setStatusCode(200) | ||
| 94 | + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) | ||
| 95 | + .end(JsonUtils.toJsonString(data)); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 4 | +import cn.hutool.core.map.MapUtil; | ||
| 5 | +import cn.hutool.core.util.StrUtil; | ||
| 6 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 7 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 8 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 9 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 10 | +import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | ||
| 11 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | ||
| 13 | +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; | ||
| 14 | +import io.vertx.core.json.JsonObject; | ||
| 15 | +import io.vertx.ext.web.RoutingContext; | ||
| 16 | + | ||
| 17 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 18 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | ||
| 19 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 20 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | ||
| 21 | + | ||
| 22 | +/** | ||
| 23 | + * IoT 网关 HTTP 协议的【认证】处理器 | ||
| 24 | + * | ||
| 25 | + * 参考 <a href="阿里云 IoT —— HTTPS 连接通信">https://help.aliyun.com/zh/iot/user-guide/establish-connections-over-https</a> | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +public class IotHttpAuthHandler extends IotHttpAbstractHandler { | ||
| 30 | + | ||
| 31 | + public static final String PATH = "/auth"; | ||
| 32 | + | ||
| 33 | + private final IotHttpUpstreamProtocol protocol; | ||
| 34 | + | ||
| 35 | + private final IotDeviceMessageProducer deviceMessageProducer; | ||
| 36 | + | ||
| 37 | + private final IotDeviceTokenService deviceTokenService; | ||
| 38 | + | ||
| 39 | + private final IotDeviceCommonApi deviceClientService; | ||
| 40 | + | ||
| 41 | + public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) { | ||
| 42 | + this.protocol = protocol; | ||
| 43 | + this.deviceMessageProducer = SpringUtil.getBean(IotDeviceMessageProducer.class); | ||
| 44 | + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); | ||
| 45 | + this.deviceClientService = SpringUtil.getBean(IotDeviceCommonApi.class); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + @Override | ||
| 49 | + public CommonResult<Object> handle0(RoutingContext context) { | ||
| 50 | + // 解析参数 | ||
| 51 | + JsonObject body = context.body().asJsonObject(); | ||
| 52 | + String clientId = body.getString("clientId"); | ||
| 53 | + if (StrUtil.isEmpty(clientId)) { | ||
| 54 | + throw invalidParamException("clientId 不能为空"); | ||
| 55 | + } | ||
| 56 | + String username = body.getString("username"); | ||
| 57 | + if (StrUtil.isEmpty(username)) { | ||
| 58 | + throw invalidParamException("username 不能为空"); | ||
| 59 | + } | ||
| 60 | + String password = body.getString("password"); | ||
| 61 | + if (StrUtil.isEmpty(password)) { | ||
| 62 | + throw invalidParamException("password 不能为空"); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + // 执行认证 | ||
| 66 | + CommonResult<Boolean> result = deviceClientService.authDevice(new IotDeviceAuthReqDTO() | ||
| 67 | + .setClientId(clientId).setUsername(username).setPassword(password)); | ||
| 68 | + if (result == null || !result.isSuccess()) { | ||
| 69 | + throw exception(DEVICE_AUTH_FAIL); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + // 生成 Token | ||
| 73 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username); | ||
| 74 | + Assert.notNull(deviceInfo, "设备信息不能为空"); | ||
| 75 | + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); | ||
| 76 | + Assert.notBlank(token, "生成 token 不能为空位"); | ||
| 77 | + | ||
| 78 | + // TODO @芋艿:发送上线消息; | ||
| 79 | + | ||
| 80 | + // 构建响应数据 | ||
| 81 | + return success(MapUtil.of("token", token)); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | +} |
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; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.router; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | import cn.hutool.core.map.MapUtil; | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | import cn.hutool.core.util.StrUtil; | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | 9 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; |
| @@ -16,11 +17,8 @@ import lombok.extern.slf4j.Slf4j; | @@ -16,11 +17,8 @@ import lombok.extern.slf4j.Slf4j; | ||
| 16 | 17 | ||
| 17 | import java.util.Map; | 18 | import java.util.Map; |
| 18 | 19 | ||
| 19 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | ||
| 20 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 21 | - | ||
| 22 | /** | 20 | /** |
| 23 | - * IoT 网关 HTTP 协议的处理器 | 21 | + * IoT 网关 HTTP 协议的【上行】处理器 |
| 24 | * | 22 | * |
| 25 | * @author 芋道源码 | 23 | * @author 芋道源码 |
| 26 | */ | 24 | */ |
| @@ -54,47 +52,35 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { | @@ -54,47 +52,35 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { | ||
| 54 | private static final String EVENT_METHOD_SUFFIX = ".post"; | 52 | private static final String EVENT_METHOD_SUFFIX = ".post"; |
| 55 | 53 | ||
| 56 | private final IotHttpUpstreamProtocol protocol; | 54 | private final IotHttpUpstreamProtocol protocol; |
| 57 | -// /** | ||
| 58 | -// * 设备上行 API | ||
| 59 | -// */ | ||
| 60 | -// private final IotDeviceUpstreamApi deviceUpstreamApi; | ||
| 61 | - /** | ||
| 62 | - * 设备消息生产者 | ||
| 63 | - */ | ||
| 64 | - private final IotDeviceMessageProducer deviceMessageProducer; | ||
| 65 | 55 | ||
| 66 | - @Override | ||
| 67 | - public void handle(RoutingContext routingContext) { | ||
| 68 | - String path = routingContext.request().path(); | 56 | + private final IotDeviceMessageProducer deviceMessageProducer; |
| 69 | 57 | ||
| 70 | - try { | ||
| 71 | - // 1. 解析通用参数 | ||
| 72 | - Map<String, String> params = parseCommonParams(routingContext); | ||
| 73 | - String productKey = params.get("productKey"); | ||
| 74 | - String deviceName = params.get("deviceName"); | ||
| 75 | - JsonObject body = routingContext.body().asJsonObject(); | 58 | + public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) { |
| 59 | + this.protocol = protocol; | ||
| 60 | + this.deviceMessageProducer = SpringUtil.getBean(IotDeviceMessageProducer.class); | ||
| 61 | + } | ||
| 76 | 62 | ||
| 77 | - // 2. 根据路径模式处理不同类型的请求 | ||
| 78 | - if (isPropertyPostPath(path)) { | ||
| 79 | - // 处理属性上报 | ||
| 80 | - handlePropertyPost(routingContext, productKey, deviceName, body); | ||
| 81 | - return; | ||
| 82 | - } | 63 | + @Override |
| 64 | + public void handle(RoutingContext context) { | ||
| 65 | + String path = context.request().path(); | ||
| 66 | + // 1. 解析通用参数 | ||
| 67 | + Map<String, String> params = parseCommonParams(context); | ||
| 68 | + String productKey = params.get("productKey"); | ||
| 69 | + String deviceName = params.get("deviceName"); | ||
| 70 | + JsonObject body = context.body().asJsonObject(); | ||
| 83 | 71 | ||
| 84 | - if (isEventPostPath(path)) { | ||
| 85 | - // 处理事件上报 | ||
| 86 | - String identifier = routingContext.pathParam("identifier"); | ||
| 87 | - handleEventPost(routingContext, productKey, deviceName, identifier, body); | ||
| 88 | - return; | ||
| 89 | - } | 72 | + // 2. 根据路径模式处理不同类型的请求 |
| 73 | + if (isPropertyPostPath(path)) { | ||
| 74 | + // 处理属性上报 | ||
| 75 | + handlePropertyPost(context, productKey, deviceName, body); | ||
| 76 | + return; | ||
| 77 | + } | ||
| 90 | 78 | ||
| 91 | - // 不支持的请求路径 | ||
| 92 | - sendErrorResponse(routingContext, "unknown", BAD_REQUEST.getCode(), "不支持的请求路径"); | ||
| 93 | - } catch (Exception e) { | ||
| 94 | - log.error("[handle][处理上行请求异常] path={}", path, e); | ||
| 95 | - String method = determineMethodFromPath(path, routingContext); | ||
| 96 | - sendErrorResponse(routingContext, method, INTERNAL_SERVER_ERROR.getCode(), | ||
| 97 | - INTERNAL_SERVER_ERROR.getMsg()); | 79 | + if (isEventPostPath(path)) { |
| 80 | + // 处理事件上报 | ||
| 81 | + String identifier = context.pathParam("identifier"); | ||
| 82 | + handleEventPost(context, productKey, deviceName, identifier, body); | ||
| 83 | + return; | ||
| 98 | } | 84 | } |
| 99 | } | 85 | } |
| 100 | 86 | ||
| @@ -169,7 +155,6 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { | @@ -169,7 +155,6 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { | ||
| 169 | // | 155 | // |
| 170 | // // 事件上报 | 156 | // // 事件上报 |
| 171 | // CommonResult<Boolean> result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); | 157 | // CommonResult<Boolean> result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); |
| 172 | -// String method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX; | ||
| 173 | // | 158 | // |
| 174 | // // 返回响应 | 159 | // // 返回响应 |
| 175 | // sendResponse(routingContext, requestId, method, result); | 160 | // sendResponse(routingContext, requestId, method, result); |
| @@ -196,20 +181,6 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { | @@ -196,20 +181,6 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { | ||
| 196 | } | 181 | } |
| 197 | 182 | ||
| 198 | /** | 183 | /** |
| 199 | - * 发送错误响应 | ||
| 200 | - * | ||
| 201 | - * @param routingContext 路由上下文 | ||
| 202 | - * @param method 方法名 | ||
| 203 | - * @param code 错误代码 | ||
| 204 | - * @param message 错误消息 | ||
| 205 | - */ | ||
| 206 | - private void sendErrorResponse(RoutingContext routingContext, String method, Integer code, | ||
| 207 | - String message) { | ||
| 208 | -// IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message); | ||
| 209 | -// IotNetComponentCommonUtils.writeJsonResponse(routingContext, errorResponse); | ||
| 210 | - } | ||
| 211 | - | ||
| 212 | - /** | ||
| 213 | * 从路径确定方法名 | 184 | * 从路径确定方法名 |
| 214 | * | 185 | * |
| 215 | * @param path 路径 | 186 | * @param path 路径 |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.service.auth; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * IoT 设备 Token 服务 Service 接口 | ||
| 7 | + * | ||
| 8 | + * @author 芋道源码 | ||
| 9 | + */ | ||
| 10 | +public interface IotDeviceTokenService { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * 创建设备 Token | ||
| 14 | + * | ||
| 15 | + * @param productKey 产品 Key | ||
| 16 | + * @param deviceName 设备名称 | ||
| 17 | + * @return 设备 Token | ||
| 18 | + */ | ||
| 19 | + String createToken(String productKey, String deviceName); | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 验证设备 Token | ||
| 23 | + * | ||
| 24 | + * @param token 设备 Token | ||
| 25 | + * @return 设备信息 | ||
| 26 | + */ | ||
| 27 | + IotDeviceAuthUtils.DeviceInfo verifyToken(String token); | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 解析用户名 | ||
| 31 | + * | ||
| 32 | + * @param username 用户名 | ||
| 33 | + * @return 设备信息 | ||
| 34 | + */ | ||
| 35 | + IotDeviceAuthUtils.DeviceInfo parseUsername(String username); | ||
| 36 | + | ||
| 37 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.service.auth; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 4 | +import cn.hutool.json.JSONObject; | ||
| 5 | +import cn.hutool.jwt.JWT; | ||
| 6 | +import cn.hutool.jwt.JWTUtil; | ||
| 7 | +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; | ||
| 8 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 10 | +import jakarta.annotation.Resource; | ||
| 11 | +import lombok.extern.slf4j.Slf4j; | ||
| 12 | +import org.springframework.stereotype.Service; | ||
| 13 | + | ||
| 14 | +import java.time.LocalDateTime; | ||
| 15 | +import java.util.HashMap; | ||
| 16 | +import java.util.Map; | ||
| 17 | + | ||
| 18 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 19 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_TOKEN_EXPIRED; | ||
| 20 | + | ||
| 21 | +/** | ||
| 22 | + * IoT 设备 Token Service 实现类:调用远程的 device http 接口,进行设备 Token 生成、解析等逻辑 | ||
| 23 | + * | ||
| 24 | + * 注意:目前仅 HTTP 协议使用 | ||
| 25 | + * | ||
| 26 | + * @author 芋道源码 | ||
| 27 | + */ | ||
| 28 | +@Service | ||
| 29 | +@Slf4j | ||
| 30 | +public class IotDeviceTokenServiceImpl implements IotDeviceTokenService { | ||
| 31 | + | ||
| 32 | + @Resource | ||
| 33 | + private IotGatewayProperties gatewayProperties; | ||
| 34 | + | ||
| 35 | + @Override | ||
| 36 | + public String createToken(String productKey, String deviceName) { | ||
| 37 | + Assert.notBlank(productKey, "productKey 不能为空"); | ||
| 38 | + Assert.notBlank(deviceName, "deviceName 不能为空"); | ||
| 39 | + // 构建 JWT payload | ||
| 40 | + Map<String, Object> payload = new HashMap<>(); | ||
| 41 | + payload.put("productKey", productKey); | ||
| 42 | + payload.put("deviceName", deviceName); | ||
| 43 | + LocalDateTime expireTime = LocalDateTimeUtils.addTime(gatewayProperties.getToken().getExpiration()); | ||
| 44 | + payload.put("exp", LocalDateTimeUtils.toEpochSecond(expireTime)); // 过期时间(exp 是 JWT 规范推荐) | ||
| 45 | + | ||
| 46 | + // 生成 JWT Token | ||
| 47 | + return JWTUtil.createToken(payload, gatewayProperties.getToken().getSecret().getBytes()); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @Override | ||
| 51 | + public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) { | ||
| 52 | + Assert.notBlank(token, "token 不能为空"); | ||
| 53 | + // 校验 JWT Token | ||
| 54 | + boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes()); | ||
| 55 | + if (!verify) { | ||
| 56 | + throw exception(DEVICE_TOKEN_EXPIRED); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + // 解析 Token | ||
| 60 | + JWT jwt = JWTUtil.parseToken(token); | ||
| 61 | + JSONObject payload = jwt.getPayloads(); | ||
| 62 | + // 检查过期时间 | ||
| 63 | + Long exp = payload.getLong("exp"); | ||
| 64 | + if (exp == null || exp > System.currentTimeMillis() / 1000) { | ||
| 65 | + throw exception(DEVICE_TOKEN_EXPIRED); | ||
| 66 | + } | ||
| 67 | + String productKey = payload.getStr("productKey"); | ||
| 68 | + String deviceName = payload.getStr("deviceName"); | ||
| 69 | + Assert.notBlank(productKey, "productKey 不能为空"); | ||
| 70 | + Assert.notBlank(deviceName, "deviceName 不能为空"); | ||
| 71 | + return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + @Override | ||
| 75 | + public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) { | ||
| 76 | + return IotDeviceAuthUtils.parseUsername(username); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/IotDeviceClientServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.service.device; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 6 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 7 | +import jakarta.annotation.PostConstruct; | ||
| 8 | +import jakarta.annotation.Resource; | ||
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 10 | +import org.springframework.boot.web.client.RestTemplateBuilder; | ||
| 11 | +import org.springframework.stereotype.Service; | ||
| 12 | +import org.springframework.web.client.RestTemplate; | ||
| 13 | + | ||
| 14 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * Iot 设备信息 Service 实现类:调用远程的 device http 接口,进行设备认证、设备获取等 | ||
| 18 | + * | ||
| 19 | + * @author 芋道源码 | ||
| 20 | + */ | ||
| 21 | +@Service | ||
| 22 | +@Slf4j | ||
| 23 | +public class IotDeviceClientServiceImpl implements IotDeviceCommonApi { | ||
| 24 | + | ||
| 25 | + @Resource | ||
| 26 | + private IotGatewayProperties gatewayProperties; | ||
| 27 | + | ||
| 28 | + private RestTemplate restTemplate; | ||
| 29 | + | ||
| 30 | + @PostConstruct | ||
| 31 | + public void init() { | ||
| 32 | + IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc(); | ||
| 33 | + restTemplate = new RestTemplateBuilder() | ||
| 34 | + .rootUri(rpc.getUrl() + "/rpc-api/iot/device/") | ||
| 35 | + .readTimeout(rpc.getReadTimeout()) | ||
| 36 | + .connectTimeout(rpc.getConnectTimeout()) | ||
| 37 | + .build(); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) { | ||
| 42 | + return doPost("auth", authReqDTO); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @SuppressWarnings("unchecked") | ||
| 46 | + private <T> CommonResult<Boolean> doPost(String url, T requestBody) { | ||
| 47 | + try { | ||
| 48 | + CommonResult<Boolean> result = restTemplate.postForObject(url, requestBody, | ||
| 49 | + (Class<CommonResult<Boolean>>) (Class<?>) CommonResult.class); | ||
| 50 | + log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); | ||
| 51 | + return result; | ||
| 52 | + } catch (Exception e) { | ||
| 53 | + log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); | ||
| 54 | + return CommonResult.error(INTERNAL_SERVER_ERROR); | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -22,6 +22,10 @@ yudao: | @@ -22,6 +22,10 @@ yudao: | ||
| 22 | url: http://127.0.0.1:48080 # 主程序 API 地址 | 22 | url: http://127.0.0.1:48080 # 主程序 API 地址 |
| 23 | connect-timeout: 30s | 23 | connect-timeout: 30s |
| 24 | read-timeout: 30s | 24 | read-timeout: 30s |
| 25 | + # 设备 Token 配置 | ||
| 26 | + token: | ||
| 27 | + secret: 1234567890123456789012345678901 | ||
| 28 | + expiration: 7d | ||
| 25 | 29 | ||
| 26 | # 协议配置 | 30 | # 协议配置 |
| 27 | protocol: | 31 | protocol: |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/auth/IotDeviceTokenServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.service.auth; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 4 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 5 | +import org.junit.jupiter.api.BeforeEach; | ||
| 6 | +import org.junit.jupiter.api.Test; | ||
| 7 | +import org.junit.jupiter.api.extension.ExtendWith; | ||
| 8 | +import org.mockito.InjectMocks; | ||
| 9 | +import org.mockito.Mock; | ||
| 10 | +import org.mockito.junit.jupiter.MockitoExtension; | ||
| 11 | + | ||
| 12 | +import java.time.Duration; | ||
| 13 | + | ||
| 14 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 15 | +import static org.mockito.Mockito.when; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * {@link IotDeviceTokenServiceImpl} 的单元测试 | ||
| 19 | + * | ||
| 20 | + * @author 芋道源码 | ||
| 21 | + */ | ||
| 22 | +@ExtendWith(MockitoExtension.class) | ||
| 23 | +class IotDeviceTokenServiceImplTest { | ||
| 24 | + | ||
| 25 | + @Mock | ||
| 26 | + private IotGatewayProperties gatewayProperties; | ||
| 27 | + | ||
| 28 | + @InjectMocks | ||
| 29 | + private IotDeviceTokenServiceImpl tokenService; | ||
| 30 | + | ||
| 31 | + private IotGatewayProperties.TokenProperties tokenProperties; | ||
| 32 | + | ||
| 33 | + @BeforeEach | ||
| 34 | + void setUp() { | ||
| 35 | + // 初始化 Token 配置 | ||
| 36 | + tokenProperties = new IotGatewayProperties.TokenProperties(); | ||
| 37 | + tokenProperties.setSecret("1234567890123456789012345678901"); | ||
| 38 | + tokenProperties.setExpiration(Duration.ofDays(7)); | ||
| 39 | + | ||
| 40 | + when(gatewayProperties.getToken()).thenReturn(tokenProperties); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @Test | ||
| 44 | + void testCreateToken_Success() { | ||
| 45 | + // 准备参数 | ||
| 46 | + String productKey = "testProduct"; | ||
| 47 | + String deviceName = "testDevice"; | ||
| 48 | + | ||
| 49 | + // 调用方法 | ||
| 50 | + String token = tokenService.createToken(productKey, deviceName); | ||
| 51 | + | ||
| 52 | + // 验证结果 | ||
| 53 | + assertNotNull(token); | ||
| 54 | + assertFalse(token.isEmpty()); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + @Test | ||
| 58 | + void testCreateToken_WithBlankParameters() { | ||
| 59 | + // 测试空白参数 | ||
| 60 | + assertNull(tokenService.createToken("", "deviceName")); | ||
| 61 | + assertNull(tokenService.createToken("productKey", "")); | ||
| 62 | + assertNull(tokenService.createToken(null, "deviceName")); | ||
| 63 | + assertNull(tokenService.createToken("productKey", null)); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Test | ||
| 67 | + void testCreateToken_WithoutConfig() { | ||
| 68 | + // 模拟配置为空 | ||
| 69 | + when(gatewayProperties.getToken()).thenReturn(null); | ||
| 70 | + | ||
| 71 | + // 调用方法 | ||
| 72 | + String token = tokenService.createToken("productKey", "deviceName"); | ||
| 73 | + | ||
| 74 | + // 验证结果 | ||
| 75 | + assertNull(token); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + @Test | ||
| 79 | + void testVerifyToken_Success() { | ||
| 80 | + // 准备参数 | ||
| 81 | + String productKey = "testProduct"; | ||
| 82 | + String deviceName = "testDevice"; | ||
| 83 | + | ||
| 84 | + // 创建 Token | ||
| 85 | + String token = tokenService.createToken(productKey, deviceName); | ||
| 86 | + assertNotNull(token); | ||
| 87 | + | ||
| 88 | + // 验证 Token | ||
| 89 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = tokenService.verifyToken(token); | ||
| 90 | + | ||
| 91 | + // 验证结果 | ||
| 92 | + assertNotNull(deviceInfo); | ||
| 93 | + assertEquals(productKey, deviceInfo.getProductKey()); | ||
| 94 | + assertEquals(deviceName, deviceInfo.getDeviceName()); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + @Test | ||
| 98 | + void testVerifyToken_WithBlankToken() { | ||
| 99 | + // 测试空白 Token | ||
| 100 | + assertNull(tokenService.verifyToken("")); | ||
| 101 | + assertNull(tokenService.verifyToken(null)); | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + @Test | ||
| 105 | + void testVerifyToken_WithInvalidToken() { | ||
| 106 | + // 测试无效 Token | ||
| 107 | + assertNull(tokenService.verifyToken("invalid.token.here")); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + @Test | ||
| 111 | + void testVerifyToken_WithoutConfig() { | ||
| 112 | + // 模拟配置为空 | ||
| 113 | + when(gatewayProperties.getToken()).thenReturn(null); | ||
| 114 | + | ||
| 115 | + // 调用方法 | ||
| 116 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = tokenService.verifyToken("any.token.here"); | ||
| 117 | + | ||
| 118 | + // 验证结果 | ||
| 119 | + assertNull(deviceInfo); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + @Test | ||
| 123 | + void testTokenRoundTrip() { | ||
| 124 | + // 测试完整的 Token 创建和验证流程 | ||
| 125 | + String productKey = "myProduct"; | ||
| 126 | + String deviceName = "myDevice"; | ||
| 127 | + | ||
| 128 | + // 1. 创建 Token | ||
| 129 | + String token = tokenService.createToken(productKey, deviceName); | ||
| 130 | + assertNotNull(token); | ||
| 131 | + | ||
| 132 | + // 2. 验证 Token | ||
| 133 | + IotDeviceAuthUtils.DeviceInfo deviceInfo = tokenService.verifyToken(token); | ||
| 134 | + assertNotNull(deviceInfo); | ||
| 135 | + assertEquals(productKey, deviceInfo.getProductKey()); | ||
| 136 | + assertEquals(deviceName, deviceInfo.getDeviceName()); | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | +} | ||
| 0 | \ No newline at end of file | 140 | \ No newline at end of file |
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
| 1 | -package cn.iocoder.yudao.module.iot.net.component.core.config; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.net.component.core.upstream.IotDeviceUpstreamClient; | ||
| 4 | -import org.springframework.boot.autoconfigure.AutoConfiguration; | ||
| 5 | -import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| 6 | -import org.springframework.context.annotation.Bean; | ||
| 7 | -import org.springframework.scheduling.annotation.EnableScheduling; | ||
| 8 | - | ||
| 9 | -/** | ||
| 10 | - * IoT 网络组件的通用自动配置类 | ||
| 11 | - * | ||
| 12 | - * @author haohao | ||
| 13 | - */ | ||
| 14 | -@EnableConfigurationProperties(IotNetComponentCommonProperties.class) | ||
| 15 | -public class IotNetComponentCommonAutoConfiguration { | ||
| 16 | - | ||
| 17 | - /** | ||
| 18 | - * 创建设备上行客户端 | ||
| 19 | - */ | ||
| 20 | - @Bean | ||
| 21 | - public IotDeviceUpstreamClient deviceUpstreamClient() { | ||
| 22 | - return new IotDeviceUpstreamClient(); | ||
| 23 | - } | ||
| 24 | - | ||
| 25 | -} | ||
| 26 | \ No newline at end of file | 0 | \ No newline at end of file |
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
| 1 | -package cn.iocoder.yudao.module.iot.net.component.core.upstream; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | -import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; | ||
| 5 | -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; | ||
| 6 | -import jakarta.annotation.Resource; | ||
| 7 | -import lombok.extern.slf4j.Slf4j; | ||
| 8 | -/** | ||
| 9 | - * 设备数据 Upstream 上行客户端 | ||
| 10 | - * <p> | ||
| 11 | - * 直接调用 IotDeviceUpstreamApi 接口 | ||
| 12 | - * | ||
| 13 | - * @author haohao | ||
| 14 | - */ | ||
| 15 | -@Slf4j | ||
| 16 | -public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { | ||
| 17 | - | ||
| 18 | - @Resource | ||
| 19 | - private IotDeviceUpstreamApi deviceUpstreamApi; | ||
| 20 | - | ||
| 21 | - @Override | ||
| 22 | - public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { | ||
| 23 | - return deviceUpstreamApi.authenticateEmqxConnection(authReqDTO); | ||
| 24 | - } | ||
| 25 | - | ||
| 26 | -} |
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
| 1 | -package cn.iocoder.yudao.module.iot.net.component.core.util; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 4 | -import cn.iocoder.yudao.module.iot.net.component.core.pojo.IotStandardResponse; | ||
| 5 | -import io.vertx.core.http.HttpHeaders; | ||
| 6 | -import io.vertx.ext.web.RoutingContext; | ||
| 7 | -import org.springframework.http.MediaType; | ||
| 8 | - | ||
| 9 | -/** | ||
| 10 | - * IoT 网络组件的通用工具类 | ||
| 11 | - * | ||
| 12 | - * @author 芋道源码 | ||
| 13 | - */ | ||
| 14 | -public class IotNetComponentCommonUtils { | ||
| 15 | - | ||
| 16 | - /** | ||
| 17 | - * 将对象转换为JSON字符串后写入HTTP响应 | ||
| 18 | - * | ||
| 19 | - * @param routingContext 路由上下文 | ||
| 20 | - * @param data 数据对象 | ||
| 21 | - */ | ||
| 22 | - @SuppressWarnings("deprecation") | ||
| 23 | - public static void writeJsonResponse(RoutingContext routingContext, Object data) { | ||
| 24 | - routingContext.response() | ||
| 25 | - .setStatusCode(200) | ||
| 26 | - .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) | ||
| 27 | - .end(JsonUtils.toJsonString(data)); | ||
| 28 | - } | ||
| 29 | - | ||
| 30 | - /** | ||
| 31 | - * 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse) | ||
| 32 | - * <p> | ||
| 33 | - * 推荐使用此方法,统一 MQTT 和 HTTP 的响应格式。使用方式: | ||
| 34 | - * | ||
| 35 | - * <pre> | ||
| 36 | - * // 成功响应 | ||
| 37 | - * IotStandardResponse response = IotStandardResponse.success(requestId, method, data); | ||
| 38 | - * IotNetComponentCommonUtils.writeJsonResponse(routingContext, response); | ||
| 39 | - * | ||
| 40 | - * // 错误响应 | ||
| 41 | - * IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message); | ||
| 42 | - * IotNetComponentCommonUtils.writeJsonResponse(routingContext, errorResponse); | ||
| 43 | - * </pre> | ||
| 44 | - * | ||
| 45 | - * @param routingContext 路由上下文 | ||
| 46 | - * @param response IotStandardResponse 响应对象 | ||
| 47 | - */ | ||
| 48 | - @SuppressWarnings("deprecation") | ||
| 49 | - public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) { | ||
| 50 | - routingContext.response() | ||
| 51 | - .setStatusCode(200) | ||
| 52 | - .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) | ||
| 53 | - .end(JsonUtils.toJsonString(response)); | ||
| 54 | - } | ||
| 55 | - | ||
| 56 | -} | ||
| 57 | \ No newline at end of file | 0 | \ No newline at end of file |
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
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; | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; | 4 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; |
| 5 | -import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO; | 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceEmqxAuthReqDTO; |
| 6 | import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils; | 6 | import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils; |
| 7 | import io.vertx.core.Handler; | 7 | import io.vertx.core.Handler; |
| 8 | import io.vertx.core.json.JsonObject; | 8 | import io.vertx.core.json.JsonObject; |
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
| 1 | -package cn.iocoder.yudao.module.iot.net.component.server.upstream; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | -import cn.iocoder.yudao.module.iot.net.component.server.config.IotNetComponentServerProperties; | ||
| 5 | -import lombok.RequiredArgsConstructor; | ||
| 6 | -import lombok.extern.slf4j.Slf4j; | ||
| 7 | -import org.springframework.web.client.RestTemplate; | ||
| 8 | - | ||
| 9 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 10 | - | ||
| 11 | -/** | ||
| 12 | - * 组件上行客户端,用于向主程序上报设备数据 | ||
| 13 | - * <p> | ||
| 14 | - * 通过 HTTP 调用远程的 IotDeviceUpstreamApi 接口 | ||
| 15 | - * | ||
| 16 | - * @author haohao | ||
| 17 | - */ | ||
| 18 | -@RequiredArgsConstructor | ||
| 19 | -@Slf4j | ||
| 20 | -public class IotComponentUpstreamClient { | ||
| 21 | - | ||
| 22 | - public static final String URL_PREFIX = "/rpc-api/iot/device/upstream"; | ||
| 23 | - | ||
| 24 | - private final IotNetComponentServerProperties properties; | ||
| 25 | - | ||
| 26 | - private final RestTemplate restTemplate; | ||
| 27 | - | ||
| 28 | -// @Override | ||
| 29 | -// public CommonResult<Boolean> updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { | ||
| 30 | -// String url = properties.getUpstreamUrl() + URL_PREFIX + "/update-state"; | ||
| 31 | -// return doPost(url, updateReqDTO); | ||
| 32 | -// } | ||
| 33 | - | ||
| 34 | - @SuppressWarnings("unchecked") | ||
| 35 | - private <T> CommonResult<Boolean> doPost(String url, T requestBody) { | ||
| 36 | - try { | ||
| 37 | - CommonResult<Boolean> result = restTemplate.postForObject(url, requestBody, | ||
| 38 | - (Class<CommonResult<Boolean>>) (Class<?>) CommonResult.class); | ||
| 39 | - log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); | ||
| 40 | - return result; | ||
| 41 | - } catch (Exception e) { | ||
| 42 | - log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); | ||
| 43 | - return CommonResult.error(INTERNAL_SERVER_ERROR); | ||
| 44 | - } | ||
| 45 | - } | ||
| 46 | -} | ||
| 47 | \ No newline at end of file | 0 | \ No newline at end of file |