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 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.date.DatePattern; |
| 5 | 5 | import cn.hutool.core.date.LocalDateTimeUtil; |
| 6 | +import cn.hutool.core.date.TemporalAccessorUtil; | |
| 6 | 7 | import cn.hutool.core.lang.Assert; |
| 7 | 8 | import cn.hutool.core.util.StrUtil; |
| 8 | 9 | import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum; |
| ... | ... | @@ -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 | 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 | 39 | ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); |
| 40 | 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 | 42 | // ========== 固件相关 1-050-008-000 ========== |
| 55 | 43 | |
| 56 | 44 | ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); |
| ... | ... | @@ -66,16 +54,10 @@ public interface ErrorCodeConstants { |
| 66 | 54 | ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); |
| 67 | 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 | 57 | // ========== IoT 数据桥梁 1-050-010-000 ========== |
| 73 | 58 | ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); |
| 74 | 59 | |
| 75 | 60 | // ========== IoT 场景联动 1-050-011-000 ========== |
| 76 | 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 | 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 | 1 | package cn.iocoder.yudao.module.iot.api.device; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.enums.RpcConstants; | |
| 3 | 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 | 8 | import jakarta.annotation.Resource; |
| 9 | +import jakarta.annotation.security.PermitAll; | |
| 7 | 10 | import org.springframework.context.annotation.Primary; |
| 8 | 11 | import org.springframework.validation.annotation.Validated; |
| 12 | +import org.springframework.web.bind.annotation.PostMapping; | |
| 9 | 13 | import org.springframework.web.bind.annotation.RestController; |
| 10 | 14 | |
| 11 | 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 | 22 | @RestController |
| 17 | 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 | 27 | @Resource |
| 22 | - private IotDeviceUpstreamService deviceUpstreamService; | |
| 28 | + private IotDeviceService deviceService; | |
| 23 | 29 | |
| 24 | 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 | 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 | 121 | */ |
| 122 | 122 | private String firmwareId; |
| 123 | 123 | |
| 124 | - // TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有 | |
| 125 | 124 | /** |
| 126 | - * 设备密钥,用于设备认证,需安全存储 | |
| 125 | + * 设备密钥,用于设备认证 | |
| 127 | 126 | */ |
| 128 | 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 | 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 | 9 | */ |
| 10 | 10 | public interface RedisKeyConstants { |
| 11 | 11 | |
| 12 | + // TODO @芋艿:弱化 deviceKey;使用 product_key + device_name 替代 | |
| 12 | 13 | /** |
| 13 | 14 | * 设备属性的数据缓存,采用 HASH 结构 |
| 14 | 15 | * <p> |
| 15 | - * KEY 格式:device_property:{productKey},${deviceName} | |
| 16 | + * KEY 格式:device_property:{deviceKey} | |
| 16 | 17 | * HASH KEY:identifier 属性标识 |
| 17 | 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 | 23 | * 设备的最后上报时间,采用 ZSET 结构 |
| ... | ... | @@ -27,6 +28,15 @@ public interface RedisKeyConstants { |
| 27 | 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 | 40 | * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) |
| 31 | 41 | * |
| 32 | 42 | * KEY 格式:device_${productKey}_${deviceName} |
| ... | ... | @@ -42,13 +52,4 @@ public interface RedisKeyConstants { |
| 42 | 52 | */ |
| 43 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; |
| 5 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; |
| 7 | 8 | import jakarta.validation.Valid; |
| ... | ... | @@ -228,4 +229,12 @@ public interface IotDeviceService { |
| 228 | 229 | */ |
| 229 | 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 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | 6 | import cn.hutool.core.util.RandomUtil; |
| 6 | 7 | import cn.hutool.core.util.StrUtil; |
| 7 | 8 | import cn.hutool.extra.spring.SpringUtil; |
| ... | ... | @@ -12,16 +13,16 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; |
| 12 | 13 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; |
| 13 | 14 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 14 | 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 | 18 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 16 | 19 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; |
| 17 | 20 | import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; |
| 18 | 21 | import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; |
| 19 | 22 | import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; |
| 20 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | |
| 21 | 23 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 22 | 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 | 26 | import jakarta.annotation.Resource; |
| 26 | 27 | import jakarta.validation.ConstraintViolationException; |
| 27 | 28 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -397,15 +398,17 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 397 | 398 | return respVO; |
| 398 | 399 | } |
| 399 | 400 | |
| 401 | + // TODO @芋艿:改成通用的; | |
| 400 | 402 | @Override |
| 401 | 403 | public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { |
| 402 | 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 | 414 | private void deleteDeviceCache(IotDeviceDO device) { |
| ... | ... | @@ -459,4 +462,29 @@ public class IotDeviceServiceImpl implements IotDeviceService { |
| 459 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; |
| 5 | 4 | import jakarta.validation.Valid; |
| 6 | 5 | |
| ... | ... | @@ -48,11 +47,4 @@ public interface IotDeviceUpstreamService { |
| 48 | 47 | // */ |
| 49 | 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 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.util.IdUtil; |
| 5 | 5 | import cn.hutool.core.util.ObjUtil; |
| 6 | -import cn.hutool.core.util.StrUtil; | |
| 7 | 6 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; |
| 9 | 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 | 11 | import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; |
| 13 | 12 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 14 | 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 | 14 | import jakarta.annotation.Resource; |
| 18 | 15 | import lombok.extern.slf4j.Slf4j; |
| 19 | 16 | import org.springframework.stereotype.Service; |
| ... | ... | @@ -222,37 +219,4 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { |
| 222 | 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 | 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 | 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 | 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 | 3 | import jakarta.validation.constraints.NotEmpty; |
| 4 | 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 | 9 | * @author 芋道源码 |
| 12 | 10 | */ |
| 13 | 11 | @Data |
| 14 | -public class IotDeviceEmqxAuthReqDTO { | |
| 12 | +public class IotDeviceAuthReqDTO { | |
| 15 | 13 | |
| 16 | 14 | /** |
| 17 | 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
yudao-module-iot/yudao-module-iot-gateway/pom.xml
| ... | ... | @@ -23,6 +23,11 @@ |
| 23 | 23 | <version>${revision}</version> |
| 24 | 24 | </dependency> |
| 25 | 25 | |
| 26 | + <dependency> | |
| 27 | + <groupId>org.springframework</groupId> | |
| 28 | + <artifactId>spring-web</artifactId> | |
| 29 | + </dependency> | |
| 30 | + | |
| 26 | 31 | <!-- 消息队列相关 --> |
| 27 | 32 | <dependency> |
| 28 | 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 | 24 | public static class HttpProtocolConfiguration { |
| 25 | 25 | |
| 26 | 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 | 31 | @Bean | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | +import jakarta.validation.constraints.NotEmpty; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 3 | 5 | import lombok.Data; |
| 4 | 6 | import org.springframework.boot.context.properties.ConfigurationProperties; |
| 5 | 7 | import org.springframework.validation.annotation.Validated; |
| 6 | 8 | |
| 9 | +import java.time.Duration; | |
| 7 | 10 | import java.util.List; |
| 8 | 11 | |
| 9 | 12 | @ConfigurationProperties(prefix = "yudao.iot.gateway") |
| ... | ... | @@ -15,6 +18,10 @@ public class IotGatewayProperties { |
| 15 | 18 | * 设备 RPC 服务配置 |
| 16 | 19 | */ |
| 17 | 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 | 34 | /** |
| 28 | 35 | * 主程序 API 地址 |
| 29 | 36 | */ |
| 37 | + @NotEmpty(message = "主程序 API 地址不能为空") | |
| 30 | 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 | 86 | /** |
| 61 | 87 | * 是否开启 |
| 62 | 88 | */ |
| 89 | + @NotNull(message = "是否开启不能为空") | |
| 63 | 90 | private Boolean enabled; |
| 64 | 91 | /** |
| 65 | 92 | * 服务端口 |
| ... | ... | @@ -74,6 +101,7 @@ public class IotGatewayProperties { |
| 74 | 101 | /** |
| 75 | 102 | * 是否开启 |
| 76 | 103 | */ |
| 104 | + @NotNull(message = "是否开启不能为空") | |
| 77 | 105 | private Boolean enabled; |
| 78 | 106 | /** |
| 79 | 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 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.http; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | |
| 4 | 3 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 5 | 4 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; | |
| 6 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; |
| 7 | 7 | import io.vertx.core.AbstractVerticle; |
| 8 | 8 | import io.vertx.core.Vertx; |
| ... | ... | @@ -25,8 +25,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { |
| 25 | 25 | |
| 26 | 26 | private final IotGatewayProperties.HttpProperties httpProperties; |
| 27 | 27 | |
| 28 | - private final IotDeviceMessageProducer deviceMessageProducer; | |
| 29 | - | |
| 30 | 28 | private HttpServer httpServer; |
| 31 | 29 | |
| 32 | 30 | @Override |
| ... | ... | @@ -38,10 +36,11 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { |
| 38 | 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 | 40 | router.post(IotHttpUpstreamHandler.PROPERTY_PATH).handler(upstreamHandler); |
| 44 | 41 | router.post(IotHttpUpstreamHandler.EVENT_PATH).handler(upstreamHandler); |
| 42 | + IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); | |
| 43 | + router.post(IotHttpAuthHandler.PATH).handler(authHandler); | |
| 45 | 44 | |
| 46 | 45 | // 启动 HTTP 服务器 |
| 47 | 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 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.hutool.core.map.MapUtil; |
| 5 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.hutool.extra.spring.SpringUtil; | |
| 6 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | 9 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; |
| ... | ... | @@ -16,11 +17,8 @@ import lombok.extern.slf4j.Slf4j; |
| 16 | 17 | |
| 17 | 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 | 23 | * @author 芋道源码 |
| 26 | 24 | */ |
| ... | ... | @@ -54,47 +52,35 @@ public class IotHttpUpstreamHandler implements Handler<RoutingContext> { |
| 54 | 52 | private static final String EVENT_METHOD_SUFFIX = ".post"; |
| 55 | 53 | |
| 56 | 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 | 155 | // |
| 170 | 156 | // // 事件上报 |
| 171 | 157 | // CommonResult<Boolean> result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); |
| 172 | -// String method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX; | |
| 173 | 158 | // |
| 174 | 159 | // // 返回响应 |
| 175 | 160 | // sendResponse(routingContext, requestId, method, result); |
| ... | ... | @@ -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 | 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
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 | 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 | 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 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | 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 | 6 | import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils; |
| 7 | 7 | import io.vertx.core.Handler; |
| 8 | 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 | 0 | \ No newline at end of file |