1. 为什么远程 MCP 需要 OAuth
本地 MCP(stdio)把服务器直接挂在客户端进程里,没有网络边界,鉴权退化为「信任本机」。
远程 MCP(Streamable HTTP / SSE)则不同:服务器是一个网络服务,任何客户端都能连上。此时必须回答「你是谁、你能用什么」,而 MCP 协议选择的答案是 OAuth 2.0。
一句话:stdio 靠进程边界隔离,远程靠 OAuth 建立身份与授权边界——鉴权方式由「传输层暴露面」决定。
1.1 鉴权需求拆解
| 需求 | 说明 |
|---|---|
| 身份认证 | 客户端是谁(应用 + 用户) |
| 授权 | 客户端能访问哪些工具/资源 |
| 最小权限 | 只给必要 scope |
| 令牌生命周期 | 短期访问 + 可刷新 |
| 吊销 | 令牌泄露可主动撤销 |
1.2 MCP 的 OAuth 场景
客户端(MCP Client) 服务器(MCP Server)
│ │
│ 1. 调用工具(无令牌) │
│ ← 401 Unauthorized │
│ 2. 发现授权端点 │
│ ← 返回 OAuth 元数据 │
│ 3. 发起授权码流程 │
│ 4. 获得 access_token │
│ 5. 带令牌重试 │
一句话:OAuth 在 MCP 中是「先握手、后授权、令牌访问」——401 触发发现与授权,令牌成为后续调用的通行证。
2. 动态客户端注册(RFC 7591)
传统 OAuth 要求客户端预注册(拿 client_id/client_secret)。MCP 客户端是成千上万的独立 Agent,无法逐个预注册,于是采用 Dynamic Client Registration。
2.1 注册请求
// POST /register
{
"client_name": "claude-desktop",
"redirect_uris": ["http://127.0.0.1:4567/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none", // 公开客户端,无需 secret
"scope": "tools:read resources:read"
}
2.2 注册响应
{
"client_id": "f3c1...",
"client_secret": "", // 公开客户端为空
"client_id_issued_at": 1750000000,
"token_endpoint_auth_method": "none"
}
2.3 公开 vs 机密客户端
| 类型 | 认证方式 | 适用 |
|---|---|---|
| 公开客户端 | PKCE,无 secret | 桌面/移动 Agent |
| 机密客户端 | client_secret | 后端服务 |
一句话:动态注册让每个 MCP 客户端按需获取 client_id——公开客户端用 PKCE 替代 secret,这是「多 Agent 生态」能跑起来的前提。
3. 授权码流程 + PKCE
3.1 PKCE 为什么必需
公开客户端没有 secret,授权码可能被截获。PKCE(Proof Key for Code Exchange) 用「变换后的随机挑战码」绑定授权码,即使授权码泄露也无法换取令牌。
3.2 完整流程
1. 客户端生成 code_verifier(随机字符串)
2. 计算 code_challenge = base64url(SHA256(code_verifier))
3. 跳转授权端点 ?code_challenge=...&code_challenge_method=S256
4. 用户授权 → 回调带 code
5. 客户端用 code + code_verifier 换 access_token
6. 服务器校验 SHA256(verifier) == challenge → 签发令牌
3.3 令牌请求
// POST /token
{
"grant_type": "authorization_code",
"code": "auth_code_123",
"code_verifier": "原始随机字符串",
"redirect_uri": "http://127.0.0.1:4567/callback",
"client_id": "f3c1..."
}
一句话:PKCE 把「客户端没有 secret」的弱点,用「一次性随机挑战码」补齐——授权码即使泄露,缺 verifier 也换不到令牌。
4. 访问令牌与刷新令牌轮换
4.1 双令牌
access_token(短期,如 15 分钟):每次工具调用携带
refresh_token(长期,如 30 天):仅用于换新访问令牌
分离收益:
- access 短命 → 泄露窗口小
- refresh 可吊销 → 会话可控
- 轮换 → 旧 refresh 作废,防重放
4.2 刷新与轮换
access_token 过期
→ POST /token grant_type=refresh_token
→ 服务器校验 refresh_token → 签发新 access + 新 refresh
→ 旧 refresh 立即作废
→ 检测到旧 refresh 复用 → 整条会话吊销(疑似被盗)
4.3 令牌在 MCP 请求中的携带
Authorization: Bearer <access_token>
每个 Streamable HTTP 请求都带
401 → 客户端走刷新流程后重试
4.4 令牌存储安全
- access_token:内存(会话级)
- refresh_token:加密存储(密钥链 / 加密 DB)
- 绝不写入日志 / 前端代码
- 吊销时机:用户登出、令牌疑似泄露、轮换异常
一句话:双令牌 + 轮换是远程 MCP 的会话骨架——access 扛调用、refresh 扛续命,轮换让每一次续期都「向前推进、旧钥作废」。
5. 多 Server 的鉴权协调
一个 Agent 客户端往往连接多个远程 MCP Server。每个 Server 独立 OAuth,客户端要管理多套令牌。
5.1 客户端鉴权管理层
MCP Client
├─ AuthStore: server → {access, refresh, client_id}
├─ TokenRefresher: 过期自动刷新(并发防抖)
└─ 每个 Server 独立通道
协调点:
- 各 Server 的授权端点可能不同
- 各 Server 的 scope 不同
- 刷新并发 → 加锁防重复刷新
5.2 令牌刷新防抖
async function getToken(server: Server): Promise<string> {
if (fresh(server.accessToken)) return server.accessToken;
// 并发调用共享同一刷新 Promise(防抖)
if (!server.refreshing) {
server.refreshing = refresh(server);
}
const tok = await server.refreshing;
server.refreshing = null;
return tok;
}
5.3 权限最小化
- 每 Server 只申请它需要的 scope
- 工具级授权:MCP 服务器可在内部再做 ACL
- 用户授权界面:一次性列出「哪些 Server 要什么权限」
一句话:多 Server 鉴权的难点不是 OAuth 本身,而是「多套令牌的编排」——独立存储、并发防抖、最小 scope,三件事做对才能流畅协作。
6. 无 OAuth 的本地场景
6.1 stdio 传输为何免鉴权
stdio:客户端 fork 服务器进程 → 双向管道
身份边界 = 进程边界(谁能 fork 谁就能用)
信任 = 本机信任 → 无需网络级鉴权
结论:stdio 本地服务器默认「信任进程」,鉴权交给操作系统
6.2 何时仍要鉴权
- stdio 服务器代理外部服务 → 需向外部服务鉴权(如数据库密码)
- stdio 桥接到远程 → 由远程端点负责 OAuth
- 多用户共享本地服务器 → 需用户级授权
6.3 选型矩阵
| 部署 | 传输 | 鉴权 |
|---|---|---|
| 本地工具 | stdio | 进程信任(免 OAuth) |
| 远程服务 | Streamable HTTP | OAuth 2.0 + PKCE |
| 内部服务 | HTTP | OAuth / mTLS |
| 桥接 | stdio→HTTP | 远程端负责 |
一句话:鉴权不是「越多越好」——本地 stdio 用进程信任换零配置,远程才需要 OAuth;选型跟着暴露面走。
7. 常见陷阱与排障
| 陷阱 | 症状 | 解决 |
|---|---|---|
| redirect_uri 不匹配 | 授权失败 | 注册与回调严格一致 |
| code_verifier 未存 | 换令牌失败 | 流程内存态保存 verifier |
| 刷新并发 | 重复刷新 / 旧令牌互踢 | 刷新防抖 + 单飞 |
| access 过期未刷新 | 401 循环 | 401 触发刷新后重试 |
| secret 进日志 | 令牌泄露 | 打码 + 日志白名单 |
| 多 Server scope 混乱 | 越权访问 | 每 Server 独立最小 scope |
排障思路:
1. 抓 401 → 看是否触发刷新
2. 抓授权回调 → 核对 code/challenge
3. 查令牌端点 → 看 grant_type 是否正确
4. 查吊销列表 → 确认未被动吊销
8. 总结
MCP 的鉴权体系可以概括为「远程 OAuth、本地免鉴、令牌轮换」:
| 层面 | 方案 | 要点 |
|---|---|---|
| 传输边界 | stdio 免鉴 / HTTP 鉴权 | 按暴露面选型 |
| 客户端身份 | 动态注册 RFC 7591 | 公开客户端 + PKCE |
| 授权流程 | Authorization Code + PKCE | 挑战码绑定授权码 |
| 会话 | 双令牌 + 轮换 | access 短命、refresh 可吊销 |
| 多 Server | AuthStore + 刷新防抖 | 独立令牌、最小 scope |
鉴权是远程 MCP 的安全地基:动态注册解决「谁都能连」,PKCE 解决「没有 secret 的证明」,双令牌轮换解决「会话的生命周期」,多 Server 协调解决「Agent 连接多个服务的现实」。把这四层做扎实,远程 MCP 才能真正安全地暴露给外部客户端。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。