Dify MCP 集成实验(05):认证体系——MCP Server 如何做企业级认证?
1. 业务场景
先讲一个我们实际遇到的场景。
一家做客服工单 SaaS 的公司,要接客户现场的真实数据源(mock ERP)——但客户的安全评审第一关就是认证:没有认证的 MCP 接入根本过不了。企业数据源的鉴权有三种典型场景,由简到繁:内部服务用预共享密钥(header 静态 token)、系统级服务账号(client_credentials)、用户级授权(OAuth 2.1 授权码 + PKCE——管理员授权后,普通用户查询走已授权 token)。
我们第一次接这类需求时,第一反应是「认证不就是加个 header 嘛」。真正动手才发现——认证是三层模式、失败形态、token 生命周期的组合题:OAuth 授权码流程涉及 Console 配置入口、回调地址、授权页面、刷新触发,每一步没实测过,客户问细节就露怯。我们把三种模式全部跑通,把失败形态整理成表,才敢写进服务描述。
这不是个例。任何「企业客户有真实数据源要接」的集成都是这个模式:源码确认了认证层实现(PKCE S256 / 三种 grant type / MCPClientWithAuthRetry),但实操流程(Console 配置入口、回调地址、授权页面、刷新触发)全部未实测——不实测,交付时就是两眼一抹黑。
2. 场景痛点
这个流程的痛点,在认证落地时体现得最直接:
- 安全评审过不了:无认证的 MCP 接入在客户现场根本过不了安全评审——方案再漂亮,第一关就卡死。
- OAuth 流程是最大未知点:Console 配置入口、回调地址、授权页面、刷新触发——实操流程全没验证过,客户一问细节就露怯。
- 失败形态说不清:401/403/invalid_grant 报错形态多,分不清是认证失败还是 SSRF 拦截(client_credentials 凭据错会被误判成「blocked by SSRF protection」)——排查没方向。
- 授权后工具空:1.16.1 工具拉取只在 create,授权后 tools 空——配完授权,工具反而「消失」了。
本质上,认证不是「加个头」——三种模式、失败形态、token 生命周期都要实测,才能写进服务描述。
3. 方案:为什么是 SDK 2.0 AuthSettings
实测 Dify 连接 MCP server 的三种鉴权方式全流程:自定义 headers → client_credentials → OAuth 2.1 授权码 + PKCE。
选它的理由:
- SDK 自动托管:
AuthSettings+token_verifier——未授权自动 401 + WWW-Authenticate,自动暴露/.well-known/oauth-protected-resource/mcp(RFC 9728 格式),不用手写认证中间件; - 三模式递进实测:header(内部服务)→ client_credentials(系统级服务账号)→ OAuth 授权码 + PKCE(用户级授权)——覆盖企业数据源的全部典型场景;
- 产出可交付:「Dify MCP 认证三步走」标准操作 + 失败形态表——直接写进服务描述,客户现场照着走。
这篇文章我们就用它给受保护的 ERP
数据源(get_erp_order)配齐三种鉴权方式,走通全流程并沉淀失败形态表。
4. 整体架构
链路很清晰:受保护 server(get_erp_order)↔︎ mock OAuth 服务器(metadata/authorize/token)↔︎ Dify(api 回调端点 + web 鉴权配置)。关键设计是三种鉴权模式在同一个 server 上递进实测,失败形态逐类记录——每种模式都验证到「授权后调用成功」为止。
5. 模块设计
5.1 Server 侧:AuthSettings + token_verifier(SDK 2.0 自动托管)
from mcp.server.mcpserver import MCPServer, AuthSettings
from mcp.server.auth.middleware import token_verifier
server = MCPServer(
name="dify107_05_auth_server", version="1.0.0",
auth=AuthSettings(
issuer_url="http://host.docker.internal:8906",
resource_server_url="http://localhost:8905",
required_scopes=["erp:read"],
),
token_verifier=token_verifier, # 未授权 401 + WWW-Authenticate
)SDK 自动托管
/.well-known/oauth-protected-resource/mcp(RFC 9728
格式,authorization_servers 指向 issuer)。
5.2 三模式实测链路
| 模式 | 流程 |
|---|---|
| 一:自定义 headers | provider 配置 headers
{Authorization: Bearer ***} → 试连带 header 通过 →
工具拉取成功 |
| 二:client_credentials | mock metadata grant_types=[client_credentials] → provider 配 client_id/secret → POST /token(Basic Auth)→ authed |
| 三:OAuth 授权码 + PKCE | DCR 注册 → authorization_url(PKCE S256,state 存 Redis)→ auto-approve 授权 → Dify 回调端点 → token 交换 → authed |
5.3 失败形态表(验收/排查用)
| 场景 | 报错形态 | 排查指引 |
|---|---|---|
| 无 token 调工具 | server 401
{"error":"invalid_token"} → Dify tool failed |
检查 provider authed/headers |
| client_credentials 凭据错 | mock 401 invalid_client → Dify「Client credentials flow failed...blocked by SSRF protection」(误判) | 先本地 curl 复现区分 mock 401 vs squid 403 |
| metadata 缺字段 | Dify「Failed to discover OAuth metadata from server」 | metadata 必须含 authorization_endpoint/token_endpoint/response_types_supported |
| authorize 缺参数 | 400 invalid_request | code_challenge/redirect_uri/state 必填 |
| PKCE 校验失败 | 400 invalid_grant PKCE 校验失败 | code_verifier 与 code_challenge 不匹配 |
| token 过期/无效 | server 401 → MCPClientWithAuthRetry 刷新(refresh_token) | 刷新失败则清凭据重新授权 |
| 授权后工具空 | tools=[],「Tool with name xxx not found」 | 1.16.1 限制:工具拉取只在 create,授权后手动补/重导 |
6. 运行验证
| 验证项 | 预期 | 结果 |
|---|---|---|
| header 鉴权 | 配置后调用成功;缺 header 报错 | 通过 |
| client_credentials | 配置后自动鉴权调用成功 | 通过(auth 200 + 调用 succeeded) |
| OAuth 授权码 + PKCE | 授权流程走通、token 存库、授权后调用成功 | 通过(全自动化,无真实浏览器) |
| token 自动刷新 | 401 触发 MCPClientWithAuthRetry 用 refresh_token 刷新 | 机制源码确认(机制确认,端到端过期刷新未实测) |
| 失败形态表 | 5 类以上失败形态 + 排查指引 | 通过(见 4.3) |
| 认证能力清单 | OAuth 2.1 + PKCE / client_credentials / refresh 自动刷新 / DCR / 资源发现 | 通过 |
7. 实战坑
| 坑 | 现象 | 修复 |
|---|---|---|
| AccessToken 必填 client_id | SDK 2.0 token_verifier 返回 AccessToken 缺 client_id 报 ValidationError | token 校验/构造时带上 client_id(实测) |
| metadata 必填字段 | client_credentials 模式 metadata 缺 authorization_endpoint/response_types 也报错 | Dify OAuthMetadata 模型强制,grant_types 决定模式(实测) |
| client_credentials 用 Basic Auth | mock /token 只读 form → invalid_client 401 → 被 ssrf_proxy 误判「SSRF blocked」 | 解析
Authorization: Basic ***;先本地 curl 复现区分(实测) |
| 回调端点完成交换 | POST auth 传 code 报「State parameter is required」 | code 必须走回调端点 GET /console/api/mcp/oauth/callback?code&state(state 从 Redis 取 code_verifier)(实测) |
| 授权后 tools 不刷新 | 1.16.1 工具拉取只在 create,授权后 tools 空 | 手动补 DB tools 字段(含 outputSchema 才字段展开)或删了重导(实测) |
| token 校验与 store | mock 重启 TOKEN_STORE(内存)清空 → Dify DB token server 不认 → 401 | 演示环境放宽前缀校验;生产用共享存储/introspection(实测) |
| sync/async 坑 | sync def 端点里 request.json()/form() 是 async | 必须 async def + await(实测) |
8. 实验文档及源码获取
- 实验文档(完整操作步骤):DIFY-107-05:认证体系.md
- 源码(可直接导入):dify107_05_验证应用.yml
- Server 源码:dify107_05_auth_server 目录(含 mock_oauth.py)
- 交付验证记录(认证能力清单 + 失败形态表):验证记录-05-认证体系.md
- 全部目录:dify-107/experiments | dify-107/dsl | dify-107/servers | dify-107/delivery
文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。