AI Agent 安全实践:从 MCP 工具调用到 Java 权限校验

👋 关于作者

你好,这里是一条 泥憨鱼 | 专注于 后端开发 、AI 前沿技术与计算机基础 。

🏠 个人主页 :一条泥憨鱼

通过技术博客记录技术成长,分享学习心得与经验。期待与您共同交流!

📚精选技术专栏:

|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| ❄️后端开发 | ❄️Redis| 黑马点评 |
| ❄️从0开始学习计算机网络 | ❄️数据结构与算法 |
| ❄️苍穹外卖 | ❄️探秘AI与agent |

某运维给内部 Agent 接上数据库查询工具,有人在对话里说"把测试库里那张临时表清一下",模型自行拼了一条不带条件的删除语句,工具照单执行。模型输出只是文本,工具调用却把这段文本变成了不可撤销的副作用。下面依次讲 Agent 为什么非要用工具、MCP 在链路上负责到哪一步、Java 后端怎么补鉴权,以及为什么剩下的部分仍要靠隔离、审计和审批兜底。

为什么 Agent 需要外部工具

大语言模型本身有三个硬边界:它没有跨会话的持久状态,知识停留在训练截止时间点,而且只能输出 token,不能直接改变任何外部系统。让它回答"上个月订单量是多少",它要么编一个数字,要么坦白不知道。

**工具调用(tool calling / function calling)**就是为绕开这三个边界设计的。模型不直接执行动作,而是输出一段结构化的调用意图,比如工具名加参数 JSON,由宿主程序去真正执行,再把结果塞回上下文。整个循环是:模型决定调什么 → 宿主执行 → 结果回填 → 模型继续推理。

问题的根源也在这里。自然语言到结构化调用的翻译由模型完成,而模型是概率系统,它对工具名和参数的理解不保证正确。参数里的表名、租户 ID、文件路径、SQL 片段,全都是模型生成的字符串。工程上必须把它们当成外部用户输入来对待,而不是当成可信的内部调用。

常见的误解是"模型知道自己被授权做什么"。它不知道。上下文里写了"你只能查只读库",这句话对模型是软约束,对攻击者是提示注入的入口。真正的约束必须写在模型看不见、也改不了的地方。

MCP 解决了什么,又不负责什么

在 MCP 出现之前,每接一个工具就要为每个客户端写一套适配:Claude Desktop 一套、IDE 插件一套、自研 Agent 框架又一套。N 个客户端乘 M 个工具,集成成本是乘积级的。**MCP(Model Context Protocol)**把这个矩阵收敛成两层:工具服务端按统一协议暴露能力,客户端按统一协议发现和调用。

它定义的核心动作很朴素:客户端先拉取工具清单(名称、描述、参数 schema),模型据此决定调用哪个工具、填什么参数,客户端再把调用请求发给服务端。工具描述大致长这样:

复制代码
{

  "name": "query_order",

  "description": "按订单号查询订单详情,只读",

  "inputSchema": {

    "type": "object",

    "properties": {

      "orderId": { "type": "string" },

      "tenantId": { "type": "string" }

    },

    "required": ["orderId"]

  }

}

注意 tenantId 出现在了参数 schema 里。这是很多人踩坑的地方:如果服务端直接采信这个字段,模型(或被注入的提示词)就可以填任意租户,横向越权。

MCP 的边界必须说清楚。它是一份通信与发现协议,规定消息怎么序列化、工具怎么描述、调用怎么往返。它不规定也不强制:调用方是谁、这个身份有没有权限、高风险操作要不要审批、多租户数据怎么隔离。协议层把这些全部留给服务端实现。换句话说,MCP 让工具更容易被接进来,也让"接进来之后谁负责鉴权"这个问题更紧迫。

在 Java 后端做工具白名单与权限校验

结论先摆出来:权限判定必须绑定认证主体,也就是这次调用背后真实的人或服务身份,绝不能依赖模型在参数里自述的任何字段。下面是一个最小可运行的 Spring Boot 骨架。

先建工具注册表,把每个工具和它要求的最小权限绑在一起:

java 复制代码
public record ToolDef(String name, String requiredPermission, boolean highRisk) {}



@Component

public class ToolRegistry {

    private final Map<String, ToolDef> tools = Map.of(

        "query_order", new ToolDef("query_order", "order:read", false),

        "refund_order", new ToolDef("refund_order", "order:refund", true)

    );



    public Optional<ToolDef> find(String name) {

        return Optional.ofNullable(tools.get(name));

    }

}

白名单的价值在于默认拒绝:注册表里没有的工具名,无论模型怎么编都调不出去。这比维护一份黑名单可靠得多,因为攻击面的增长永远快过黑名单的维护速度。

调用入口做三件事:解析认证主体、匹配白名单、校验权限。

java 复制代码
@Service

public class ToolInvoker {

    private final ToolRegistry registry;

    private final AuthContext auth;   // 从 JWT / Session 解析,非模型提供



    public Object invoke(String toolName, Map<String, Object> args) {

        ToolDef def = registry.find(toolName)

            .orElseThrow(() -> new SecurityException("tool not allowed: " + toolName));



        // 权限来自认证主体,与 args 无关

        if (!auth.hasPermission(def.requiredPermission())) {

            throw new SecurityException("forbidden: " + toolName);

        }

        // 租户强制覆盖:忽略模型传来的 tenantId

        args.put("tenantId", auth.tenantId());



        validateArgs(toolName, args);          // 参数二次校验

        if (def.highRisk()) {

            approvalGate.require(auth.userId(), toolName, args); // 高风险走审批

        }

        return executor.run(toolName, args);

    }

}

这里有几个设计点。

第一,权限判断的输入是 auth,不是参数。auth 来自请求头里的令牌,经过签名校验后解析出来,模型碰不到。一旦把 hasPermission 写成读取 args.get("role"),整个体系就塌了。

第二,租户 ID 用服务端身份覆盖,而不是校验。校验意味着"我看看你填的对不对",覆盖意味着"你填什么都不算数"。后者更简单也更安全,因为模型根本没有正当理由指定租户。

第三,参数二次校验不能省。工具描述里的 JSON Schema 只约束类型和必填项,管不住语义。比如 orderId 是字符串,但长度、字符集、是否属于当前租户,都要在服务端再查一遍。SQL 拼接类工具尤其要防:参数里出现分号、注释符或 UNION 关键字时,应该直接拒绝,而不是交给下游数据库去赌。

第四,高危工具走审批闸门。refund_order 这种一旦执行就无法回滚的操作,调用前挂起,推给人工确认,确认通过再执行。闸门放在 Invoker 层,而不是放在具体工具实现里,这样所有高危工具共用一套逻辑,不会漏。

测试环境隔离、审计与人工审批

前面三层解决的是"能不能调"。还有一类问题它管不了:一个合法用户、在合法权限内、调了一个合法工具,但参数填错了。比如把生产库的表名填成测试库的,或者金额多打一个零。这类错误靠权限拦不住,只能靠环境和流程兜底。

测试环境隔离是第一道。Agent 的工具配置里,数据库连接串、对象存储桶、消息队列地址都应该是独立的沙箱资源,和生产物理隔离。很多团队的教训是"先让 Agent 连测试库跑通再说",结果测试库和生产库共用一个实例,只是 schema 不同。这种情况下,一个拼错的 schema 名就是一次生产事故。

审计日志要记的东西比想象中多。至少包括:认证主体(谁)、工具名(做了什么)、完整参数(用什么参数)、执行结果或错误(结果如何)、时间戳和请求链路 ID。参数要原样落库,不要脱敏掉关键字段,否则事后复盘时根本还原不了现场。日志本身也要防篡改,写入后只追加不修改。

人工审批的粒度可以按风险分级:只读工具自动放行,写操作要求二次确认,删除、退款、批量更新这类挂起等人工。审批记录同样是审计的一部分,谁批的、什么时候批的、批的时候看到了什么参数,都要留痕。

这三样东西不是 MCP 的替代品,也不是权限校验的替代品,它们补的是权限模型覆盖不到的那一段:合法身份下的错误决策。

收束

这篇文章从一个越权删除的场景出发,依次讲了工具调用的必要性、MCP 的协议边界、Java 后端的白名单与鉴权实现,以及隔离、审计和审批的补位作用。贯穿始终的一条线是:模型输出属于不可信输入,安全边界只能落在模型之外的服务端。

拿到任何 Agent 工具调用链,先问一句"这个参数是谁生成的"。凡是模型能生成的字段,包括工具名、资源标识、租户、角色、SQL 片段,都必须经过服务端重新确认或直接覆盖。只要有一个字段被默认信任,整条链路的鉴权就等于没有。

相关推荐
东方佑1 小时前
用先验重构线性注意力:为什么它至少比普通线性注意力更接近标准注意力
人工智能·机器学习·重构
lucas_AI1 小时前
Read-Parse-Describe:文档解析终于把「图里画了什么」也写进结果了
人工智能
Nebula_g1 小时前
云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解
java·开发语言·网络·数据库·状态模式·javase
萧瑟余晖1 小时前
Spring Security 6.x新特性与架构演进详解
java·spring·架构
aneasystone本尊1 小时前
学习大模型训练的目标函数:Next Token Prediction
人工智能
星核0penstarry1 小时前
## 一、为什么 Spark UDF 越来越成为瓶颈
人工智能·api·ai编程·gpu·灰度发布·ai 智能体重写·等价性测试
中空板生产 线1 小时前
现代中空板的发展趋势
人工智能
七牛云行业应用1 小时前
10月17日,DeepSeek连发两项更新:V4.1-Flash-1017上线,Harness升级接入Search/Fetch
人工智能·ai·ai编程
\光辉岁月/1 小时前
1.Object类
java·面向对象·object类