fix: harden gateway auth and credential handling
This commit is contained in:
@@ -2,12 +2,14 @@ package com.iot.gateway.mcp;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.iot.gateway.common.BackendInternalClient;
|
||||
import com.iot.gateway.common.BizException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@@ -17,13 +19,19 @@ public class McpToolRegistry {
|
||||
@Autowired
|
||||
private BackendInternalClient internalClient;
|
||||
|
||||
/**
|
||||
* 演示/开发模式开关。生产环境下必须为 false,避免将后端故障误报为“成功”或演示响应
|
||||
*/
|
||||
@Value("${gateway.mock.enabled:false}")
|
||||
private boolean mockEnabled;
|
||||
|
||||
public List<Map<String, Object>> getToolDefinitions() {
|
||||
List<Map<String, Object>> tools = new ArrayList<>();
|
||||
|
||||
// 1. iot_device_list
|
||||
Map<String, Object> t1 = new HashMap<>();
|
||||
t1.put("name", "iot_device_list");
|
||||
t1.put("description", "查询当前租户名下的所有工业物联网设备台账与实时运行状态(在线/报警/停机)。");
|
||||
t1.put("description", "查询当前租户名下的所有工业物联网设备台账与实时运行状态(在线/报警/停机)。所需权限范围: device.read");
|
||||
Map<String, Object> s1 = new HashMap<>();
|
||||
s1.put("type", "object");
|
||||
Map<String, Object> p1 = new HashMap<>();
|
||||
@@ -37,7 +45,7 @@ public class McpToolRegistry {
|
||||
// 2. iot_telemetry_latest
|
||||
Map<String, Object> t2 = new HashMap<>();
|
||||
t2.put("name", "iot_telemetry_latest");
|
||||
t2.put("description", "召测指定工业设备的最新实时点位遥测数据(注塑机温度、注塑压力、螺杆转速、能耗电流等)。");
|
||||
t2.put("description", "召测指定工业设备的最新实时点位遥测数据(注塑机温度、注塑压力、螺杆转速、能耗电流等)。所需权限范围: telemetry.read");
|
||||
Map<String, Object> s2 = new HashMap<>();
|
||||
s2.put("type", "object");
|
||||
Map<String, Object> p2 = new HashMap<>();
|
||||
@@ -50,12 +58,12 @@ public class McpToolRegistry {
|
||||
// 3. iot_device_control
|
||||
Map<String, Object> t3 = new HashMap<>();
|
||||
t3.put("name", "iot_device_control");
|
||||
t3.put("description", "向边缘物联设备安全下发反写控制指令(如设定注塑温度、设备启停控制、工艺参数微调)。");
|
||||
t3.put("description", "向边缘物联设备安全下发反写控制指令(如设定注塑温度、设备启停控制、工艺参数微调)。所需权限范围: device.control");
|
||||
Map<String, Object> s3 = new HashMap<>();
|
||||
s3.put("type", "object");
|
||||
Map<String, Object> p3 = new HashMap<>();
|
||||
p3.put("deviceId", Map.of("type", "string", "description", "目标设备编码"));
|
||||
p3.put("command", Map.of("type", "string", "description", "指令动作: start/stop/set_param"));
|
||||
p3.put("command", Map.of("type", "string", "description", "指令动作: start/stop/reboot/set_param"));
|
||||
p3.put("params", Map.of("type", "object", "description", "指令参数键值对"));
|
||||
s3.put("properties", p3);
|
||||
s3.put("required", List.of("deviceId", "command"));
|
||||
@@ -65,7 +73,7 @@ public class McpToolRegistry {
|
||||
// 4. iot_alarm_query
|
||||
Map<String, Object> t4 = new HashMap<>();
|
||||
t4.put("name", "iot_alarm_query");
|
||||
t4.put("description", "按设备与时间范围检索未处理的设备故障与历史严重告警记录。");
|
||||
t4.put("description", "按设备与时间范围检索未处理的设备故障与历史严重告警记录。所需权限范围: alarm.read");
|
||||
Map<String, Object> s4 = new HashMap<>();
|
||||
s4.put("type", "object");
|
||||
Map<String, Object> p4 = new HashMap<>();
|
||||
@@ -78,8 +86,20 @@ public class McpToolRegistry {
|
||||
return tools;
|
||||
}
|
||||
|
||||
public Map<String, Object> executeTool(String toolName, Map<String, Object> args, Long tenantId) {
|
||||
log.info("执行 MCP 工具调用: tool={}, tenantId={}, args={}", toolName, tenantId, args);
|
||||
/**
|
||||
* 执行 MCP 工具调用,严格校验租户上下文与授权范围 Scope
|
||||
*/
|
||||
public Map<String, Object> executeTool(String toolName, Map<String, Object> args, Long tenantId, List<String> scopes, boolean isAdmin) {
|
||||
log.info("执行 MCP 工具调用: tool={}, tenantId={}, args={}, scopes={}, isAdmin={}",
|
||||
toolName, tenantId, args, scopes, isAdmin);
|
||||
|
||||
// 1. 严格租户隔离校验
|
||||
if (tenantId == null) {
|
||||
throw new BizException(401, "MCP 工具执行拒绝: 缺少有效的租户上下文,严禁匿名或跨租户执行");
|
||||
}
|
||||
|
||||
// 2. 细粒度 Scope 校验
|
||||
checkToolScope(toolName, scopes, isAdmin);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
@@ -103,22 +123,40 @@ public class McpToolRegistry {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 兜底降级响应
|
||||
// 非 mock 模式下直接明确报错
|
||||
if (!mockEnabled) {
|
||||
String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "核心物联后端服务不可用或查询超时";
|
||||
throw new BizException(502, "召测设备列表失败: " + errMsg);
|
||||
}
|
||||
|
||||
// 开发演示兜底
|
||||
result.put("tenantId", tenantId);
|
||||
result.put("isMock", true);
|
||||
result.put("total", 2);
|
||||
result.put("devices", List.of(
|
||||
Map.of("deviceCode", "DEV20261009001", "deviceName", "1号注塑机 (海天MA1200)", "online", 1),
|
||||
Map.of("deviceCode", "DEV20261009002", "deviceName", "2号注塑机 (海天MA2000)", "online", 0)
|
||||
));
|
||||
} else if ("iot_telemetry_latest".equals(toolName)) {
|
||||
String deviceId = args != null && args.containsKey("deviceId") ? String.valueOf(args.get("deviceId")) : "DEV20261009001";
|
||||
String deviceId = args != null && args.containsKey("deviceId") ? String.valueOf(args.get("deviceId")) : "";
|
||||
if (deviceId.isEmpty()) {
|
||||
throw new BizException(400, "缺少必填参数: deviceId");
|
||||
}
|
||||
|
||||
JsonNode resp = internalClient.get("/api/internal/open/telemetry/latest", tenantId, Map.of("deviceId", deviceId));
|
||||
if (resp != null && resp.has("code") && resp.get("code").asInt() == 200 && resp.has("data")) {
|
||||
result.put("tenantId", tenantId);
|
||||
result.put("deviceId", deviceId);
|
||||
result.put("data", resp.get("data"));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!mockEnabled) {
|
||||
String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "后端遥测接口不可用或设备无数据";
|
||||
throw new BizException(502, "召测设备遥测失败: " + errMsg);
|
||||
}
|
||||
|
||||
result.put("isMock", true);
|
||||
result.put("deviceId", deviceId);
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
result.put("telemetry", Map.of(
|
||||
@@ -128,17 +166,26 @@ public class McpToolRegistry {
|
||||
"screw_speed_rpm", 85.0
|
||||
));
|
||||
} else if ("iot_device_control".equals(toolName)) {
|
||||
// 控制指令下发:绝不能在后端失败时误报“EXECUTED”
|
||||
JsonNode resp = internalClient.post("/api/internal/open/commands", tenantId, args);
|
||||
if (resp != null && resp.has("code") && resp.get("code").asInt() == 200 && resp.has("data")) {
|
||||
result.put("tenantId", tenantId);
|
||||
result.put("result", resp.get("data"));
|
||||
result.put("success", true);
|
||||
result.put("status", "EXECUTED");
|
||||
result.put("data", resp.get("data"));
|
||||
return result;
|
||||
}
|
||||
|
||||
result.put("success", true);
|
||||
result.put("deviceId", args != null ? args.get("deviceId") : "");
|
||||
result.put("status", "EXECUTED");
|
||||
result.put("message", "控制指令已成功通过网关下发");
|
||||
String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "后端指令网关通信超时或未响应";
|
||||
if (!mockEnabled) {
|
||||
log.error("下发设备控制指令失败: tool={}, tenantId={}, args={}, err={}", toolName, tenantId, args, errMsg);
|
||||
throw new BizException(502, "控制指令执行失败: " + errMsg);
|
||||
}
|
||||
|
||||
result.put("isMock", true);
|
||||
result.put("success", false);
|
||||
result.put("status", "MOCK_MODE_REFUSED");
|
||||
result.put("message", "当前为开发演示模式,已拒绝向物理设备真实下发控制指令: " + errMsg);
|
||||
} else if ("iot_alarm_query".equals(toolName)) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (args != null && args.containsKey("deviceId")) params.put("deviceId", String.valueOf(args.get("deviceId")));
|
||||
@@ -151,13 +198,50 @@ public class McpToolRegistry {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!mockEnabled) {
|
||||
String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "后端告警接口不可用";
|
||||
throw new BizException(502, "查询告警失败: " + errMsg);
|
||||
}
|
||||
|
||||
result.put("isMock", true);
|
||||
result.put("alarms", List.of(
|
||||
Map.of("alarmId", "ALM-2026100901", "deviceId", "DEV20261009001", "severity", 3, "title", "模具温度异常过高")
|
||||
));
|
||||
} else {
|
||||
result.put("error", "未知的工具名称: " + toolName);
|
||||
throw new BizException(400, "未知的 MCP 工具名称: " + toolName);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void checkToolScope(String toolName, List<String> scopes, boolean isAdmin) {
|
||||
if (isAdmin) return;
|
||||
|
||||
String requiredScope;
|
||||
switch (toolName) {
|
||||
case "iot_device_list" -> requiredScope = "device.read";
|
||||
case "iot_telemetry_latest" -> requiredScope = "telemetry.read";
|
||||
case "iot_device_control" -> requiredScope = "device.control";
|
||||
case "iot_alarm_query" -> requiredScope = "alarm.read";
|
||||
default -> requiredScope = "all";
|
||||
}
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
throw new BizException(403, "权限不足: 客户端未具备任何授权范围,无法调用 [" + toolName + "]");
|
||||
}
|
||||
|
||||
String normReq = requiredScope.replace(":", ".");
|
||||
boolean match = false;
|
||||
for (String s : scopes) {
|
||||
String norm = s.trim().replace(":", ".");
|
||||
if (norm.equals(normReq) || norm.equals("all") || norm.equals("*")) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
throw new BizException(403, "权限不足: 调用工具 [" + toolName + "] 需要具备 [" + requiredScope + "] 授权范围");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user