MCP 的 OAuth 鉴权与会话:动态注册、PKCE 与令牌轮换

系统讲解 MCP 的 OAuth 2.0 鉴权与会话管理:授权码流程 + PKCE、动态客户端注册(RFC 7591)、访问/刷新令牌与轮换、多 Server 的鉴权协调、令牌存储与安全边界,以及无 OAuth(本地 stdio)与远程场景的鉴权选型。

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 HTTPOAuth 2.0 + PKCE
内部服务HTTPOAuth / 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 可吊销
多 ServerAuthStore + 刷新防抖独立令牌、最小 scope

鉴权是远程 MCP 的安全地基:动态注册解决「谁都能连」,PKCE 解决「没有 secret 的证明」,双令牌轮换解决「会话的生命周期」,多 Server 协调解决「Agent 连接多个服务的现实」。把这四层做扎实,远程 MCP 才能真正安全地暴露给外部客户端。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「AI工程」更多文章

  1. MCP 资源模板与订阅:URI 模板、ListChanged 通知与上下文注入
  2. MCP 服务器测试框架:in-memory 传输、协议断言与端到端测试
  3. MCP 客户端 SDK 深入:TypeScript 客户端 API、传输状态机与错误处理