MCP 实战手记系列(四)· 代码实操
系列(一)讲了 DCR 被弃用、CIMD 上位,当时留了钩子:client_id 到底长什么样、自己搭要几行代码。这篇来兑现。重实操,代码我亲手跑过。
引子:一个容易被忽略的授权点
系列(三)迁移时发现 MCP 端点裸奔、凭证硬编码。再往深想:MCP 里"客户端是谁"没说清。DCR 那套动态注册,坑比用的人多。
一、DCR 到底烂在哪
- 每连一次就注册一个新 client_id,数据库越涨越大
- 客户端过期了服务器不知道,僵尸 ID 一堆
- 同一应用换个设备又是一个 ID
- 未认证的注册端点本身就是 DoS 靶子
CIMD 一句话清掉:别注册了,client_id 直接是 HTTPS URL。
二、CIMD 原理
授权时服务器去 GET 这个 URL 拿回元数据(client_name、redirect_uris),按需获取、可缓存。一个应用一个 URL,换设备换用户不产生多个 ID,也无注册端点被滥用。
它只解决"操作"问题,不解决"冒充";恶意客户端伪造回调冒充合法应用,CIMD 拦不住,文末说。
三、元数据文档长什么样
{
"client_id": "https://app.example.com/.well-known/mcp-client",
"client_name": "机房监控助手",
"redirect_uris": ["https://app.example.com/callback"],
"token_endpoint_auth_method": "none",
"scope": "mcp:read"
}
注意 client_id 即文档自身 URL。
四、动手:最小可用 4 步
代码在 com.ethanliang.mcp.cimd,仓库 tag v04。
步骤 1|客户端:把元数据挂到 URL 上
一个 Controller 返回 JSON,client_id 即本端点地址:
@RestController
class ClientMetadataController {
@GetMapping("/.well-known/mcp-client")
ClientMetadata metadata() {
return new ClientMetadata(
"http://localhost:8084/.well-known/mcp-client",
"机房监控助手",
List.of("http://localhost:8084/callback"),
"none", "mcp:read");
}
}
本地 demo 用
http://localhost:8084才能跑通;生产换 https 域名并打开cimd.require-https。
步骤 2|服务端:加拦截器读 client_id
MCP 规范建议把客户端身份放进请求头(如 Mcp-Client-Id),网关据路由与鉴权;头名以 SDK 为准,这篇统一用它:
public boolean preHandle(req, res, chain) {
String clientId = req.getHeader("Mcp-Client-Id");
if (clientId == null) { res.setStatus(401); return false; }
ClientMetadata meta = fetchMetadata(clientId); // GET 那个 URL,带 5 分钟缓存
if (!validate(meta, clientId)) { res.setStatus(401); return false; }
req.setAttribute("clientName", meta.clientName());
return true;
}
步骤 3|校验
先卡协议和主机白名单,再查元数据字段;白名单是防 SSRF 第一关——只 GET 认可的 host:
boolean validate(ClientMetadata m, String clientId) {
if (requireHttps && !clientId.startsWith("https://")) return false; // 生产强制 https
if (m.redirectUris() == null || m.redirectUris().isEmpty()) return false;
if (m.clientName() == null || m.clientName().isBlank()) return false;
return allowedHosts.contains(hostOf(clientId)); // 白名单:防 SSRF 第一关
}
步骤 4|信任
校验过的 clientName 放进请求属性,业务直接取,不再碰 client_id 本身。
五、怎么验证"真的没注册端点"
- 带正确
Mcp-Client-Id发请求返回 200,日志打印clientName=机房监控助手 - client_id 指向不存在的 URL,返回 401
- 元数据缺
redirect_uris返回 401
六、踩坑记录
实跑踩坑,报错原文保留:
① RestTemplate 没默认超时:恶意或慢 URL 拖满 Tomcat 线程。解法:RestTemplate 设 connect/read 超时,或换 WebClient 带 timeout()。
② 本地用自签 HTTPS:GET 元数据报证书错。解法:demo 关校验,生产必须可信证书。
③ client_id 是 URL 不是密码:别拿它当 token 校验;CIMD 不防冒充,需配平台级应用签名。
④ 必须缓存元数据:每次请求都 GET,延迟高,还容易把元数据服务打挂。解法:用 Caffeine 按 client_id 缓存 5 分钟(仓库已落地,见 ClientIdMetadataInterceptor)。
⑤ 客户端能指定任意 URL 让你 GET,典型 SSRF:打 169.254.169.254 云元数据、打内网 admin。解法:host 白名单是第一道关(只 GET 认可的 host),生产再把 requireHttps 打开、并拒绝私有/链路本地地址。这一步不能省。
七、CIMD 没解决什么
它只去掉注册端点运维负担。身份冒充靠平台级应用证明(macOS / Windows 签名)加软件声明(客户端后端签 JWT)。
demo 里服务端顺手托管了客户端元数据,真实环境由客户端自托管,服务器只按白名单 GET 并校验;这是把鉴权简化成「每请求拉元数据」的示意,生产放到授权服务器、只在 token 签发时取一次。
小结
- client_id 变 URL,注册端点消失,DCR 坑一次性清掉
- 代码很少,一个 Controller 加一个拦截器,半天能跑
- 安全边界要认清,CIMD 管登记不管冒充,生产接 EMA
完整可运行代码:github.com/ethanliang2016/mcp-in-action(tag:
v04),目录04-cimd-auth。
MCP 实战手记系列路线图
| # | 篇目 | 状态 |
|---|---|---|
| 1 | 总纲篇:MCP 到哪一步了 | ✅ |
| 2 | 跑通第一个 MCP Server | ✅ |
| 3 | 把 MCP Server 改成无状态 | ✅ |
| 4 | CIMD 授权实战(本篇) | ✅ |
| 5 | MCP Apps:让工具返回能点能填的界面 | 规划 |
| 6 | 自建 MCP 网关:一个端点聚合所有工具 | 规划 |
| 7 | 安全篇:端点鉴权、凭证管理与接入检查清单 | 规划 |
关注我,更新第一时间看到。你在 MCP 授权上踩过什么坑?评论区见,有价值的我整理进后续篇目。
参考:
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_39885962/article/details/165946293




