feat(iot):【网关设备:72%】动态注册的初步实现(未测试、额外优化代码),基于 stateful-sauteeing-pillow.md 规划

This commit is contained in:
YunaiV
2026-01-25 12:58:17 +08:00
parent 38a21ad59c
commit b4ce72ea7d
29 changed files with 313 additions and 457 deletions

View File

@@ -7,8 +7,10 @@ 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.biz.dto.IotSubDeviceRegisterFullReqDTO;
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.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -21,6 +23,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@@ -66,4 +70,11 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
return success(deviceService.registerDevice(reqDTO));
}
@Override
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register-sub")
@PermitAll
public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(@RequestBody IotSubDeviceRegisterFullReqDTO reqDTO) {
return success(deviceService.registerSubDevices(reqDTO));
}
}

View File

@@ -49,7 +49,7 @@ public class IotProductSaveReqVO {
private String codecType;
@Schema(description = "是否开启动态注册", example = "false")
@NotEmpty(message = "是否开启动态注册不能为空")
@NotNull(message = "是否开启动态注册不能为空")
private Boolean registerEnabled;
}

View File

@@ -44,8 +44,8 @@ public interface ErrorCodeConstants {
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, "设备已激活,不允许重复注册");
ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.device;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
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;
@@ -44,18 +45,6 @@ public interface IotDeviceService {
*/
void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
// TODO @芋艿:先这么实现。未来看情况,要不要自己实现
/**
* 更新设备的所属网关
*
* @param id 编号
* @param gatewayId 网关设备 ID
*/
default void updateDeviceGateway(Long id, Long gatewayId) {
updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId));
}
/**
* 更新设备状态
*
@@ -358,6 +347,25 @@ public interface IotDeviceService {
// ========== 设备动态注册 ==========
/**
* 直连/网关设备动态注册
*
* @param reqDTO 动态注册请求
* @return 注册结果(包含 DeviceSecret
*/
IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
/**
* 网关子设备动态注册
* <p>
* 与 {@link #handleSubDeviceRegisterMessage} 方法的区别:
* 该方法网关设备信息通过 reqDTO 参数传入,而 {@link #handleSubDeviceRegisterMessage} 方法通过 gatewayDevice 参数传入
*
* @param reqDTO 子设备注册请求(包含网关设备信息)
* @return 注册结果列表
*/
List<IotSubDeviceRegisterRespDTO> registerSubDevices(@Valid IotSubDeviceRegisterFullReqDTO reqDTO);
/**
* 处理子设备动态注册消息(网关设备上报)
*
@@ -367,12 +375,4 @@ public interface IotDeviceService {
*/
List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
/**
* 设备动态注册(直连设备/网关)
*
* @param reqDTO 动态注册请求
* @return 注册结果(包含 DeviceSecret
*/
IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
}

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.service.device;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
@@ -14,6 +15,7 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
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;
@@ -142,11 +144,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
private void initDevice(IotDeviceDO device, IotProductDO product) {
device.setProductId(product.getId()).setProductKey(product.getProductKey())
.setDeviceType(product.getDeviceType());
// 生成密钥
device.setDeviceSecret(IotDeviceAuthUtils.generateDeviceSecret());
// 设置设备状态为未激活
device.setState(IotDeviceStateEnum.INACTIVE.getState());
.setDeviceType(product.getDeviceType())
.setDeviceSecret(generateDeviceSecret()) // 生成密钥
.setState(IotDeviceStateEnum.INACTIVE.getState()); // 默认未激活
}
private String generateDeviceSecret() {
return IdUtil.fastSimpleUUID();
}
@Override
@@ -448,7 +452,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) {
IotDeviceDO device = validateDeviceExists(id);
// 使用 IotDeviceAuthUtils 生成认证信息
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class);
}
@@ -496,7 +500,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
@Override
public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) {
// 1. 校验设备是否存在
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
if (deviceInfo == null) {
log.error("[authDevice][认证失败username({}) 格式不正确]", authReqDTO.getUsername());
return false;
@@ -510,7 +514,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
}
// 2. 校验密码
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) {
log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
return false;
@@ -666,7 +670,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
private IotDeviceDO addDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceAuthReqDTO subDeviceAuth) {
// 1.1 解析子设备信息
IotDeviceAuthUtils.DeviceInfo subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername());
IotDeviceIdentity subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername());
if (subDeviceInfo == null) {
throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID);
}
@@ -814,63 +818,78 @@ public class IotDeviceServiceImpl implements IotDeviceService {
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);
// 1.3 验证 productSecret
if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
throw exception(DEVICE_REGISTER_SECRET_INVALID);
}
// 4. 查找设备(预注册模式:设备必须已存在)
// TODO @AI设备不用提前有这个有问题
// 1.4 校验设备是否已存在(已存在则不允许重复注册)
IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName());
if (device == null) {
throw exception(DEVICE_NOT_EXISTS);
if (device != null) {
throw exception(DEVICE_REGISTER_ALREADY_EXISTS);
}
// 5. 校验设备是否已激活(已激活的设备不允许重复注册)
if (!Objects.equals(device.getState(), IotDeviceStateEnum.INACTIVE.getState())) {
throw exception(DEVICE_ALREADY_ACTIVATED);
}
// 6. 返回设备密钥
// 2.1 自动创建设备
IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
.setDeviceName(reqDTO.getDeviceName())
.setProductId(product.getId());
device = createDevice0(createReqVO);
log.info("[registerDevice][产品({}) 自动创建设备({})]",
reqDTO.getProductKey(), reqDTO.getDeviceName());
// 2.2 返回设备密钥
return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
}
@Override
public List<IotSubDeviceRegisterRespDTO> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
// 1. 校验网关设备
IotDeviceDO gatewayDevice = getSelf().getDeviceFromCache(reqDTO.getGatewayProductKey(), reqDTO.getGatewayDeviceName());
// 2. 遍历注册每个子设备
return registerSubDevices0(gatewayDevice, reqDTO.getSubDevices());
}
@Override
public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
// 1.1 校验网关设备类型
if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
throw exception(DEVICE_NOT_GATEWAY);
}
// 1.2 解析参数
// 1. 解析参数
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)) {
List<IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.convertList(message.getParams(), IotSubDeviceRegisterReqDTO.class);
// 2. 遍历注册每个子设备
return registerSubDevices0(gatewayDevice, subDevices);
}
private List<IotSubDeviceRegisterRespDTO> registerSubDevices0(IotDeviceDO gatewayDevice,
List<IotSubDeviceRegisterReqDTO> subDevices) {
// 1.1 校验网关设备
if (gatewayDevice == null) {
throw exception(DEVICE_NOT_EXISTS);
}
if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
throw exception(DEVICE_NOT_GATEWAY);
}
// 1.2 注册设备不能为空
if (CollUtil.isEmpty(subDevices)) {
throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
}
// 2. 遍历注册每个子设备
List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(paramsList.size());
for (IotSubDeviceRegisterReqDTO params : paramsList) {
List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(subDevices.size());
for (IotSubDeviceRegisterReqDTO subDevice : subDevices) {
try {
IotDeviceDO device = registerSubDevice(gatewayDevice, params);
IotDeviceDO device = registerSubDevice0(gatewayDevice, subDevice);
results.add(new IotSubDeviceRegisterRespDTO(
params.getProductKey(), params.getDeviceName(), device.getDeviceSecret()));
subDevice.getProductKey(), subDevice.getDeviceName(), device.getDeviceSecret()));
} catch (Exception ex) {
log.error("[handleSubDeviceRegisterMessage][子设备({}/{}) 注册失败]",
params.getProductKey(), params.getDeviceName(), ex);
log.error("[registerSubDevices0][子设备({}/{}) 注册失败]",
subDevice.getProductKey(), subDevice.getDeviceName(), ex);
}
}
// 3. 返回响应数据(包含成功注册的子设备列表)
return results;
}
// TODO @AI阿里云的设备必须存在
private IotDeviceDO registerSubDevice(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
private IotDeviceDO registerSubDevice0(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
// 1.1 校验产品
IotProductDO product = productService.getProductByProductKey(params.getProductKey());
if (product == null) {
@@ -880,28 +899,28 @@ public class IotDeviceServiceImpl implements IotDeviceService {
if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
}
// 1.3 查找设备是否已存在
// 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;
if (existDevice == null) {
throw exception(DEVICE_NOT_EXISTS);
}
// 1.4 校验是否绑定到其他网关
if (existDevice.getGatewayId() != null && ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) {
throw exception(DEVICE_GATEWAY_BINDTO_EXISTS,
existDevice.getProductKey(), existDevice.getDeviceName());
}
// 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;
// 2. 绑定到网关(如果尚未绑定)
if (existDevice.getGatewayId() == null) {
// 2.1 更新数据库
deviceMapper.updateById(new IotDeviceDO().setId(existDevice.getId()).setGatewayId(gatewayDevice.getId()));
// 2.2 清空对应缓存
deleteDeviceCache(existDevice);
log.info("[registerSubDevice][网关({}/{}) 绑定子设备({}/{})]",
gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
existDevice.getProductKey(), existDevice.getDeviceName());
}
return existDevice;
}
}

View File

@@ -3,7 +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.hutool.core.util.IdUtil;
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;
@@ -55,11 +55,15 @@ public class IotProductServiceImpl implements IotProductService {
// 2. 插入
IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
.setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
.setProductSecret(IotDeviceAuthUtils.generateProductSecret());
.setProductSecret(generateProductSecret());
productMapper.insert(product);
return product.getId();
}
private String generateProductSecret() {
return IdUtil.fastSimpleUUID();
}
@Override
@CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id")
public void updateProduct(IotProductSaveReqVO updateReqVO) {

View File

@@ -4,8 +4,12 @@ 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.biz.dto.IotSubDeviceRegisterFullReqDTO;
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 java.util.List;
/**
* IoT 设备通用 API
@@ -31,11 +35,19 @@ public interface IotDeviceCommonApi {
CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
/**
* 设备动态注册(一型一密)
* 直连/网关设备动态注册(一型一密)
*
* @param reqDTO 动态注册请求
* @return 注册结果(包含 DeviceSecret
*/
CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
/**
* 网关子设备动态注册(网关代理转发)
*
* @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
* @return 注册结果列表
*/
CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
}

View File

@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 设备认证 Request DTO
@@ -9,6 +11,8 @@ import lombok.Data;
* @author 芋道源码
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceAuthReqDTO {
/**

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* IoT 子设备动态注册 Request DTO
* <p>
* 额外包含了网关设备的标识信息
*
* @author 芋道源码
*/
@Data
public class IotSubDeviceRegisterFullReqDTO {
/**
* 网关设备 ProductKey
*/
@NotEmpty(message = "网关产品标识不能为空")
private String gatewayProductKey;
/**
* 网关设备 DeviceName
*/
@NotEmpty(message = "网关设备名称不能为空")
private String gatewayDeviceName;
/**
* 子设备注册列表
*/
@NotNull(message = "子设备注册列表不能为空")
private List<IotSubDeviceRegisterReqDTO> subDevices;
}

View File

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

View File

@@ -8,7 +8,7 @@ import lombok.Data;
* 用于 thing.event.post 消息的 params 参数
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services">阿里云 - 设备上报事件</a>
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
*/
@Data
public class IotDeviceEventPostReqDTO {

View File

@@ -11,7 +11,7 @@ import java.util.Map;
* 本质是一个 Mapkey 为属性标识符value 为属性值
*
* @author 芋道源码
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services">阿里云 - 设备上报属性</a>
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-attributes">阿里云 - 设备上报属性</a>
*/
public class IotDevicePropertyPostReqDTO extends HashMap<String, Object> {

View File

@@ -1,12 +1,10 @@
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;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
/**
* IoT 设备【认证】的工具类,参考阿里云
@@ -15,49 +13,12 @@ import lombok.NoArgsConstructor;
*/
public class IotDeviceAuthUtils {
/**
* 认证信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AuthInfo {
/**
* 客户端 ID
*/
private String clientId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
/**
* 设备信息
*/
@Data
public static class DeviceInfo {
private String productKey;
private String deviceName;
}
public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) {
public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
String clientId = buildClientId(productKey, deviceName);
String username = buildUsername(productKey, deviceName);
String password = buildPassword(deviceSecret,
buildContent(clientId, productKey, deviceName, deviceSecret));
return new AuthInfo(clientId, username, password);
return new IotDeviceAuthReqDTO(clientId, username, password);
}
public static String buildClientId(String productKey, String deviceName) {
@@ -80,70 +41,12 @@ public class IotDeviceAuthUtils {
"productKey" + productKey;
}
public static DeviceInfo parseUsername(String username) {
public static IotDeviceIdentity parseUsername(String username) {
String[] usernameParts = username.split("&");
if (usernameParts.length != 2) {
return null;
}
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);
return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
}
}

View File

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.json.JsonObject;
@@ -201,7 +202,7 @@ public class IotEmqxAuthEventHandler {
*/
private void handleDeviceStateChange(String username, boolean online) {
// 1. 解析设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
return;

View File

@@ -51,7 +51,7 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler(this);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);

View File

@@ -8,7 +8,7 @@ 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.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
@@ -74,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
}
// 校验 token
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token);
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
Assert.notNull(deviceInfo, "设备信息不能为空");
// 校验设备信息是否匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())

View File

@@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -72,7 +72,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username);
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空位");

View File

@@ -33,7 +33,6 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
// TODO @AI参数不太对看看我写的建议
JsonObject body = context.body().asJsonObject();
String productKey = body.getString("productKey");
if (StrUtil.isEmpty(productKey)) {
@@ -43,21 +42,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
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 不能为空");
String productSecret = body.getString("productSecret");
if (StrUtil.isEmpty(productSecret)) {
throw invalidParamException("productSecret 不能为空");
}
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey)
.setDeviceName(deviceName)
.setRandom(random)
.setSign(sign);
.setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
result.checkError();

View File

@@ -2,12 +2,15 @@ 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 cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@@ -27,13 +30,10 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
*/
public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
private final IotHttpUpstreamProtocol protocol;
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService deviceMessageService;
public IotHttpRegisterSubHandler(IotHttpUpstreamProtocol protocol) {
this.protocol = protocol;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
public IotHttpRegisterSubHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
@@ -42,18 +42,19 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
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());
// 2. 解析子设备列表
JsonObject body = context.body().asJsonObject();
List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
// TODO @AI可能还是需要一个新的 deviceApi 接口。因为 register sub 子设备不太一行;
// 2.3 发送消息
Object responseData = deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
// 3. 调用子设备动态注册
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
.setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
result.checkError();
// 3. 返回结果
return success(responseData);
// 4. 返回结果
return success(result.getData());
}
}

View File

@@ -9,6 +9,7 @@ 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.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
@@ -214,7 +215,7 @@ public class IotMqttUpstreamHandler {
}
// 4. 获取设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
return false;

View File

@@ -10,6 +10,7 @@ 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.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
@@ -521,7 +522,7 @@ public class IotMqttWsUpstreamHandler {
}
// 3. 获取设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[authenticateDevice][用户名格式不正确username: {}]", username);
return null;

View File

@@ -11,6 +11,7 @@ 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.IotDeviceRespDTO;
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.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec;
import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec;
@@ -162,7 +163,7 @@ public class IotTcpUpstreamHandler implements Handler<NetSocket> {
}
// 2.1 解析设备信息
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.getUsername());
if (deviceInfo == null) {
sendErrorResponse(socket, message.getRequestId(), "解析设备信息失败", codecType);
return;

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.iot.gateway.service.auth;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
/**
* IoT 设备 Token Service 接口
@@ -24,7 +24,7 @@ public interface IotDeviceTokenService {
* @param token 设备 Token
* @return 设备信息
*/
IotDeviceAuthUtils.DeviceInfo verifyToken(String token);
IotDeviceIdentity verifyToken(String token);
/**
* 解析用户名
@@ -32,6 +32,6 @@ public interface IotDeviceTokenService {
* @param username 用户名
* @return 设备信息
*/
IotDeviceAuthUtils.DeviceInfo parseUsername(String username);
IotDeviceIdentity parseUsername(String username);
}

View File

@@ -5,6 +5,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import jakarta.annotation.Resource;
@@ -48,7 +49,7 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
}
@Override
public IotDeviceAuthUtils.DeviceInfo verifyToken(String token) {
public IotDeviceIdentity verifyToken(String token) {
Assert.notBlank(token, "token 不能为空");
// 校验 JWT Token
boolean verify = JWTUtil.verify(token, gatewayProperties.getToken().getSecret().getBytes());
@@ -68,11 +69,11 @@ public class IotDeviceTokenServiceImpl implements IotDeviceTokenService {
String deviceName = payload.getStr("deviceName");
Assert.notBlank(productKey, "productKey 不能为空");
Assert.notBlank(deviceName, "deviceName 不能为空");
return new IotDeviceAuthUtils.DeviceInfo().setProductKey(productKey).setDeviceName(deviceName);
return new IotDeviceIdentity(productKey, deviceName);
}
@Override
public IotDeviceAuthUtils.DeviceInfo parseUsername(String username) {
public IotDeviceIdentity parseUsername(String username) {
return IotDeviceAuthUtils.parseUsername(username);
}

View File

@@ -6,9 +6,13 @@ 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.biz.dto.IotSubDeviceRegisterFullReqDTO;
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.gateway.config.IotGatewayProperties;
import java.util.List;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -61,6 +65,11 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
return doPost("/register", reqDTO, new ParameterizedTypeReference<>() { });
}
@Override
public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
return doPost("/register-sub", reqDTO, new ParameterizedTypeReference<>() { });
}
private <T, R> CommonResult<R> doPost(String url, T body,
ParameterizedTypeReference<CommonResult<R>> responseType) {
try {

View File

@@ -1,203 +0,0 @@
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

@@ -7,6 +7,7 @@ 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.event.IotDeviceEventPostReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
@@ -22,6 +23,7 @@ import org.junit.jupiter.api.Test;
* <p>使用步骤:
* <ol>
* <li>启动 yudao-module-iot-gateway 服务HTTP 端口 8092</li>
* <li>运行 {@link #testDeviceRegister()} 测试直连设备动态注册(一型一密)</li>
* <li>运行 {@link #testAuth()} 获取设备 token将返回的 token 粘贴到 {@link #TOKEN} 常量</li>
* <li>运行以下测试方法:
* <ul>
@@ -45,11 +47,78 @@ public class IotDirectDeviceHttpProtocolIntegrationTest {
private static final String DEVICE_NAME = "small";
private static final String DEVICE_SECRET = "0baa4c2ecc104ae1a26b4070c218bdf3";
/**
* 产品密钥(从 iot_product 表的 product_secret 字段获取),用于动态注册
*/
private static final String PRODUCT_SECRET = "your_product_secret";
/**
* 动态注册的设备名称
*/
private static final String REGISTER_DEVICE_NAME = "test-register-device";
/**
* 直连设备 Token从 {@link #testAuth()} 方法获取后,粘贴到这里
*/
private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9kdWN0S2V5IjoiNGF5bVpnT1RPT0NyREtSVCIsImV4cCI6MTc2OTMwNTA1NSwiZGV2aWNlTmFtZSI6InNtYWxsIn0.mf3MEATCn5bp6cXgULunZjs8d00RGUxj96JEz0hMS7k";
// ===================== 动态注册测试 =====================
/**
* 直连设备动态注册测试(一型一密)
* <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 构建请求参数
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO();
reqDTO.setProductKey(PRODUCT_KEY);
reqDTO.setDeviceName(REGISTER_DEVICE_NAME);
reqDTO.setProductSecret(PRODUCT_SECRET);
String payload = JsonUtils.toJsonString(reqDTO);
// 1.3 输出请求
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, REGISTER_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);
}
// ===================== 认证测试 =====================
/**

View File

@@ -75,7 +75,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
public void testAuth() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME, GATEWAY_DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
@@ -108,7 +108,7 @@ public class IotGatewayDeviceHttpProtocolIntegrationTest {
String url = String.format("http://%s:%d/topic/sys/%s/%s/thing/topo/add",
SERVER_HOST, SERVER_PORT, GATEWAY_PRODUCT_KEY, GATEWAY_DEVICE_NAME);
// 1.2 构建子设备认证信息
IotDeviceAuthUtils.AuthInfo subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
IotDeviceAuthReqDTO subAuthInfo = IotDeviceAuthUtils.getAuthInfo(
SUB_DEVICE_PRODUCT_KEY, SUB_DEVICE_NAME, SUB_DEVICE_SECRET);
IotDeviceAuthReqDTO subDeviceAuth = new IotDeviceAuthReqDTO()
.setClientId(subAuthInfo.getClientId())

View File

@@ -63,7 +63,7 @@ public class IotGatewaySubDeviceHttpProtocolIntegrationTest {
public void testAuth() {
// 1.1 构建请求
String url = String.format("http://%s:%d/auth", SERVER_HOST, SERVER_PORT);
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO()
.setClientId(authInfo.getClientId())
.setUsername(authInfo.getUsername())