feat:【IoT 物联网】增加网关 HTTP 协议的鉴权,基于 JWT 轻量级
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.iot.api.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.RpcConstants;
|
||||
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.service.device.IotDeviceService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备 API 实现类
|
||||
*
|
||||
* @author haohao
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary // 保证优先匹配,因为 yudao-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入
|
||||
public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth")
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) {
|
||||
return success(deviceService.authDevice(authReqDTO));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.api.device;
|
||||
|
||||
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 jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* * 设备数据 Upstream 上行 API 实现类
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary // 保证优先匹配,因为 yudao-module-iot-net-component-core 也有 IotDeviceUpstreamApi 的实现,并且也可能会被 biz 引入
|
||||
public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceUpstreamService deviceUpstreamService;
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
|
||||
boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*
|
||||
* TODO 芋艿:后续删除
|
||||
* iot API 包,定义并实现提供给其它模块的 API
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.api;
|
||||
@@ -121,23 +121,10 @@ public class IotDeviceDO extends TenantBaseDO {
|
||||
*/
|
||||
private String firmwareId;
|
||||
|
||||
// TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有
|
||||
/**
|
||||
* 设备密钥,用于设备认证,需安全存储
|
||||
* 设备密钥,用于设备认证
|
||||
*/
|
||||
private String deviceSecret;
|
||||
/**
|
||||
* MQTT 客户端 ID
|
||||
*/
|
||||
private String mqttClientId;
|
||||
/**
|
||||
* MQTT 用户名
|
||||
*/
|
||||
private String mqttUsername;
|
||||
/**
|
||||
* MQTT 密码
|
||||
*/
|
||||
private String mqttPassword;
|
||||
/**
|
||||
* 认证类型(如一机一密、动态注册)
|
||||
*/
|
||||
|
||||
@@ -9,14 +9,15 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
|
||||
*/
|
||||
public interface RedisKeyConstants {
|
||||
|
||||
// TODO @芋艿:弱化 deviceKey;使用 product_key + device_name 替代
|
||||
/**
|
||||
* 设备属性的数据缓存,采用 HASH 结构
|
||||
* <p>
|
||||
* KEY 格式:device_property:{productKey},${deviceName}
|
||||
* KEY 格式:device_property:{deviceKey}
|
||||
* HASH KEY:identifier 属性标识
|
||||
* VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO}
|
||||
*/
|
||||
String DEVICE_PROPERTY = "iot:device_property:%s,%s";
|
||||
String DEVICE_PROPERTY = "iot:device_property:%s";
|
||||
|
||||
/**
|
||||
* 设备的最后上报时间,采用 ZSET 结构
|
||||
@@ -26,6 +27,15 @@ public interface RedisKeyConstants {
|
||||
*/
|
||||
String DEVICE_REPORT_TIMES = "iot:device_report_times";
|
||||
|
||||
/**
|
||||
* 设备关联的网关 serverId 缓存,采用 HASH 结构
|
||||
*
|
||||
* KEY 格式:device_server_id
|
||||
* HASH KEY:{productKey},{deviceName}
|
||||
* VALUE 数据类型:String serverId
|
||||
*/
|
||||
String DEVICE_SERVER_ID = "iot:device_server_id";
|
||||
|
||||
/**
|
||||
* 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户)
|
||||
*
|
||||
@@ -42,13 +52,4 @@ public interface RedisKeyConstants {
|
||||
*/
|
||||
String THING_MODEL_LIST = "iot:thing_model_list";
|
||||
|
||||
/**
|
||||
* 设备关联的网关 serverId 缓存,采用 HASH 结构
|
||||
*
|
||||
* KEY 格式:device_server_id
|
||||
* HASH KEY:{productKey},{deviceName}
|
||||
* VALUE 数据类型:String serverId
|
||||
*/
|
||||
String DEVICE_SERVER_ID = "iot:device_server_id";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.framework.security.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import cn.iocoder.yudao.module.iot.enums.ApiConstants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
/**
|
||||
* IoT 模块的 Security 配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false, value = "iotSecurityConfiguration")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Bean("iotAuthorizeRequestsCustomizer")
|
||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||
return new AuthorizeRequestsCustomizer() {
|
||||
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
// RPC 服务的安全配置
|
||||
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.framework.security.core;
|
||||
@@ -2,6 +2,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.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -228,4 +229,12 @@ public interface IotDeviceService {
|
||||
*/
|
||||
List<IotDeviceDO> getDeviceListByProductKeyAndNames(String productKey, List<String> deviceNames);
|
||||
|
||||
/**
|
||||
* 认证设备
|
||||
*
|
||||
* @param authReqDTO 认证信息
|
||||
* @return 是否认证成功
|
||||
*/
|
||||
boolean authDevice(IotDeviceAuthReqDTO authReqDTO);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,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.ObjUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
@@ -12,16 +13,16 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
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.enums.IotDeviceStateEnum;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper;
|
||||
import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
|
||||
import cn.iocoder.yudao.module.iot.core.enums.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 cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -397,15 +398,17 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return respVO;
|
||||
}
|
||||
|
||||
// TODO @芋艿:改成通用的;
|
||||
@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());
|
||||
// MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(),
|
||||
// device.getDeviceSecret());
|
||||
// return new IotDeviceMqttConnectionParamsRespVO()
|
||||
// .setMqttClientId(mqttSignResult.getClientId())
|
||||
// .setMqttUsername(mqttSignResult.getUsername())
|
||||
// .setMqttPassword(mqttSignResult.getPassword());
|
||||
return null;
|
||||
}
|
||||
|
||||
private void deleteDeviceCache(IotDeviceDO device) {
|
||||
@@ -459,4 +462,29 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return deviceMapper.selectByProductKeyAndDeviceNames(productKey, deviceNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) {
|
||||
// 1. 校验设备是否存在
|
||||
IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
|
||||
if (deviceInfo == null) {
|
||||
log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername());
|
||||
return false;
|
||||
}
|
||||
String deviceName = deviceInfo.getDeviceName();
|
||||
String productKey = deviceInfo.getProductKey();
|
||||
IotDeviceDO device = getSelf().getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName);
|
||||
if (device == null) {
|
||||
log.warn("[authDevice][设备({}/{}) 不存在]", productKey, deviceName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验密码
|
||||
IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
|
||||
if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) {
|
||||
log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package cn.iocoder.yudao.module.iot.service.device.control;
|
||||
|
||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@@ -48,11 +47,4 @@ public interface IotDeviceUpstreamService {
|
||||
// */
|
||||
// void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO);
|
||||
|
||||
/**
|
||||
* Emqx 连接认证
|
||||
*
|
||||
* @param authReqDTO Emqx 连接认证 DTO
|
||||
*/
|
||||
boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.service.device.control;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO;
|
||||
@@ -12,8 +11,6 @@ import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
|
||||
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.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;
|
||||
@@ -222,37 +219,4 @@ public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService {
|
||||
// sendDeviceMessage(message, device);
|
||||
}
|
||||
|
||||
// TODO @芋艿:后续需要考虑,http 的认证
|
||||
@Override
|
||||
public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
|
||||
log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO);
|
||||
// 1.1 校验设备是否存在。username 格式:${DeviceName}&${ProductKey}
|
||||
String[] usernameParts = authReqDTO.getUsername().split("&");
|
||||
if (usernameParts.length != 2) {
|
||||
log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]");
|
||||
return false;
|
||||
}
|
||||
String deviceName = usernameParts[0];
|
||||
String productKey = usernameParts[1];
|
||||
// 1.2 获得设备
|
||||
IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName);
|
||||
if (device == null) {
|
||||
log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", productKey, deviceName);
|
||||
return false;
|
||||
}
|
||||
// TODO @haohao:需要记录,记录设备的最后时间
|
||||
|
||||
// 2. 校验密码
|
||||
String deviceSecret = device.getDeviceSecret();
|
||||
String clientId = authReqDTO.getClientId();
|
||||
MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId);
|
||||
// TODO 建议,先失败,return false;
|
||||
if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) {
|
||||
log.info("[authenticateEmqxConnection][认证成功]");
|
||||
return true;
|
||||
}
|
||||
log.error("[authenticateEmqxConnection][认证失败,密码不正确]");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.util;
|
||||
|
||||
import cn.hutool.crypto.digest.HMac;
|
||||
import cn.hutool.crypto.digest.HmacAlgorithm;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* MQTT 签名工具类
|
||||
*
|
||||
* 提供静态方法来计算 MQTT 连接参数
|
||||
*/
|
||||
public class MqttSignUtils {
|
||||
|
||||
/**
|
||||
* 计算 MQTT 连接参数
|
||||
*
|
||||
* @param productKey 产品密钥
|
||||
* @param deviceName 设备名称
|
||||
* @param deviceSecret 设备密钥
|
||||
* @return 包含 clientId, username, password 的结果对象
|
||||
*/
|
||||
public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) {
|
||||
return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 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;
|
||||
// 构建签名内容
|
||||
StringBuilder signContentBuilder = new StringBuilder()
|
||||
.append("clientId").append(clientId)
|
||||
.append("deviceName").append(deviceName)
|
||||
.append("deviceSecret").append(deviceSecret)
|
||||
.append("productKey").append(productKey);
|
||||
|
||||
// 使用 HMac 计算签名
|
||||
byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8);
|
||||
String signContent = signContentBuilder.toString();
|
||||
HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key);
|
||||
String password = mac.digestHex(signContent);
|
||||
|
||||
return new MqttSignResult(clientId, username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* MQTT 签名结果类
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class MqttSignResult {
|
||||
|
||||
private final String clientId;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user