【功能新增】IoT: 添加 EMQX 插件,支持设备连接认证和 MQTT 连接参数获取,优化配置文件

This commit is contained in:
安浩浩
2025-02-20 18:30:57 +08:00
parent 8e7bbfe0da
commit ca95752266
32 changed files with 685 additions and 163 deletions

View File

@@ -4,15 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService;
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* * 设备数据 Upstream 上行 API 实现类
* * 设备数据 Upstream 上行 API 实现类
*/
@RestController
@Validated
@@ -61,6 +60,12 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
return success(true);
}
@Override
public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
Boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO);
return success(result);
}
// ========== 插件相关 ==========
@Override

View File

@@ -7,8 +7,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService;
@@ -177,4 +177,11 @@ public class IotDeviceController {
return success(true);
}
@GetMapping("/mqtt-connection-params")
@Operation(summary = "获取 MQTT 连接参数")
@PreAuthorize("@ss.hasPermission('iot:device:mqtt-connection-params')")
public CommonResult<IotDeviceMqttConnectionParamsRespVO> getMqttConnectionParams(@RequestParam("deviceId") Long deviceId) {
return success(deviceService.getMqttConnectionParams(deviceId));
}
}

View File

@@ -168,4 +168,12 @@ public interface IotDeviceService {
*/
IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport);
/**
* 获取 MQTT 连接参数
*
* @param deviceId 设备 ID
* @return MQTT 连接参数
*/
IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId);
}

View File

@@ -20,6 +20,8 @@ import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.util.MqttSignUtils;
import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult;
import jakarta.annotation.Resource;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
@@ -123,10 +125,8 @@ public class IotDeviceServiceImpl implements IotDeviceService {
.setDeviceType(product.getDeviceType());
// 生成并设置必要的字段
// TODO @芋艿:各种 mqtt 是不是可以简化!
device.setDeviceSecret(generateDeviceSecret())
.setMqttClientId(generateMqttClientId())
.setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey()))
.setMqttPassword(generateMqttPassword());
// clientId、username、password 根据规则实时生成
device.setDeviceSecret(generateDeviceSecret());
// 设置设备状态为未激活
device.setState(IotDeviceStateEnum.INACTIVE.getState());
}
@@ -318,35 +318,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return IdUtil.fastSimpleUUID();
}
/**
* 生成 MQTT Client ID
*
* @return 生成的 MQTT Client ID
*/
private String generateMqttClientId() {
return IdUtil.fastSimpleUUID();
}
/**
* 生成 MQTT Username
*
* @param deviceName 设备名称
* @param productKey 产品 Key
* @return 生成的 MQTT Username
*/
private String generateMqttUsername(String deviceName, String productKey) {
return deviceName + "&" + productKey;
}
/**
* 生成 MQTT Password
*
* @return 生成的 MQTT Password
*/
private String generateMqttPassword() {
return RandomUtil.randomString(32);
}
@Override
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
@@ -417,6 +388,17 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return respVO;
}
@Override
public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) {
IotDeviceDO device = validateDeviceExists(deviceId);
MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(),
device.getDeviceSecret());
return new IotDeviceMqttConnectionParamsRespVO()
.setMqttClientId(mqttSignResult.getClientId())
.setMqttUsername(mqttSignResult.getUsername())
.setMqttPassword(mqttSignResult.getPassword());
}
private void deleteDeviceCache(IotDeviceDO device) {
// 保证 Spring AOP 触发
getSelf().deleteDeviceCache0(device);

View File

@@ -62,4 +62,11 @@ public interface IotDeviceUpstreamService {
*/
void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO);
/**
* Emqx 连接认证
*
* @param authReqDTO Emqx 连接认证 DTO
*/
Boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO);
}

View File

@@ -20,6 +20,8 @@ import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService;
import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
import cn.iocoder.yudao.module.iot.util.MqttSignUtils;
import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -58,25 +60,26 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService {
// 2.1 情况一:属性上报
String requestId = IdUtil.fastSimpleUUID();
if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) {
reportDeviceProperty(((IotDevicePropertyReportReqDTO)
new IotDevicePropertyReportReqDTO().setRequestId(requestId).setReportTime(LocalDateTime.now())
.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
reportDeviceProperty(((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO()
.setRequestId(requestId).setReportTime(LocalDateTime.now())
.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
.setProperties((Map<String, Object>) simulatorReqVO.getData()));
return;
}
// 2.2 情况二:事件上报
if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) {
reportDeviceEvent(((IotDeviceEventReportReqDTO)
new IotDeviceEventReportReqDTO().setRequestId(requestId).setReportTime(LocalDateTime.now())
.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
.setIdentifier(simulatorReqVO.getIdentifier()).setParams((Map<String, Object>) simulatorReqVO.getData()));
reportDeviceEvent(((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId)
.setReportTime(LocalDateTime.now())
.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
.setIdentifier(simulatorReqVO.getIdentifier())
.setParams((Map<String, Object>) simulatorReqVO.getData()));
return;
}
// 2.3 情况三:状态变更
if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.STATE.getType())) {
updateDeviceState(((IotDeviceStateUpdateReqDTO)
new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now())
.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO()
.setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now())
.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()))
.setState((Integer) simulatorReqVO.getData()));
return;
}
@@ -277,6 +280,37 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService {
sendDeviceMessage(message, device);
}
@Override
public Boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO);
// 1. 校验设备是否存在
// username 格式:${DeviceName}&${ProductKey}
String[] usernameParts = authReqDTO.getUsername().split("&");
if (usernameParts.length != 2) {
log.error("[authenticateEmqxConnection][认证失败username 格式不正确]");
return Boolean.FALSE;
}
String deviceName = usernameParts[0];
String productKey = usernameParts[1];
IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(
productKey, deviceName);
if (device == null) {
log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]",
productKey, deviceName);
return Boolean.FALSE;
}
// 2. 校验密码
String deviceSecret = device.getDeviceSecret();
String clientId = authReqDTO.getClientId();
MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId);
if (!StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) {
log.error("[authenticateEmqxConnection][认证失败,密码不正确]");
return Boolean.FALSE;
}
log.info("[authenticateEmqxConnection][认证成功]");
return Boolean.TRUE;
}
private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) {
// 1. 【异步】记录设备与插件实例的映射
pluginInstanceService.updateDevicePluginInstanceProcessIdAsync(device.getDeviceKey(), reqDTO.getProcessId());

View File

@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.iot.util;
import lombok.Getter;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
* MQTT 签名工具类
* 提供静态方法来计算 MQTT 连接参数。
*/
public class MqttSignUtils {
private static final String SIGN_METHOD = "hmacsha256";
/**
* 计算 MQTT 连接参数
*
* @param productKey 产品密钥
* @param deviceName 设备名称
* @param deviceSecret 设备密钥
* @return 包含 clientId, username, password 的结果对象
*/
public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) {
String clientId = productKey + "." + deviceName;
String username = deviceName + "&" + productKey;
String signContent = String.format("clientId%sdeviceName%sdeviceSecret%sproductKey%s",
clientId, deviceName, deviceSecret, productKey);
String password = sign(signContent, deviceSecret);
return new MqttSignResult(clientId, username, password);
}
/**
* 计算 MQTT 连接参数
*
* @param productKey 产品密钥
* @param deviceName 设备名称
* @param deviceSecret 设备密钥
* @param clientId 客户端 ID
* @return 包含 clientId, username, password 的结果对象
*/
public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) {
String username = deviceName + "&" + productKey;
String signContentBuilder = "clientId" + clientId +
"deviceName" + deviceName +
"deviceSecret" + deviceSecret +
"productKey" + productKey;
String password = sign(signContentBuilder, deviceSecret);
return new MqttSignResult(clientId, username, password);
}
private static String sign(String content, String key) {
try {
Mac mac = Mac.getInstance(SIGN_METHOD);
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), SIGN_METHOD));
byte[] signData = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return bytesToHex(signData);
} catch (Exception e) {
throw new RuntimeException("Failed to sign content with HmacSHA256", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
/**
* MQTT 签名结果类
*/
@Getter
public static class MqttSignResult {
private final String clientId;
private final String username;
private final String password;
public MqttSignResult(String clientId, String username, String password) {
this.clientId = clientId;
this.username = username;
this.password = password;
}
}
}