feat(iot):【网关设备:70%】动态注册的初步实现(未测试),基于 stateful-sauteeing-pillow.md 规划

This commit is contained in:
YunaiV
2026-01-25 11:16:07 +08:00
parent 1309be39c3
commit 38a21ad59c
22 changed files with 663 additions and 94 deletions

View File

@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -57,4 +59,11 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
}));
}
@Override
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
@PermitAll
public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) {
return success(deviceService.registerDevice(reqDTO));
}
}

View File

@@ -27,6 +27,12 @@ public class IotProductRespVO {
@ExcelProperty("产品标识")
private String productKey;
@Schema(description = "产品密钥", requiredMode = Schema.RequiredMode.REQUIRED)
private String productSecret;
@Schema(description = "是否开启动态注册", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean registerEnabled;
@Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long categoryId;

View File

@@ -48,4 +48,8 @@ public class IotProductSaveReqVO {
@NotEmpty(message = "数据格式不能为空")
private String codecType;
@Schema(description = "是否开启动态注册", example = "false")
@NotEmpty(message = "是否开启动态注册不能为空")
private Boolean registerEnabled;
}

View File

@@ -32,6 +32,14 @@ public class IotProductDO extends TenantBaseDO {
* 产品标识
*/
private String productKey;
/**
* 产品密钥,用于一型一密动态注册
*/
private String productSecret;
/**
* 是否开启动态注册
*/
private Boolean registerEnabled;
/**
* 产品分类编号
* <p>

View File

@@ -40,9 +40,12 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
// 设备注册相关错误码 1-050-003-200
// 设备注册相关错误码 1-050-003-200
ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
ErrorCode DEVICE_REGISTER_SIGN_INVALID = new ErrorCode(1_050_003_211, "动态注册签名验证失败");
ErrorCode DEVICE_ALREADY_ACTIVATED = new ErrorCode(1_050_003_212, "设备已激活,不允许重复注册");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");

View File

@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
@@ -292,7 +294,7 @@ public interface IotDeviceService {
*/
List<IotDeviceDO> getDeviceListByHasLocation();
// ========== 网关-子设备绑定相关 ==========
// ========== 网关-拓扑管理(后台操作) ==========
/**
* 绑定子设备到网关
@@ -354,6 +356,8 @@ public interface IotDeviceService {
*/
IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice);
// ========== 设备动态注册 ==========
/**
* 处理子设备动态注册消息(网关设备上报)
*
@@ -363,4 +367,12 @@ public interface IotDeviceService {
*/
List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
/**
* 设备动态注册(直连设备/网关)
*
* @param reqDTO 动态注册请求
* @return 注册结果(包含 DeviceSecret
*/
IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
}

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.iot.service.device;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
@@ -18,6 +18,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
@@ -77,6 +79,10 @@ public class IotDeviceServiceImpl implements IotDeviceService {
@Lazy // 延迟加载,解决循环依赖
private IotDeviceMessageService deviceMessageService;
private IotDeviceServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
@Override
public Long createDevice(IotDeviceSaveReqVO createReqVO) {
return createDevice0(createReqVO).getId();
@@ -138,7 +144,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
device.setProductId(product.getId()).setProductKey(product.getProductKey())
.setDeviceType(product.getDeviceType());
// 生成密钥
device.setDeviceSecret(generateDeviceSecret());
device.setDeviceSecret(IotDeviceAuthUtils.generateDeviceSecret());
// 设置设备状态为未激活
device.setState(IotDeviceStateEnum.INACTIVE.getState());
}
@@ -368,15 +374,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectCountByGroupId(groupId);
}
/**
* 生成 deviceSecret
*
* @return 生成的 deviceSecret
*/
private String generateDeviceSecret() {
return IdUtil.fastSimpleUUID();
}
@Override
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
@@ -566,7 +563,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectListByHasLocation();
}
// ========== 网关-子设备绑定相关 ==========
// ========== 网关-拓扑管理(后台操作) ==========
@Override
@Transactional(rollbackFor = Exception.class)
@@ -766,77 +763,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities);
}
@Override
public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
// 1.1 校验网关设备类型
if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
throw exception(DEVICE_NOT_GATEWAY);
}
// 1.2 解析参数
if (!(message.getParams() instanceof List)) {
throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
}
List<IotSubDeviceRegisterReqDTO> paramsList = JsonUtils.convertList(message.getParams(),
IotSubDeviceRegisterReqDTO.class);
if (CollUtil.isEmpty(paramsList)) {
throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
}
// 2. 遍历注册每个子设备
List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(paramsList.size());
for (IotSubDeviceRegisterReqDTO params : paramsList) {
try {
IotDeviceDO device = registerSubDevice(gatewayDevice, params);
results.add(new IotSubDeviceRegisterRespDTO(
params.getProductKey(), params.getDeviceName(), device.getDeviceSecret()));
} catch (Exception ex) {
log.error("[handleSubDeviceRegisterMessage][子设备({}/{}) 注册失败]",
params.getProductKey(), params.getDeviceName(), ex);
}
}
// 3. 返回响应数据(包含成功注册的子设备列表)
return results;
}
private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
// 1.1 校验产品
IotProductDO product = productService.getProductByProductKey(params.getProductKey());
if (product == null) {
throw exception(PRODUCT_NOT_EXISTS);
}
// 1.2 校验产品是否为网关子设备类型
if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
}
// 1.3 查找设备是否已存在
IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName());
if (existDevice != null) {
// 校验是否绑定到当前网关
if (ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) {
throw exception(DEVICE_GATEWAY_BINDTO_EXISTS,
existDevice.getProductKey(), existDevice.getDeviceName());
}
// 已存在则返回设备信息
return existDevice;
}
// 2. 创建新设备
IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
.setDeviceName(params.getDeviceName())
.setProductId(product.getId())
.setGatewayId(gatewayDevice.getId());
IotDeviceDO newDevice = createDevice0(createReqVO);
log.info("[registerSubDevice][网关({}/{}) 注册子设备({}/{})]",
gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
newDevice.getProductKey(), newDevice.getDeviceName());
return newDevice;
}
private IotDeviceServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
/**
* 发送拓扑变更通知给网关设备
*
@@ -875,4 +801,107 @@ public class IotDeviceServiceImpl implements IotDeviceService {
}
}
// ========== 设备动态注册 ==========
@Override
public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) {
// 1.1 校验产品
IotProductDO product = productService.getProductByProductKey(reqDTO.getProductKey());
if (product == null) {
throw exception(PRODUCT_NOT_EXISTS);
}
// 1.2 校验产品是否开启动态注册
if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
throw exception(DEVICE_REGISTER_DISABLED);
}
// 1.3 验证签名
if (!IotDeviceAuthUtils.verifyRegisterSign(product.getProductSecret(),
reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getRandom(), reqDTO.getSign())) {
throw exception(DEVICE_REGISTER_SIGN_INVALID);
}
// 4. 查找设备(预注册模式:设备必须已存在)
// TODO @AI设备不用提前有这个有问题
IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName());
if (device == null) {
throw exception(DEVICE_NOT_EXISTS);
}
// 5. 校验设备是否已激活(已激活的设备不允许重复注册)
if (!Objects.equals(device.getState(), IotDeviceStateEnum.INACTIVE.getState())) {
throw exception(DEVICE_ALREADY_ACTIVATED);
}
// 6. 返回设备密钥
return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
}
@Override
public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
// 1.1 校验网关设备类型
if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
throw exception(DEVICE_NOT_GATEWAY);
}
// 1.2 解析参数
if (!(message.getParams() instanceof List)) {
throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
}
List<IotSubDeviceRegisterReqDTO> paramsList = JsonUtils.convertList(message.getParams(),
IotSubDeviceRegisterReqDTO.class);
if (CollUtil.isEmpty(paramsList)) {
throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
}
// 2. 遍历注册每个子设备
List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(paramsList.size());
for (IotSubDeviceRegisterReqDTO params : paramsList) {
try {
IotDeviceDO device = registerSubDevice(gatewayDevice, params);
results.add(new IotSubDeviceRegisterRespDTO(
params.getProductKey(), params.getDeviceName(), device.getDeviceSecret()));
} catch (Exception ex) {
log.error("[handleSubDeviceRegisterMessage][子设备({}/{}) 注册失败]",
params.getProductKey(), params.getDeviceName(), ex);
}
}
// 3. 返回响应数据(包含成功注册的子设备列表)
return results;
}
// TODO @AI阿里云的设备必须存在
private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
// 1.1 校验产品
IotProductDO product = productService.getProductByProductKey(params.getProductKey());
if (product == null) {
throw exception(PRODUCT_NOT_EXISTS);
}
// 1.2 校验产品是否为网关子设备类型
if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
}
// 1.3 查找设备是否已存在
IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName());
if (existDevice != null) {
// 校验是否绑定到当前网关
if (ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) {
throw exception(DEVICE_GATEWAY_BINDTO_EXISTS,
existDevice.getProductKey(), existDevice.getDeviceName());
}
// 已存在则返回设备信息
return existDevice;
}
// 2. 创建新设备
IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
.setDeviceName(params.getDeviceName())
.setProductId(product.getId())
.setGatewayId(gatewayDevice.getId());
IotDeviceDO newDevice = createDevice0(createReqVO);
log.info("[registerSubDevice][网关({}/{}) 注册子设备({}/{})]",
gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
newDevice.getProductKey(), newDevice.getDeviceName());
return newDevice;
}
}

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO;
@@ -53,7 +54,8 @@ public class IotProductServiceImpl implements IotProductService {
// 2. 插入
IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
.setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus());
.setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
.setProductSecret(IotDeviceAuthUtils.generateProductSecret());
productMapper.insert(product);
return product.getId();
}

View File

@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
/**
* IoT 设备通用 API
@@ -28,4 +30,12 @@ public interface IotDeviceCommonApi {
*/
CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
/**
* 设备动态注册(一型一密)
*
* @param reqDTO 动态注册请求
* @return 注册结果(包含 DeviceSecret
*/
CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
}

View File

@@ -33,9 +33,10 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
// ========== 设备注册 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/register-devices
// 可参考https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
SUB_DEVICE_REGISTER("thing.sub.register", "设备动态注册", true),
DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
// ========== 设备属性 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
/**
* IoT 设备动态注册 Request DTO
* <p>
* 用于直连设备/网关的一型一密动态注册:使用 ProductSecret 验证签名,返回 DeviceSecret
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@Data
public class IotDeviceRegisterReqDTO {
/**
* 产品标识
*/
@NotEmpty(message = "产品标识不能为空")
private String productKey;
/**
* 设备名称
*/
@NotEmpty(message = "设备名称不能为空")
private String deviceName;
// TODO @AI可以去掉 random 字段;
/**
* 随机数,用于签名
*/
@NotEmpty(message = "随机数不能为空")
private String random;
// TODO @AI看起来是直接带 productSecret 阿里云上,你在检查下!
/**
* 签名
*/
@NotEmpty(message = "签名不能为空")
private String sign;
}

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 设备动态注册 Response DTO
* <p>
* 用于直连设备/网关的一型一密动态注册响应
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceRegisterRespDTO {
/**
* 产品标识
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 设备密钥
*/
private String deviceSecret;
}

View File

@@ -6,10 +6,12 @@ import lombok.Data;
/**
* IoT 子设备动态注册 Request DTO
* <p>
* 用于 thing.sub.register 消息的 params 数组元素
* 用于 thing.auth.register.sub 消息的 params 数组元素
*
* 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
*
* @author 芋道源码
* @see <a href="http://help.aliyun.com/zh/marketplace/dynamic-registration-of-sub-devices">阿里云 - 动态注册子设备</a>
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
@Data
public class IotSubDeviceRegisterReqDTO {

View File

@@ -7,10 +7,10 @@ import lombok.NoArgsConstructor;
/**
* IoT 子设备动态注册 Response DTO
* <p>
* 用于 thing.sub.register 响应的设备信息
* 用于 thing.auth.register.sub 响应的设备信息
*
* @author 芋道源码
* @see <a href="http://help.aliyun.com/zh/marketplace/dynamic-registration-of-sub-devices">阿里云 - 动态注册子设备</a>
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
@Data
@NoArgsConstructor

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
@@ -87,4 +88,62 @@ public class IotDeviceAuthUtils {
return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]);
}
// ========== 动态注册相关方法 ==========
// TODO @AI想了下还是放回到对应的 productService、deviceService 更合适;
/**
* 生成产品密钥
*
* @return 产品密钥UUID
*/
public static String generateProductSecret() {
return IdUtil.fastSimpleUUID();
}
/**
* 生成设备密钥
*
* @return 设备密钥UUID
*/
public static String generateDeviceSecret() {
return IdUtil.fastSimpleUUID();
}
// TODO @AI去掉 random
/**
* 计算动态注册签名
* <p>
* 参考阿里云规范,参数按字典序排列拼接
*
* @param productSecret 产品密钥
* @param productKey 产品标识
* @param deviceName 设备名称
* @param random 随机数
* @return 签名
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">一型一密</a>
*/
public static String buildRegisterSign(String productSecret, String productKey, String deviceName, String random) {
String content = "deviceName" + deviceName + "productKey" + productKey + "random" + random;
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
.digestHex(content);
}
// TODO @AI是不是调用方自己验证就好了不要这里面抽
/**
* 验证动态注册签名
*
* @param productSecret 产品密钥
* @param productKey 产品标识
* @param deviceName 设备名称
* @param random 随机数
* @param sign 待验证的签名
* @return 是否验证通过
*/
public static boolean verifyRegisterSign(String productSecret, String productKey,
String deviceName, String random, String sign) {
String expectedSign = buildRegisterSign(productSecret, productKey, deviceName, random);
return expectedSign.equals(sign);
}
}

View File

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
@@ -47,6 +49,10 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
// 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(this);
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);

View File

@@ -7,6 +7,7 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import io.vertx.core.Handler;
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
private void beforeHandle(RoutingContext context) {
// 如果不需要认证,则不走前置处理
String path = context.request().path();
if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) {
if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
return;
}

View File

@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* IoT 网关 HTTP 协议的【设备动态注册】处理器
* <p>
* 用于直连设备/网关的一型一密动态注册,不需要认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
public static final String PATH = "/auth/register/device";
private final IotDeviceCommonApi deviceApi;
public IotHttpRegisterHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
// TODO @AI参数不太对看看我写的建议
JsonObject body = context.body().asJsonObject();
String productKey = body.getString("productKey");
if (StrUtil.isEmpty(productKey)) {
throw invalidParamException("productKey 不能为空");
}
String deviceName = body.getString("deviceName");
if (StrUtil.isEmpty(deviceName)) {
throw invalidParamException("deviceName 不能为空");
}
String random = body.getString("random");
if (StrUtil.isEmpty(random)) {
throw invalidParamException("random 不能为空");
}
String sign = body.getString("sign");
if (StrUtil.isEmpty(sign)) {
throw invalidParamException("sign 不能为空");
}
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey)
.setDeviceName(deviceName)
.setRandom(random)
.setSign(sign);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
result.checkError();
// 3. 返回结果
return success(result.getData());
}
}

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* IoT 网关 HTTP 协议的【子设备动态注册】处理器
* <p>
* 用于子设备的动态注册,需要网关认证
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
*/
public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
/**
* 路径:/auth/register/sub-device/:productKey/:deviceName
* <p>
* productKey 和 deviceName 是网关设备的标识
*/
public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
private final IotHttpUpstreamProtocol protocol;
private final IotDeviceMessageService deviceMessageService;
public IotHttpRegisterSubHandler(IotHttpUpstreamProtocol protocol) {
this.protocol = protocol;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析通用参数
String productKey = context.pathParam("productKey");
String deviceName = context.pathParam("deviceName");
// 2.1 解析消息
byte[] bytes = context.body().buffer().getBytes();
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, productKey, deviceName);
// 2.2 设置方法
message.setMethod(IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod());
// TODO @AI可能还是需要一个新的 deviceApi 接口。因为 register sub 子设备不太一行;
// 2.3 发送消息
Object responseData = deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
// 3. 返回结果
return success(responseData);
}
}

View File

@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
@@ -54,6 +56,11 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { });
}
@Override
public CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO) {
return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { });
}
private <T, R> CommonResult<R> doPost(String url, T body,
ParameterizedTypeReference<CommonResult<R>> responseType) {
try {

View File

@@ -0,0 +1,203 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.util.Collections;
// TODO @AI合并到 IotDirectDeviceHttpProtocolIntegrationTest 里呀,没必要拆开;只搞一个直连设备的注册就好了;
/**
* IoT 设备动态注册 HTTP 协议集成测试(手动测试)
*
* <p>测试场景一型一密One Type One Secret动态注册机制
*
* <p><b>前置条件:</b>
* <ol>
* <li>产品已开启动态注册registerEnabled = true</li>
* <li>设备已预先创建(预注册模式)</li>
* <li>设备 deviceSecret 为空(未激活状态)</li>
* </ol>
*
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务HTTP 端口 8092</li>
* <li>运行 {@link #testDeviceRegister()} 测试直连设备/网关动态注册</li>
* <li>运行 {@link #testSubDeviceRegister()} 测试子设备动态注册(需要先获取网关 Token</li>
* </ol>
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
*/
@Slf4j
@SuppressWarnings("HttpUrlsUsage")
public class IotDeviceRegisterHttpProtocolIntegrationTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 8092;
// ===================== 直连设备/网关动态注册配置(根据实际情况修改) =====================
/**
* 产品 Key需要开启动态注册
*/
private static final String PRODUCT_KEY = "4aymZgOTOOCrDKRT";
/**
* 产品密钥(从 iot_product 表的 product_secret 字段获取)
*/
private static final String PRODUCT_SECRET = "your_product_secret";
/**
* 设备名称需要预先创建deviceSecret 为空)
*/
private static final String DEVICE_NAME = "test-register-device";
// ===================== 网关设备信息(用于子设备动态注册) =====================
private static final String GATEWAY_PRODUCT_KEY = "m6XcS1ZJ3TW8eC0v";
private static final String GATEWAY_DEVICE_NAME = "sub-ddd";
private static final String GATEWAY_DEVICE_SECRET = "b3d62c70f8a4495487ed1d35d61ac2b3";
/**
* 网关设备 Token从网关认证获取后粘贴到这里
*/
private static final String GATEWAY_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoibTZYY1MxWkozVFc4ZUMwdiIsImV4cCI6MTc2OTg2NjY3OCwiZGV2aWNlTmFtZSI6InN1Yi1kZGQifQ.nCLSAfHEjXLtTDRXARjOoFqpuo5WfArjFWweUAzrjKU";
// ===================== 子设备信息(用于子设备动态注册) =====================
private static final String SUB_DEVICE_PRODUCT_KEY = "jAufEMTF1W6wnPhn";
private static final String SUB_DEVICE_NAME = "test-sub-register-device";
// ===================== 直连设备/网关动态注册测试 =====================
/**
* 直连设备/网关动态注册测试
* <p>
* 使用产品密钥productSecret进行签名验证成功后返回设备密钥deviceSecret
* <p>
* 注意:此接口不需要 Token 认证
*/
@Test
public void testDeviceRegister() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth/register/device", SERVER_HOST, SERVER_PORT);
// 1.2 生成签名
String random = IdUtil.fastSimpleUUID();
String sign = IotDeviceAuthUtils.buildRegisterSign(PRODUCT_SECRET, PRODUCT_KEY, DEVICE_NAME, random);
// 1.3 构建请求参数
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
reqDTO.setProductKey(PRODUCT_KEY);
reqDTO.setDeviceName(DEVICE_NAME);
reqDTO.setRandom(random);
reqDTO.setSign(sign);
String payload = JsonUtils.toJsonString(reqDTO);
// 1.4 输出请求
log.info("[testDeviceRegister][请求 URL: {}]", url);
log.info("[testDeviceRegister][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testDeviceRegister][响应体: {}]", response);
log.info("[testDeviceRegister][成功后可使用返回的 deviceSecret 进行一机一密认证]");
}
/**
* 测试动态注册后使用 deviceSecret 进行认证
* <p>
* 此测试需要先执行 testDeviceRegister 获取 deviceSecret
*/
@Test
public void testAuthAfterRegister() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
// TODO 将 testDeviceRegister 返回的 deviceSecret 填入此处
String deviceSecret = "返回的deviceSecret";
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, deviceSecret);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testAuthAfterRegister][请求 URL: {}]", url);
log.info("[testAuthAfterRegister][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testAuthAfterRegister][响应体: {}]", response);
}
// ===================== 网关认证测试 =====================
/**
* 网关设备认证测试:获取网关设备 Token用于后续子设备动态注册
*/
@Test
public void testGatewayAuth() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())
.setPassword(authInfo.getPassword());
String payload = JsonUtils.toJsonString(authReqDTO);
// 1.2 输出请求
log.info("[testGatewayAuth][请求 URL: {}]", url);
log.info("[testGatewayAuth][请求体: {}]", payload);
// 2.1 发送请求
String response = HttpUtil.post(url, payload);
// 2.2 输出结果
log.info("[testGatewayAuth][响应体: {}]", response);
log.info("[testGatewayAuth][请将返回的 token 复制到 GATEWAY_TOKEN 常量中]");
}
// ===================== 子设备动态注册测试 =====================
/**
* 子设备动态注册测试
* <p>
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
* <p>
* 注意:此接口需要网关 Token 认证
*/
@Test
public void testSubDeviceRegister() {
// 1.1 构建请求(需要网关认证)
String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();
subDevice.setProductKey(SUB_DEVICE_PRODUCT_KEY);
subDevice.setDeviceName(SUB_DEVICE_NAME);
String payload = JsonUtils.toJsonString(MapUtil.builder()
.put("id", IdUtil.fastSimpleUUID())
.put("method", IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())
.put("version", "1.0")
.put("params", Collections.singletonList(subDevice))
.build());
// 1.3 输出请求
log.info("[testSubDeviceRegister][请求 URL: {}]", url);
log.info("[testSubDeviceRegister][请求体: {}]", payload);
// 2.1 发送请求(需要网关 Token
try (HttpResponse httpResponse = HttpUtil.createPost(url)
.header("Authorization", GATEWAY_TOKEN)
.body(payload)
.execute()) {
// 2.2 输出结果
log.info("[testSubDeviceRegister][响应体: {}]", httpResponse.body());
log.info("[testSubDeviceRegister][成功后可使用返回的 deviceSecret 进行子设备认证]");
}
}
}

View File

@@ -211,11 +211,13 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
* 子设备动态注册测试
* <p>
* 网关设备代理子设备进行动态注册,平台返回子设备的 deviceSecret
* <p>
* 注意:此接口需要网关 Token 认证
*/
@Test
public void testSubDeviceRegister() {
// 1.1 构建请求
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/sub/register",
String url = String.format("http://%s:%d/auth/register/sub-device/%s/%s",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建请求参数
IotSubDeviceRegisterReqDTO subDevice = new IotSubDeviceRegisterReqDTO();