Commit 643cc4cfd2fa1717842966ee7b97a927ba6e8f89

Authored by YunaiV
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
1 -/**  
2 - * TODO 芋艿:占位  
3 - */  
4 -package cn.iocoder.yudao.module.iot.api.device.dto;  
5 \ 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/package-info.java deleted
1 -/**  
2 - * 占位  
3 - *  
4 - * TODO 芋艿:后续删除  
5 - */  
6 -package cn.iocoder.yudao.module.iot.api;  
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
1 /** 1 /**
2 - * 占位  
3 - *  
4 - * TODO 芋艿:后续删除 2 + * iot API 包,定义并实现提供给其它模块的 API
5 */ 3 */
6 package cn.iocoder.yudao.module.iot.api; 4 package cn.iocoder.yudao.module.iot.api;
7 \ No newline at end of file 5 \ No newline at end of file
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
1 -/**  
2 - * 占位  
3 - */  
4 -package cn.iocoder.yudao.module.iot.framework.security.core;  
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&lt;RoutingContext&gt; { @@ -54,47 +52,35 @@ public class IotHttpUpstreamHandler implements Handler&lt;RoutingContext&gt; {
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&lt;RoutingContext&gt; { @@ -169,7 +155,6 @@ public class IotHttpUpstreamHandler implements Handler&lt;RoutingContext&gt; {
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&lt;RoutingContext&gt; { @@ -196,20 +181,6 @@ public class IotHttpUpstreamHandler implements Handler&lt;RoutingContext&gt; {
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
1 -cn.iocoder.yudao.module.iot.net.component.core.config.IotNetComponentCommonAutoConfiguration  
2 \ 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-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