Semantic Kernel 中的 MCP OAuth 认证实战:基于 RFC 9728 的授权服务器与资源服务器分离方案
2026/9/12 12:30:02 网站建设 项目流程

Semantic Kernel 中的 MCP OAuth 认证实战:基于 RFC 9728 的授权服务器与资源服务器分离方案

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

本文围绕仓库内python/samples/demos/mcp_with_oauth演示项目,系统讲解如何在 Semantic Kernel 中通过 OAuth 2.0 认证安全地连接 Model Context Protocol(MCP)服务器。该示例按 RFC 9728 规范将授权服务器(Authorization Server, AS)与资源服务器(Resource Server, RS)分离,完整覆盖从启动 AS、启动受保护的 MCP 资源服务器,到让 SK Agent 自动完成 OAuth 授权码流程并调用受保护工具的全过程。读完本文,你将掌握 MCP OAuth 认证的完整运行命令、客户端认证组件的接入方式,以及认证背后的源码级实现原理。


一、为什么需要 OAuth 保护 MCP 服务器

MCP 服务器向外暴露工具(Tool)与资源(Resource),一旦这些工具涉及用户私有数据或敏感系统信息,就必须验证调用者的身份。OAuth 2.0 是业界标准的授权协议,而 MCP 规范基于 RFC 9728(Protected Resource Metadata for OAuth 2.0)推荐了一种新的部署形态:

  • 授权服务器(AS):独立负责客户端注册、授权码签发、令牌发放与令牌检查(Introspection),不承载业务数据;
  • 资源服务器(RS):即实际的 MCP 服务器,负责执行业务工具,但只信任 AS 签发的令牌。

这种 AS/RS 分离的架构可以类比"身份证签发机构"与"门禁系统"的关系:门禁只负责核验身份证真伪,不负责发证。对企业而言,AS 可以复用已有的身份体系(如 Auth0、Entra ID 等),多个 MCP 资源服务器共用同一个 AS,实现统一认证与集中管控。

本演示(README)的核心代码源自官方 MCP Python SDK 的simple-auth示例,服务端与部分客户端认证代码均基于该示例改造而来,目的是演示 SK 客户端如何与这类受 OAuth 保护的 MCP 服务器完成连接。


二、演示架构全景

整个演示由三个进程组成,端口分配如下:

组件端口角色启动命令
Authorization Server9000客户端注册、授权、令牌签发、/introspect令牌检查uv run mcp-simple-auth-as --port=9000
Resource Server(MCP Server)8001提供受保护的 MCP 工具,向 AS 校验令牌uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
SK Agent 客户端3030(回调)通过MCPStreamableHttpPlugin连接 RS,并处理 OAuth 回调uv --env-file .env run agent

认证链路为:SK 客户端 → 资源服务器(8001)→ 授权服务器(9000)。RS 自身不存储用户与令牌,而是把令牌交给 AS 的/introspect端点校验(RFC 7662 Token Introspection),校验通过后才允许调用工具。


三、分步运行完整指南

Step 1:启动授权服务器(AS)

# 进入 simple-auth 目录 cd samples/demos/mcp_with_oauth/server # 在 9000 端口启动授权服务器 uv run mcp-simple-auth-as --port=9000

AS 提供的能力(详见 auth_server.py):

  • OAuth 2.0 完整流程:客户端注册(/register)、授权(/authorize)、令牌交换(/token);
  • 基于简单凭据的认证:内置演示账号demo_user / demo_password,无需外部身份提供商;
  • 令牌检查端点:为资源服务器提供 RFC 7662 风格的/introspect端点,RS 无需直接访问令牌存储即可校验令牌有效性。

--port参数默认值为 9000,服务启动后监听http://localhost:9000,同时挂载/login(登录页)与/login/callback(登录回调)两个路由。

Step 2:启动资源服务器(MCP Server)

在另一个终端中执行:

# 在另一个终端,进入 simple-auth 目录 cd samples/demos/mcp_with_oauth/server # 在 8001 端口启动资源服务器,并连接到授权服务器,使用 streamable-http 传输 uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http # 生产环境推荐:启用 RFC 8707 严格资源校验 uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http --oauth-strict

资源服务器命令行参数(定义见 server.py):

参数默认值说明
--port8001资源服务器监听端口
--auth-serverhttp://localhost:9000授权服务器地址,资源服务器会据此推导 introspection 端点为{auth-server}/introspect
--transportstreamable-http传输协议,可选ssestreamable-http
--oauth-strict关闭开启后启用 RFC 8707 资源(audience)严格校验,生产环境推荐开启

资源服务器暴露的唯一工具是get_time,返回服务器当前时间。它的存在是为了演示:未经 OAuth 认证的调用者无法访问该工具(见 server.py 中的工具定义与注释)。

Step 3:配置客户端并运行 Agent

客户端使用 Azure OpenAI 作为 Agent 的对话服务。有两种配置方式:

方式一:使用全局 venv 中已有的 Azure 配置;

方式二:在samples/demos/mcp_with_oauth/agent目录下创建.env文件,填入以下内容:

AZURE_OPENAI_ENDPOINT= AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=

然后运行:

cd samples/demos/mcp_with_oauth # 使用 streamable HTTP 插件启动 agent,并加载 .env 中的 Azure 配置 uv --env-file .env run agent

也可以直接打开 agent/main.py 在 IDE 中运行。Agent 启动后会自动打开浏览器跳转到授权页面,使用演示账号demo_user / demo_password登录并完成授权,随后向 Agent 提问"What time is it?",Agent 将调用受保护的get_time工具并返回当前时间。


四、客户端源码解析:SK 如何接入 OAuth 认证

客户端代码(agent/main.py)展示了 SK 连接受保护 MCP 服务器的完整模式,核心是三个自定义组件 + 一个插件。

4.1 令牌存储:InMemoryTokenStorage

OAuth 客户端需要持久化令牌与客户端注册信息,SK 通过TokenStorage抽象来管理:

class InMemoryTokenStorage(TokenStorage): """Simple in-memory token storage implementation.""" def __init__(self): self._tokens: OAuthToken | None = None self._client_info: OAuthClientInformationFull | None = None async def get_tokens(self) -> OAuthToken | None: return self._tokens async def set_tokens(self, tokens: OAuthToken) -> None: self._tokens = tokens async def get_client_info(self) -> OAuthClientInformationFull | None: return self._client_info async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: self._client_info = client_info

示例采用内存存储,进程结束后令牌即丢失。接入真实场景时,应把TokenStorage换成基于数据库或文件系统的实现,让令牌跨进程、跨会话复用(尤其要持久化 refresh token)。

4.2 本地回调服务器:CallbackServer

OAuth 授权码流程要求客户端提供一个回调地址接收授权码。示例在本地 3030 端口启动了一个轻量 HTTP 服务器(CallbackServer),在后台线程中监听http://localhost:3030/callback

  • 收到code参数:提取授权码与state,返回 "Authorization Successful!" 页面并自动关闭窗口;
  • 收到error参数:记录错误并返回失败页面;
  • 其他请求:返回 404。

wait_for_callback(timeout=300)会阻塞等待授权码,超时或出错则抛出异常(main.py)。

4.3 组装 OAuth 客户端提供者

client_metadata_dict = { "client_name": "Simple Auth Client", "redirect_uris": ["http://localhost:3030/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "client_secret_post", } async def _default_redirect_handler(authorization_url: str) -> None: """Default redirect handler that opens the URL in a browser.""" print(f"Opening browser for authorization: {authorization_url}") webbrowser.open(authorization_url) oauth_auth = OAuthClientProvider( server_url="http://localhost:9000", client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict), storage=InMemoryTokenStorage(), redirect_handler=_default_redirect_handler, callback_handler=callback_handler, )

关键参数说明:

参数作用
server_url授权服务器地址(http://localhost:9000),客户端据此发现授权端点
client_metadata客户端元数据,声明重定向 URI、支持的授权类型(授权码 + 刷新令牌)与令牌端点认证方式(client_secret_post
storage令牌与客户端信息存储
redirect_handler拿到授权 URL 后如何跳转,示例用webbrowser.open打开浏览器
callback_handler等待并返回授权码与 state 的异步回调

4.4 通过MCPStreamableHttpPlugin连接受保护服务器

async with MCPStreamableHttpPlugin( name="AuthServer", description="Auth Server Plugin", url="http://localhost:8001/mcp", auth=oauth_auth, timeout=timedelta(seconds=60), ) as oath_plugin: agent = ChatCompletionAgent( service=AzureChatCompletion(credential=AzureCliCredential()), name="ProtectedAgent", instructions="Answer the users questions.", plugins=[oath_plugin], ) ... response = await agent.get_response(messages=user_input, thread=thread)

MCPStreamableHttpPlugin定义于 mcp.py,构造参数包括name(插件名)、url(MCP 服务器地址)、descriptionload_tools/load_prompts(是否加载 MCP 工具与提示词,默认均为True)、request_timeouttimeoutheaders等,未识别的关键字参数会透传给底层streamablehttp_client——示例中的auth=oauth_auth正是通过这一透传机制注入到 MCP 传输层,使插件在连接资源服务器时自动完成 OAuth 流程。

工具调用成功后,输出类似(见 main.py 中的预期输出注释):

🖥️ Started callback server on http://localhost:3030 Opening browser for authorization: http://localhost:9000/authorize?response_type... ⏳ Waiting for authorization callback... # User: What time is it? # ProtectedAgent: The current time is 16:54:55 (4:54 PM) on July 10, 2025, in the UTC timezone.

五、服务端原理:授权服务器与资源服务器如何协同

5.1 授权服务器:凭据登录 → 授权码 → 访问令牌

授权服务器实现于 auth_server.py 与 simple_auth_provider.py,其完整授权链路由以下步骤构成:

  1. 客户端注册register_client把客户端元数据存入内存字典,供后续校验;
  2. 发起授权authorize生成随机state并保存redirect_uricode_challengeresource(RFC 8707)等上下文,返回指向/login的登录页地址;
  3. 登录校验/login/callback收到表单提交的用户名密码与 state,与SimpleAuthSettings中的演示凭据比对(默认demo_user / demo_password,可通过MCP_DEMO_USERNAMEMCP_DEMO_PASSWORD环境变量覆盖),校验通过后生成mcp_{hex}格式的授权码(有效期 300 秒),并重定向回客户端回调地址;
  4. 令牌交换exchange_authorization_code用授权码换取访问令牌,令牌格式为mcp_{32位hex},有效期 3600 秒,同时记录令牌与用户、resource 的映射;
  5. 刷新令牌:示例刻意未实现(load_refresh_token返回Noneexchange_refresh_token抛出NotImplementedError),仅支持授权码流程。

5.2 令牌检查端点/introspect

资源服务器需要一种不直接访问令牌存储的校验方式,AS 因此暴露了 RFC 7662 风格的 introspection 端点(auth_server.py):

async def introspect_handler(request: Request) -> Response: form = await request.form() token = form.get("token") if not token or not isinstance(token, str): return JSONResponse({"active": False}, status_code=400) access_token = await oauth_provider.load_access_token(token) if not access_token: return JSONResponse({"active": False}) return JSONResponse({ "active": True, "client_id": access_token.client_id, "scope": " ".join(access_token.scopes), "exp": access_token.expires_at, "iat": int(time.time()), "token_type": "Bearer", "aud": access_token.resource, # RFC 8707 audience claim })

响应中的active字段是校验结果的核心:令牌不存在或已过期(超过 3600 秒)都会返回active: falseaud字段回传令牌签发时的 resource,供 RS 做 RFC 8707 严格校验。

5.3 资源服务器:使用 IntrospectionTokenVerifier 校验令牌

资源服务器(server.py)通过IntrospectionTokenVerifier(token_verifier.py)完成令牌校验,校验逻辑:

  1. 防 SSRF:拒绝指向非https://、非localhost/127.0.0.1的 introspection 端点;
  2. 安全 HTTP 客户端:设置 10 秒超时、连接池上限与强制 SSL 校验;
  3. 调用 introspection 端点:POSTtoken字段到{auth-server}/introspect,状态码非 200 或active为 false 则拒绝;
  4. RFC 8707 资源校验(仅在--oauth-strict开启时生效):对比令牌aud声明与 RS 自身的 resource URL,使用check_resource_allowed进行层级匹配;无aud声明的令牌直接判定无效。

从该实现可以推断,生产环境的令牌校验还应补充连接池复用、限流与重试、更完善的错误处理等能力(源码注释中也明确列出了这些待办项)。此外,ResourceServerSettings支持MCP_RESOURCE_前缀的环境变量覆盖(如MCP_RESOURCE_OAUTH_STRICT),便于容器化部署时注入配置。


六、工程实践要点

6.1 目录与入口速查

  • 客户端:agent/main.py,入口agent定义于 pyproject.toml 的[project.scripts]agent = "agent.main:cli");
  • 授权服务器:auth_server.py,入口mcp-simple-auth-as
  • 资源服务器:server.py,入口mcp-simple-auth-rs
  • 依赖声明:客户端依赖semantic-kernel[mcp]click;服务端依赖mcppydanticstarletteuvicorn等,详见 server/pyproject.toml。

6.2 将演示落地为生产方案的清单

  1. 替换令牌存储:将InMemoryTokenStorage换为持久化实现,支持 refresh token 复用,避免每次启动重复授权;
  2. 对接企业身份体系:AS 的SimpleOAuthProvider只是演示实现,生产环境应替换为 Auth0、Entra ID 等企业级授权服务器;
  3. 强制开启严格校验:资源服务器务必启用--oauth-strict,确保令牌携带正确的 resource(audience);
  4. 传输与网络安全:演示使用本地明文 HTTP,生产环境应使用 HTTPS 并部署在可信网络边界内;
  5. 服务端安全加固IntrospectionTokenVerifier的注释明确指出生产实现应考虑连接池复用、限流重试与更细化的配置。

6.3 延伸阅读

本演示只展示了 MCP 客户端认证的一条路径。仓库中还有更多 MCP 与 SK 集成的样例可对照阅读:

  • agent_with_http_mcp_plugin.py:无需认证的 streamable HTTP MCP 插件连接方式,可与本文对比理解auth参数的差异;
  • mcp_as_plugin.py 与 agent_with_mcp_agent.py:其他 MCP 接入形态;
  • agent_with_mcp_sampling.py:MCP sampling 机制的授权控制。

七、总结

本演示项目完整呈现了 OAuth 2.0 保护下的 MCP 服务器接入 Semantic Kernel 的标准路径:独立授权服务器签发与检查令牌、资源服务器通过 introspection 校验令牌、SK 客户端通过OAuthClientProviderMCPStreamableHttpPlugin自动完成授权码流程。掌握这一模式后,你可以将任何符合 RFC 9728/7662/8707 规范的受保护 MCP 服务器无缝接入 SK Agent,让大模型应用在获得 MCP 工具生态能力的同时,守住身份认证与授权这道安全边界。

【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询