【功能新增】IoT: 添加 EMQX 插件,支持设备连接认证和 MQTT 连接参数获取,优化配置文件
This commit is contained in:
@@ -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
|
||||
|
@@ -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));
|
||||
}
|
||||
|
||||
}
|
@@ -168,4 +168,12 @@ public interface IotDeviceService {
|
||||
*/
|
||||
IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport);
|
||||
|
||||
/**
|
||||
* 获取 MQTT 连接参数
|
||||
*
|
||||
* @param deviceId 设备 ID
|
||||
* @return MQTT 连接参数
|
||||
*/
|
||||
IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId);
|
||||
|
||||
}
|
@@ -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);
|
||||
|
@@ -62,4 +62,11 @@ public interface IotDeviceUpstreamService {
|
||||
*/
|
||||
void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO);
|
||||
|
||||
/**
|
||||
* Emqx 连接认证
|
||||
*
|
||||
* @param authReqDTO Emqx 连接认证 DTO
|
||||
*/
|
||||
Boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO);
|
||||
|
||||
}
|
||||
|
@@ -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());
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user