
篇(七)做了端点鉴权------但那只是一层。
本文把散落的安全点串成体系,逐层在本机跑通,并说清哪些我验证过、哪些没有。
引子:鉴权过了,然后呢
篇(七)给网关加了个 Bearer 校验,没带令牌的请求进不来。看起来安全了。
但上线第一周就撞了个问题:Agent 拿着合法令牌进来,能查任何门店的销售数据------天河城店的店长,查到了北京路店和深圳店的营业额。
原因很直白:我们只验证了"谁在调用"(客户端),没验证"代表谁调用"(最终用户)。前者是鉴权,后者是授权,中间差着好几层。
这就是六层的由来。
六层是什么
| 层 | 作用 |
|---|---|
| L1 传输安全 | 链路本身可信吗(mTLS) |
| L2 客户端身份 | 谁在调用(CIMD / Token) |
| L3 用户身份透传 | 代表谁调用(最终用户) |
| L4 访问控制 | 这个用户能看什么(RBAC) |
| L5 流量防护 | 调用多频繁(限流 / 熔断) |
| L6 审计追踪 | 发生过什么(结构化日志) |
单层的失效方式很一致:任何一层被绕过,后面就全裸。所以这六层不是选做清单,是纵深。
范围说明:这六层覆盖的是调用链安全 ------"谁在调、能调什么、调多快、留没留痕"。内容安全(输入校验、输出过滤、Prompt 注入防护)是另一条线,本篇不涉及,别以为 MCP 安全只有这六件事。
L1 传输安全(mTLS)
双方互验证书,服务端确认客户端身份,客户端也确认服务端不是冒充的。
⚠️ 本机未实测 :mTLS 需要签发客户端证书、配置 truststore,属于部署侧工作。本模块只给了 SSL 配置位与"在哪一层终止"的说明,真实双向认证我还没在真实证书体系下跑过。
一个决策点:mTLS 在哪一层终止?
- 在网关/接入层终止:后端拿到的是明文,简单,但要求内网可信
- 透传到应用:更安全,证书管理成本高
判断依据就一条:你能不能接受内网明文。有服务网格兜底就选接入层终止,没有就透传到应用。我倾向前者。
L2 客户端身份
篇(四)讲过 CIMD(用 HTTPS URL 当 client_id)。本篇用静态 Bearer Token 替代演示------CIMD 要求服务端能 GET 到客户端的 metadata URL,本机演示环境把这个环节省掉了。
这不代表 CIMD 只能公网用,内网结合自签 CA 同样可行,只是这条路我没验证。
实测(无令牌 / 错令牌):
HTTP 401 {"error":"invalid client token"}
两层判断:有没有令牌、令牌对不对。这两步是所有防护的地基,做起来也最便宜。
L3 用户身份透传(最难的一层)
客户端身份 ≠ 用户身份。Agent 是代表某个店长在调用,服务端得知道这个店长是谁。
标准正在成形:DPoP(Agent Identity WG 在推)、ID-JAG(已被 EMA 使用)、RFC 8693 token exchange、SEP-1933(Workload Identity Federation)。标准落地前没有公认做法。
我的过渡方案:请求头带 X-User-Id,服务端提取后放进 UserContext,工具层取用。
⚠️ 这层的局限,讲透 :
这是身份断言,不是身份验证 ------
X-User-Id是客户端自己声明的,服务端选择信任,没有任何密码学校验。它防不住:伪造用户、委托链、凭证轮换、audience/issuer 校验。
适用范围仅限内部可信网络。真实生产要等标准落地(DPoP / ID-JAG),或自建签发校验。
我宁可把它当"局限"讲,也不包装成"生产级安全能力"------资深读者一眼能看出差别。
生产后长什么样(迁移路径) :现在用X-User-Id顶,是因为标准没定。标准落地后,这一层会换成可验证的身份令牌 ------客户端带一个由可信签发方签发的、含 issuer + audience 校验的令牌(DPoP / ID-JAG 都是往这个方向走),服务端验证签名、而不是信任声明。到那时"断言"才升级成"验证",本文 L3 的过渡方案自然退役。这是把当前做法当"过渡"而不是"终点"的原因。
实测(缺用户身份):
"拒绝:未携带用户身份(缺 X-User-Id)"
L4 访问控制(RBAC)
门店场景天然适合讲这层:店长只能查自己门店,区域经理可查区域内,总部可查全部。
我最初把权限判断放在网关层------写完发现行不通:网关看到的是 get_store_sales(storeCode=ST002) 这种调用,它不知道 ST002 属于谁的数据。判断能不能看,只有工具自己知道参数语义。于是挪到了工具层。
实测六种组合:
| 用户(角色) | 查 | 结果 |
|---|---|---|
| manager-st001(店长) | ST001(自己) | ✅ 天河城店 12,480 元 |
| manager-st001(店长) | ST002(别人) | ❌ 无权访问 |
| manager-st002(店长) | ST002(自己) | ✅ 北京路店 9,860 元 |
| regional-gz(区域) | ST001(区域内) | ✅ 放行 |
| regional-gz(区域) | ST003(区域外) | ❌ 无权访问 |
| hq(总部) | ST003 | ✅ 深圳华强北店 15,230 元 |
| stranger(无角色) | ST001 | ❌ DENIED |
判断逻辑就一段 switch,位置比逻辑本身更重要。
进阶取舍(写给会做到几十个工具的人) :工具少时"每工具一段 switch"够用;工具一多就散------权限判断散落在各工具里,改一条策略要改多处、还容易漏。生产上更常见的做法是把策略集中:一个中央授权服务(或 policy 引擎)持有"用户 → 角色 → 工具/资源 → 动作"的规则,各工具通过统一拦截器调它判定,自己不再写判断。"ST002 属于谁"这类资源归属下沉成一张归属表,由授权服务查。这条路我在第(十三)篇多租户细讲,本篇只点这个方向------知道"位置比逻辑重要",也要知道"位置最终要收敛到一处"。
L5 流量防护(限流)
用的 Bucket4j,规格 5 次 / 10 秒。实测连发 8 次:
200 200 200 200 200 429 429 429
前 5 次放行,第 6 次起被拦。令牌桶按 5/10s 贪婪补充。
⚠️ 一个我踩到的坑 :这里的桶是内存 Map 。单实例没问题,多实例部署时每个实例各算各的,放行上限变成实例数 × 5(实际能放多少还看请求怎么分发)。真实生产必须把计数外置(Redis,或在网关层统一限流)。
这条我在单机上验证不出来,是看代码推出来的------所以标为待生产验证,不假装已验证。
L6 审计追踪
每次调用打一条结构化日志,含路径、方法、判定结果、用户、耗时:
AUDIT | path=/mcp | method=POST | result=PASS | user=hq | costMs=0
AUDIT | path=/mcp | method=POST | result=REJECT_RATE_LIMIT | user=- | costMs=0
被拒绝的请求也要记。只记成功的日志,等于出事时看不到攻击。
⚠️ 这层的边界 :日志现在只落在单实例本地文件------多实例下会分散在各台机器上,实例销毁即丢失。生产需要集中采集(ELK / 云日志服务),并对接防篡改存储,否则安全事件里这份日志的证据力很弱。这条我没做。
落地优先级:2 → 1 → 5 → 4 → 6 → 3
| 顺序 | 层 | 理由 |
|---|---|---|
| 1 | L2 客户端身份 | 最便宜、最标准,一步挡掉绝大部分非法调用 |
| 2 | L1 mTLS | 部署侧工作,做完链路就可信 |
| 3 | L5 限流 | 防自己人误伤,比防攻击者更常发生 |
| 4 | L4 RBAC | 需要业务语义,但数据一分级就必须做 |
| 5 | L6 审计 | 出事能回溯,也倒逼前几层做扎实 |
| 6 | L3 用户身份 | 标准未定,先用过渡方案顶着 |
L3 排在最后,原因是它现在没有标准答案------先用断言方案顶着,等 DPoP / ID-JAG 落地再替换,比现在硬造一套强。如果你的服务只在内部可信网络里跑、且没有数据分级需求,L3 和 L4 都可以先不做。
小结
- 六层是纵深:单层被绕过就全裸,别指望一把锁解决所有问题
- RBAC 的位置比逻辑重要:网关不懂参数语义,判断得落在工具层
本机已验证 / 待生产验证
| 项 | 状态 |
|---|---|
| L2 令牌校验(401) | ✅ 本机已验证 |
| L3 身份提取与缺失拒绝 | ✅ 本机已验证(断言方案,非验证) |
| L4 RBAC 六种组合 | ✅ 本机已验证 |
| L5 限流触发(429) | ✅ 单实例已验证;多实例计数待生产验证 |
| L6 审计日志输出 | ✅ 本机已验证;集中采集 / 防篡改待生产验证(现为单实例本地文件) |
| L1 mTLS 双向证书 | ❌ 未验证,仅给配置位与部署建议 |
| CIMD 公网 metadata | ❌ 未验证(本机用静态 Token 替代) |
| 真实 IAM 下的身份校验 | ❌ 未验证 |
环境:Spring Boot 4.0.0 / Spring AI 2.0.1 / MCP Java SDK 2.0.0 / JDK 21 / Bucket4j 8.14。
示例代码仅用于技术演示,令牌为 demo 值,生产请走 CIMD/EMA 体系。
📦 本文完整可运行代码 (tag: v09,模块 09-store-ops-security,端口 8090)
- 国内访问 / 点 star:https://gitee.com/ethanliang2016/mcp-in-action
- GitHub(需代理):https://github.com/ethanliang2016/mcp-in-action
- 克隆:
git clone https://gitee.com/ethanliang2016/mcp-in-action.git
启动:java -jar store-ops-security.jar(令牌走环境变量,未进仓库)。
跑通了点个 star ⭐,跑不通直接提 issue。