Spring Boot MCP 服务的鉴权与支付防护
文章围绕其引用的 MCP 2026-07-28 规范,介绍无状态端点、JWT 校验、工具级 scope 授权和支付操作追加认证。还讨论 CIMD 与会话机制变化对身份服务及负载均衡部署的影响。
AI 深度解读
AI 能查付款记录,也能创建退款后,MCP 的安全问题就不能停在「请求带没带 token」。原文的核心主张是:把 Spring Boot MCP server 当作 OAuth 2.1 资源服务器,逐层检查 token 的用途、工具权限和认证新鲜度,再用无状态部署与审计把整条链路管起来。
作者结合支付系统经验,讨论了其所称的 2026-07-28 规范变化。对接 AI 工具的程序员,最值得带走的是这些变化对应的工程边界:哪些由 Spring Security 承担,哪些仍要自己实现,以及哪些必须通过失败测试确认。
无状态解决部署问题,不能代替权限检查
按作者对规范的梳理,MCP 从 2025-06 修订版起,就已经把服务器定位为 OAuth 资源服务器。AI 应用是客户端,登录系统是授权服务器,提供工具的后端负责验证访问令牌。
作者称,2026-07-28 修订版移除了核心协议中的 initialize 握手和 session ID。请求不再依赖某个实例记住客户端状态,负载均衡器就可以把请求分发给任意副本。
作者曾在 AirAsia 处理每天约 1.3 亿次请求。他的经验是,粘性会话容易掩盖负载倾斜:部分节点过热,平均指标却正常,部署替换节点时才集中暴露问题。不过,这段经历并不是新版 MCP 的性能对照实验。

这里要分清两层无状态。Spring Security 设置 SessionCreationPolicy.STATELESS,只能说明安全链不创建会话,不能证明 MCP 传输实现没有隐藏状态。作者建议从 Spring AI 的 stateless Streamable HTTP 模式入手,再让两个副本交替处理请求,验证行为是否一致。他也承认,仅凭发布说明无法确认其实现与最终线格式完全匹配。
关闭 CSRF 同样有条件:对应路径只接受 Authorization 请求头中的 bearer token,不读取 cookie。以后若接入 cookie 认证,就必须重新评估。
签名正确,还要证明 token 是发给你的
JWT 签名能证明令牌没有被篡改,issuer 能说明由谁签发,audience 则回答:它究竟允许访问哪个服务?
同一个身份系统可能同时服务后台管理 API 和 MCP API。如果只查签名与 issuer,就可能接受原本发给其他服务的 token。
令牌合法,不等于它有资格进入当前服务。
作者用下面的校验器,把默认 issuer 校验和 audience 校验组合起来,失败时返回标准 invalid_token:
@Bean
JwtDecoder jwtDecoder(McpSecurityProperties props) {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withIssuerLocation(props.issuer())
.build();
OAuth2TokenValidator<Jwt> issuer =
JwtValidators.createDefaultWithIssuer(props.issuer());
OAuth2TokenValidator<Jwt> audience = token ->
token.getAudience().contains(props.resourceUri())
? OAuth2TokenValidatorResult.success()
: OAuth2TokenValidatorResult.failure(new OAuth2Error(
"invalid_token", "Token audience does not include this MCP server", null));
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(issuer, audience));
return decoder;
}
重点看 getAudience().contains(props.resourceUri()):它检查 audience 集合中有没有精确匹配的资源 URI,不能改成字符串前缀匹配,否则容易混入相似域名。
客户端如何知道该去哪里登录?作者通过 RFC 9728 的 /.well-known/oauth-protected-resource 发布资源元数据,并让无令牌请求得到的 401 指向它。文档包含 resource、authorization_servers、scopes_supported 和 bearer_methods_supported,其中 resource 与上面的 audience 校验保持一致。
进入服务后,还要按工具授权。MCP 通常把工具调用汇集到同一个 HTTP 入口,路径规则无法区分查付款和创建退款。下面这段代码把权限放到工具方法上:
@Service
class PaymentTools {
@Tool(description = "Look up a payment by its id")
@PreAuthorize("hasAuthority('SCOPE_payments:read')")
PaymentView getPayment(String paymentId) {
return payments.find(paymentId);
}
@Tool(description = "Create a refund for a settled payment")
@PreAuthorize("hasAuthority('SCOPE_payments:write')")
RefundReceipt createRefund(String paymentId, BigDecimal amount) {
stepUp.require(StepUpLevel.STRONG); // 见下一节
return refunds.create(paymentId, amount);
}
}
要盯住 @PreAuthorize 是否真的经过 Spring 方法安全代理。作者提醒,@Tool 发现机制和代理必须作用于同一个 bean,不能看见注解就认定权限生效。最直接的验证是:只带 payments:read 调用 createRefund,必须得到 403。工具列表也应按调用者权限过滤,但隐藏工具不能替代执行时检查。
CIMD 管客户端身份,step-up 管危险动作
原文涉及两项职责不同的机制。
Client ID Metadata Documents(CIMD)解决「这个客户端是谁」。AI 应用把 HTTPS JSON 文档的 URL 当作 client ID,由授权服务器下载并检查应用名称、回调地址,以及文档中的 client_id 是否与来源 URL 一致。MCP 资源服务器不负责抓取它。
按作者的规范解读,Dynamic Client Registration(DCR,RFC 7591)已被弃用,相关变更编号为 PR #2858,但仍兼容,也没有明确移除日期。作者没有找到「预注册、CIMD、DCR、询问用户」这一回退顺序的规范依据,因此没有依赖它。新增的 RFC 9207 issuer 校验,则用于帮助客户端确认授权响应来自预期服务器。
CIMD 把来源不明的注册记录换成可追踪的域名,但域名本身不是可信证明。抓取外部文档还会引入服务端请求伪造(SSRF):作者示例禁止重定向,设置 2 秒连接超时、3 秒请求超时,拒绝非 200 响应及超过 16_384 字节的正文,并要求连接前拦截私网、回环和链路本地地址。
step-up 则解决「现在能不能执行危险动作」。即使 token 有 payments:write,一小时前的登录也未必足以支持此刻退款。作者的 guard 检查 acr 是否达到要求,以及 auth_time 是否处于最近 5 分钟。
权限不足用 insufficient_scope;需要更强或更新的认证时,按 RFC 9470 返回 insufficient_user_authentication,通过 WWW-Authenticate 携带 acr_values、max_age 和 resource_metadata。客户端再去授权服务器完成认证。
是否完成增强认证,只能读取已验证的 token,不能相信模型的口头确认。
作者明确说,这部分是自定义代码,客户端对 challenge 的处理也不统一,必须测试完整往返。5 分钟只是示例策略,应当配置化。
我的判断
这套方案最有价值的地方,是把安全控制落到了服务端的可验证条件:audience 限定服务,scope 限定工具,认证新鲜度约束危险操作。相比只在提示词里要求模型谨慎,这些检查不依赖模型是否听话。
但原文不是开箱即用的规范实现。作者自述写过并运行这些代码,没有给出完整工程、Spring AI 精确版本或对照测试数据。其关于新版规范的结论,也应视为作者的核对结果,不能把示例直接当成所有 starter 已具备的能力。
代码还留下了边界。CIMD 示例先用 ofByteArray() 读完正文,再检查大小,因此 16_384 字节并没有真正限制下载过程的内存占用;SsrfGuard 没有实现,DNS 解析与实际连接之间如何防绕过也没交代。step-up 示例直接读取 auth_time,缺失或类型异常时怎么返回规范错误,同样需要补齐。
另外,近期强认证不等于用户确认了某笔交易的金额和收款人。作者借用了 PSD2 强客户认证的思路,但展示的 acr、auth_time 检查不足以证明完整实现了交易动态绑定。
审计方向也合理:记录用户、客户端、工具、结果、耗时和 trace ID,敏感参数脱敏,日志进入独立的仅追加存储。不过示例切面能否捕获被方法安全提前拒绝的调用,仍取决于切面顺序,需要验证。
能拿来做什么
- 已有 MCP 服务可以参考作者的三周安排:第一周补 audience 和资源元数据,第二周补工具 scope 与列表过滤,第三周给两三个高风险工具加 step-up 和审计;排期应按实际规模调整。
- 把负向用例放进 CI:错误 audience 返回 401,只读 token 创建退款返回 403,过期认证触发 challenge;同时验证 CIMD 身份不匹配、
169.254.169.254和私网地址被拒绝,以及双副本行为一致。 - 前端或 Node 编写的 MCP 客户端,应测试从 401 发现授权服务,到处理
insufficient_scope、增强认证并重试的完整流程,避免只覆盖 token 有效时的成功调用。 - 身份团队应确认 Keycloak、Entra、Auth0 或 Okta 的 CIMD 支持计划;自建 Spring Authorization Server 则评估
RegisteredClientRepository扩展。作者建议迁移期间对 DCR 限流保留,并按实际 Spring AI 版本删除 starter 已覆盖的自定义实现。