第 07 篇:OAuth 2.1 与授权架构 —— AS/RS 分离的正确姿势

本篇是《MCP 开发实战教程》专栏的第 7 篇。上一篇我们实现了数据源的安全连接,但认证部分用的是最简单的方案------环境变量里的静态密码。本篇将带你理解 MCP 的正式认证体系:OAuth 2.1、AS/RS 分离、PKCE、动态客户端注册------让你的 MCP Server 支持企业级的 SSO 集成。

引言

你可能有过这种体验:你的 MCP Server 在团队内部用得好好的,有一天领导说"能不能让客户也用?"你心想,加个 API Key 不就行了?然后安全团队找到你:"不行,要走 OAuth。"你开始查 OAuth 文档,发现 OAuth 2.0 有四种授权模式,OAuth 2.1 又改了一堆东西,MCP 规范还要求什么 RFC 9728、RFC 8707......越看越懵。

上一篇我们用环境变量管理数据库密码,这在内部使用时没问题。但当你的 MCP Server 需要:

  • 服务多个不同的客户端(Claude Desktop、Cursor、自研 Agent)
  • 代表不同用户执行操作
  • 与企业 SSO 系统集成
  • 满足合规要求(审计谁在什么时候访问了什么)

你就需要一个正式的认证和授权体系。MCP 规范选择了 OAuth 2.1 作为标准。

本篇会带你搞清楚四件事:为什么选 OAuth 2.1、AS/RS 分离是什么意思、MCP 的 OAuth 流程长什么样、以及怎么用 FastMCP 实现它。

读完本篇,你将获得:

  • 理解 MCP OAuth 2.1 授权架构的设计动机
  • 掌握 AS/RS 分离、RFC 9728、RFC 8707 的核心概念
  • 获得一个可运行的 OAuth 保护 MCP Server 实现

1. 为什么是 OAuth 2.1

1.1 MCP 的认证需求

MCP Server 的认证需求与传统 Web API 不同:

特点 说明
多客户端 同一个 Server 要服务 Claude Desktop、Cursor、自研 Agent 等多个客户端
多用户 每个客户端代表不同的用户执行操作
动态发现 客户端需要自动发现 Server 支持的认证方式
桌面应用 很多 MCP 客户端是桌面应用,不是 Web 应用

OAuth 2.1 天然解决了这些问题------它就是为"多客户端、多用户、第三方授权"设计的。

1.2 OAuth 2.1 vs OAuth 2.0

OAuth 2.1 是 OAuth 2.0 的安全加固版,主要变化:

变化 OAuth 2.0 OAuth 2.1
PKCE 可选(仅公开客户端) 强制所有客户端
隐式授权 支持 完全移除
密码授权 支持 完全移除
重定向 URI 模糊匹配 精确匹配
刷新令牌 无特殊要求 一次性使用或绑定客户端

这些变化对 MCP 来说很重要:

  • PKCE 强制 → 防止授权码被截获(桌面应用的常见风险)
  • 隐式授权移除 → 不再有 Token 出现在 URL 中的安全隐患
  • 精确重定向 → 防止开放重定向攻击

1.3 角色定义

OAuth 2.1 定义了四个角色,在 MCP 中的对应:

OAuth 角色 MCP 中的对应 说明
Resource Owner(资源所有者) 用户 拥有数据的人
Client(客户端) MCP Client 代表用户请求访问
Authorization Server(授权服务器, AS) 外部 IdP 负责认证用户、颁发令牌
Resource Server(资源服务器, RS) MCP Server 持有资源,验证令牌

关键设计:AS 和 RS 是分离的 。MCP Server(RS)不自己做认证,而是委托给专门的授权服务器(AS)。这就是所谓的 AS/RS 分离


2. AS/RS 分离:为什么要分开

2.1 传统方式的问题

很多开发者第一次实现认证时,会把认证逻辑直接放在 MCP Server 里:

python 复制代码
# 不推荐:Server 自己做认证
@mcp.tool()
def get_data(token: str) -> str:
    """获取数据"""
    user = verify_token(token)  # Server 自己验证 Token
    if not user:
        return "认证失败"
    return query_data(user)

这种方式的问题:

问题 说明
无法支持 SSO 每个 Server 自己管认证,用户要登录多次
Token 管理复杂 Server 要自己处理 Token 生成、刷新、撤销
安全责任不清 认证逻辑分散在各个 Server 中,难以审计
扩展困难 新增 Server 要重新实现认证

2.2 AS/RS 分离的架构

AS/RS 分离后,职责清晰:

arduino 复制代码
用户
 │
 ├─→ Client ──→ AS(授权服务器)── 颁发 Token
 │                   │
 │                   ↓
 └─→ Client ──→ RS(MCP Server)── 验证 Token ──→ 数据
组件 职责
AS 认证用户、颁发 Token、管理客户端注册、支持 SSO
RS(MCP Server) 验证 Token、提供数据、不关心用户怎么登录
Client 引导用户到 AS 登录、获取 Token、用 Token 访问 RS

MCP Server 只需要做两件事:

  1. 告诉 Client "我的 AS 在哪里"(通过 RFC 9728)
  2. 验证 Client 带来的 Token 是否有效

认证的复杂性全部由 AS 承担。你可以用现有的 IdP 服务(Keycloak、Auth0、WorkOS、Okta、Azure AD),不需要自己实现。

2.3 AS/RS 分离的好处

好处 说明
SSO 集成 用户登录一次,所有 MCP Server 都能用
集中管理 认证策略、用户管理、审计日志都在 AS 中
安全专业性 IdP 服务商有专业的安全团队维护
合规友好 审计时只需要检查 AS,不需要检查每个 Server

3. MCP OAuth 流程:五步走

MCP 的 OAuth 流程有五个阶段。前两个阶段每个 Client-Server 对只执行一次,后三个阶段每次获取新 Token 都会执行。

3.1 第一步:Server 元数据发现

Client 首先需要知道 MCP Server 的 AS 在哪里。它向 Server 的 well-known 端点发送请求:

ruby 复制代码
GET https://mcp-server.example.com/.well-known/oauth-protected-resource

Server 返回(RFC 9728 Protected Resource Metadata):

json 复制代码
{
  "resource": "https://mcp-server.example.com",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["mcp:read", "mcp:tools", "mcp:prompts"],
  "bearer_methods_supported": ["header"]
}

关键字段:

  • resource:这个 MCP Server 的标识
  • authorization_servers:AS 的地址
  • scopes_supported:支持的权限范围

然后 Client 向 AS 发现元数据:

sql 复制代码
GET https://auth.example.com/.well-known/oauth-authorization-server

AS 返回(RFC 8414 Authorization Server Metadata):

json 复制代码
{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "registration_endpoint": "https://auth.example.com/register",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"]
}

这个"发现三部曲"(RFC 9728 + RFC 8414 + RFC 7591)让 Client 可以自动发现认证流程需要的所有端点,不需要任何硬编码配置。

3.2 第二步:动态客户端注册

Client 需要在 AS 注册自己,获取 client_id。MCP 规范支持 Dynamic Client Registration(RFC 7591)------Client 可以在运行时自动注册,不需要提前手动配置。

bash 复制代码
POST https://auth.example.com/register
Content-Type: application/json

{
  "client_name": "Claude Desktop",
  "redirect_uris": ["http://localhost:9090/callback"],
  "grant_types": ["authorization_code"],
  "scope": "mcp:read mcp:tools",
  "token_endpoint_auth_method": "none"
}

AS 返回:

json 复制代码
{
  "client_id": "abc123",
  "client_name": "Claude Desktop",
  "redirect_uris": ["http://localhost:9090/callback"],
  "grant_types": ["authorization_code"]
}

安全注意 :动态注册意味着任何 Client 都可以注册。对于内部 Server,你可能需要限制注册来源或要求预共享注册令牌。MCP 规范引入了 Client ID Metadata Documents(CIMD) 作为 DCR 的替代方案,提供更好的安全性。

3.3 第三步:授权请求(带 PKCE)

Client 生成 PKCE 参数,引导用户到 AS 登录:

python 复制代码
import hashlib
import base64
import secrets

# 生成 PKCE 参数
code_verifier = secrets.token_urlsafe(64)  # 43-128 字符的随机字符串
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()

Client 构造授权 URL,引导用户浏览器打开:

ini 复制代码
https://auth.example.com/authorize
  ?response_type=code
  &client_id=abc123
  &redirect_uri=http://localhost:9090/callback
  &scope=mcp:read mcp:tools
  &state=xyz789
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &resource=https://mcp-server.example.com

注意 resource 参数(RFC 8707)------它告诉 AS 这个 Token 是为哪个 MCP Server 颁发的。AS 会将这个信息绑定到 Token 中,RS 验证时可以检查 Token 的目标是否是自己。

用户在 AS 页面登录并授权后,AS 重定向回 Client:

bash 复制代码
http://localhost:9090/callback?code=auth_code_here&state=xyz789

3.4 第四步:Token 请求

Client 用授权码换取 Access Token:

ini 复制代码
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=auth_code_here
&redirect_uri=http://localhost:9090/callback
&client_id=abc123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&resource=https://mcp-server.example.com

AS 验证 PKCE 和授权码后,返回 Token:

json 复制代码
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
  "scope": "mcp:read mcp:tools"
}

3.5 第五步:资源访问

Client 用 Access Token 访问 MCP Server:

bash 复制代码
POST https://mcp-server.example.com/mcp
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json

{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}

MCP Server 验证 Token:

  1. 检查 Token 签名是否有效
  2. 检查 Token 是否过期
  3. 检查 Token 的 aud(audience)是否是自己(RFC 8707 绑定)
  4. 检查 Token 的 scope 是否允许该操作

验证通过后,Server 处理请求并返回结果。


4. 实战:用 FastMCP 实现 OAuth 保护

4.1 FastMCP 内置 OAuth 支持

FastMCP 提供了开箱即用的 JWT Token 验证支持(需要 FastMCP v2.11.0+)。最简单的方式是使用 JWTVerifier

python 复制代码
"""OAuth 保护的 MCP Server"""

import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier

# 配置 JWT 验证器
verifier = JWTVerifier(
    jwks_uri=os.environ["JWKS_URI"],           # AS 的 JWKS 端点
    issuer=os.environ["TOKEN_ISSUER"],          # Token 颁发者
    audience=os.environ.get("TOKEN_AUDIENCE"),  # Token 受众(可选)
)

mcp = FastMCP("安全数据服务", auth=verifier)


@mcp.tool()
def search_users(keyword: str) -> str:
    """搜索用户"""
    # Token 验证由中间件自动完成
    # 这里只处理业务逻辑
    results = db.search_users(keyword)
    return format_results(results)


@mcp.tool()
def get_user_detail(user_id: int) -> str:
    """获取用户详情"""
    user = db.get_user(user_id)
    if not user:
        return f"未找到用户 #{user_id}"
    return format_user(user)


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

JWTVerifier 自动完成:

  • 从 JWKS 端点获取 AS 的公钥
  • 验证 Token 签名(支持 RS256、ES256 等算法)
  • 检查 Token 是否过期
  • 验证 issueraudience

注意JWTVerifier 不直接支持 required_scopes 参数。如果需要基于 scope 的权限控制,可以在工具层面检查 Token 中的 scope 字段。

4.2 多 Provider 集成

FastMCP 的 JWTVerifier 是通用的------任何支持 JWKS 的 IdP 都可以直接使用。此外,部分 Provider 有专门的集成模块:

Provider 集成方式
Auth0 JWTVerifier + Auth0 JWKS URI
WorkOS 专门的 DCR 集成(RemoteAuthProvider
Descope 专门的集成模块
Keycloak JWTVerifier + Keycloak JWKS URI
Azure AD / Entra ID JWTVerifier + Azure JWKS URI
AWS Cognito JWTVerifier + Cognito JWKS URI

以 Auth0 为例:

python 复制代码
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier

verifier = JWTVerifier(
    jwks_uri=f"https://{os.environ['AUTH0_DOMAIN']}/.well-known/jwks.json",
    issuer=f"https://{os.environ['AUTH0_DOMAIN']}/",
    audience=os.environ["AUTH0_AUDIENCE"],
)

mcp = FastMCP("Auth0 保护的服务器", auth=verifier)

以 Keycloak 为例:

python 复制代码
import os
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier

verifier = JWTVerifier(
    jwks_uri=f"{os.environ['KEYCLOAK_URL']}/realms/{os.environ['KEYCLOAK_REALM']}/protocol/openid-connect/certs",
    issuer=f"{os.environ['KEYCLOAK_URL']}/realms/{os.environ['KEYCLOAK_REALM']}",
    audience=os.environ.get("KEYCLOAK_CLIENT_ID"),
)

mcp = FastMCP("Keycloak 保护的服务器", auth=verifier)

4.3 手动 Token 验证

如果你需要更细粒度的控制,可以手动验证 Token:

python 复制代码
import jwt
import httpx
from functools import lru_cache


@lru_cache(maxsize=1)
def get_jwks(jwks_uri: str) -> dict:
    """获取并缓存 JWKS"""
    response = httpx.get(jwks_uri)
    return response.json()


def verify_token(token: str, jwks_uri: str, issuer: str, audience: str) -> dict | None:
    """手动验证 JWT Token"""
    try:
        # 获取 JWKS
        jwks = get_jwks(jwks_uri)

        # 解码 Token header 获取 kid
        unverified = jwt.get_unverified_header(token)
        kid = unverified.get("kid")

        # 查找对应的公钥
        key = None
        for jwk in jwks.get("keys", []):
            if jwk.get("kid") == kid:
                key = jwt.algorithms.RSAAlgorithm.from_jwk(jwk)
                break

        if not key:
            return None

        # 验证 Token
        payload = jwt.decode(
            token,
            key,
            algorithms=["RS256"],
            issuer=issuer,
            audience=audience,
        )
        return payload

    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

5. 深入理解:关键 RFC 的作用

5.1 RFC 9728:Protected Resource Metadata

RFC 9728 定义了 RS(MCP Server)如何告诉 Client "我的 AS 在哪里"。

MCP Server 在 /.well-known/oauth-protected-resource 端点返回元数据,Client 据此知道去哪里认证。这解决了"鸡生蛋"问题------Client 在认证之前就需要知道 AS 的地址。

5.2 RFC 8707:Resource Indicators

RFC 8707 解决了"Token 绑定"问题。Client 在授权请求和 Token 请求中都包含 resource 参数,AS 将 Token 绑定到特定的 RS。

这防止了混淆代理人攻击(Confused Deputy Attack):

arduino 复制代码
场景:用户授权了 GitHub MCP Server 的 Token
攻击:恶意 Server 试图用这个 Token 访问 GitHub API

没有 RFC 8707:
  Token → 任何 Server 都能用 → 攻击成功

有 RFC 8707:
  Token(绑定 github.com)→ 恶意 Server 验证 audience 不匹配 → 拒绝

当前状态resource 参数的支持取决于 AS 的实现。MCP 规范要求 Client 必须发送 resource,但 AS 可以选择忽略它。MCP Steering Committee 正在推动主流 IdP(Okta、Entra ID、AWS)支持这个参数。

5.3 PKCE:防授权码截获

PKCE(Proof Key for Code Exchange)防止授权码在传输过程中被截获。即使攻击者获取了授权码,没有 code_verifier 也无法换取 Token。

这对 MCP 桌面客户端特别重要------桌面应用的本地回调端口可能被其他应用监听。PKCE 确保只有发起授权请求的 Client 才能完成 Token 交换。


6. 常见问题与踩坑记录

Q1: 内部使用的 MCP Server 也需要 OAuth 吗?

不一定。如果你的 Server 只在本地运行(STDIO 传输),不需要 OAuth------STDIO 的安全性来自进程隔离。但如果你的 Server 通过 HTTP 暴露(即使是内网),建议加上 OAuth,因为 HTTP 端口可能被意外暴露。

Q2: 动态客户端注册安全吗?

有风险。默认情况下,任何 Client 都可以注册。建议:

  • 对内部 Server:限制注册来源或要求预共享注册令牌
  • 对公开 Server:使用 CIMD(Client ID Metadata Documents)替代传统 DCR
  • 对企业环境:使用管理员预注册客户端

Q3: 我可以用 API Key 代替 OAuth 吗?

可以,但功能有限。API Key 只能标识"哪个应用在调用",不能标识"代表哪个用户"。如果你的 Server 需要区分不同用户的数据访问权限,必须用 OAuth。

Q4: Token 过期后怎么办?

Client 使用 refresh_token 获取新的 access_token,不需要用户重新登录。MCP 的 OAuth 流程中,Token 刷新对用户是透明的。

Q5: 如何选择 IdP?

场景 推荐 IdP
企业内部 Keycloak(开源)、Azure AD、Okta
SaaS 产品 Auth0、WorkOS、Stytch
AWS 生态 Cognito
快速原型 WorkOS(免费层慷慨)或 Keycloak

总结

  1. OAuth 2.1 是 MCP 的认证标准:它解决了 MCP 的多客户端、多用户、动态发现需求。PKCE 强制、隐式授权移除、精确重定向等安全加固让桌面应用场景更安全。

  2. AS/RS 分离是核心设计:MCP Server(RS)不自己做认证,而是委托给专门的授权服务器(AS)。这让 SSO 集成、集中管理、合规审计都成为可能。

  3. 五步流程自动完成:Server 元数据发现 → 动态客户端注册 → 授权请求(带 PKCE)→ Token 请求 → 资源访问。Client 和 Server 通过 well-known 端点自动发现对方的能力。

  4. RFC 9728 和 RFC 8707 是关键:RFC 9728 让 Client 发现 Server 的 AS,RFC 8707 让 Token 绑定到特定 Server。两者共同防止了混淆代理人攻击。

  5. FastMCP 让实现变简单TokenVerifier 中间件自动处理 Token 验证,支持 Auth0、WorkOS、Keycloak 等多种 Provider。几行代码就能给 MCP Server 加上企业级认证。

下篇预告

认证解决了"你是谁"的问题,下一篇解决"怎么高效访问"的问题。《MCP 网关》将带你了解 Cloudflare MCP Server Portals、IBM/Azure 等企业级网关方案------让 MCP Server 的部署、扩展、监控变得简单。


附录:参考资料

相关推荐
闵孚龙1 小时前
PyTorch 系列 之 nn.Module:所有模型的骨架
人工智能·pytorch·python
海天一色y1 小时前
深入理解 Function Calling、MCP 与 Skills:AI Agent 的三层能力架构
人工智能·mcp·skills
小星AI1 小时前
FastMCP 2.0 实战:10 分钟给 Claude Code 装上手
人工智能·agent
昨日之日20061 小时前
Higgs Audio v3 - 超自然多语言情感TTS,一键克隆声音 一键整合包下载
人工智能·音视频
极客老王说Agent1 小时前
2026全业务链条断层破解:智能体如何重构端到端业务闭环
人工智能·ai·chatgpt·重构
云烟成雨TD1 小时前
Spring AI 1.x 系列【61】Spring AI 2.0 升级指南
java·人工智能·spring
Luhui Dev1 小时前
几何图,现在可以用 API 一句话生成
人工智能·数学·luhuidev
咕咕AI学堂2 小时前
大模型应用开发:Prompt Engineering 从经验法则到工程化实践
人工智能
名不经传的养虾人2 小时前
从0到1:企业级AI项目迭代日记 Vol.47|从“能说”到“能上手”
大数据·人工智能·ai编程·企业ai·多agent协作