MCP 鉴权与安全:你的 MCP Server 可能正在裸奔
上个月我写了一个 MCP Server 给团队用,加了 Streamable HTTP 传输层,跑在办公室内网。第二天运维大叔跑来问我:你这服务怎么谁都能调?
我一查,慌了。没有鉴权,没有白名单,任何一个能访问内网的人都可以往我的 MCP Server 发请求,调工具、读资源,全裸奔。
MCP 协议从设计上就没考虑过鉴权。它的默认传输层 stdio 走标准输入输出——只有你本地能启动的进程才能用它,自然不需要鉴权。但一旦你把它搬到网络上——无论是 SSE 还是新的 Streamable HTTP——安全就变成了你的问题。
官网给的说法也很直白:“MCP 目前不实现任何认证或授权机制。”
MCP 安全到底缺什么
MCP 协议的安全假设很简单:传输层安全由你负责。但实际落地时,问题集中在三个层面:
1. 没有身份验证
MCP 的 initialize 请求里有一个 capabilities 字段,声明服务端能干什么。但它不做身份校验——任何人发了 initialize,服务端都会回复“你好,我可以调用这些工具”。有人能连上就意味着有人能用你的所有工具。
2. 工具调用没有权限分层
你写了一个 list_files 工具和一个 reboot_server 工具。在 MCP 的模型里,它们都是平等的 tool——没有“只读操作不需要鉴权、危险操作需要二次确认”这种区分。客户端能调用所有工具,或者一个都不能调。
3. 资源访问范围不可控
你的 MCP 服务端暴露了一个资源路径 documents://reports/public 和一个 documents://internal/hr。客户端一旦连接就能访问所有已注册的资源模板,你的代码没法在协议层说“这个资源只有管理员能读”。
你的 MCP Server 暴露了什么
拿我那个裸奔的 Server 举例,用 Python SDK 搭的,长这样:
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
app = Server("my-server")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(name="read_file", ...),
Tool(name="execute_sql", ...), # 这里有个大坑
Tool(name="send_notification", ...),
]
没有一行鉴权代码。任何一个会发 HTTP 请求的人都可以:
- 读你指定路径下的任何文件
- 执行你数据库里的 SQL
- 通过你对接的 IM 工具发通知
更可怕的是,如果你的 MCP Server 跑在 GitHub Codespaces、Ollama 或者其他公网可达的环境,本应是“本地专用”的工具可能被任何知道端口的人调用。
你能做什么
MCP 协议本身不管鉴权,但你在应用层可以做不少事。
方案一:API Key 中间件
最直接的方案——在所有 MCP request 前面加一层中间件校验。我后来给 Server 加了个简单的 decorator:
from functools import wraps
API_KEYS = {"sk-prod-xxx": "admin", "sk-ro-xxx": "readonly"}
async def require_auth(request):
token = request.headers.get("authorization", "").replace("Bearer ", "")
role = API_KEYS.get(token)
if not role:
raise PermissionError("Invalid API key")
return role
然后在每个工具调用前加角色校验。readonly 用户只能调只读工具,admin 才能调写操作。这是我个人觉得性价比最高的方案——15 行代码挡掉 90% 的风险。
方案二:OAuth 2.0 集成
MCP 协议的 Authorization 规范已经有草案了(目前还是 proposed 阶段),采用 OAuth 2.0 的 device authorization grant 流程。如果你在用 Claude Desktop 或者支持 MCP 的客户端工具接入,可以在传输层用 OAuth。
实现起来比较重——你需要一个完整的 OAuth server(或者对接已有的)、处理 token 刷新、session 管理。但好处是用户不需要手配 API Key,浏览器跳转登录就行,体验好很多。
方案三:传输层白名单
如果 MCP Server 只服务特定客户端,简单粗暴但有效——在 HTTP server 层做 IP 白名单:
ALLOWED_ORIGINS = {"http://localhost:3000", "http://claude.desktop"}
ALLOWED_IPS = {"127.0.0.1", "192.168.1."}
async def ip_whitelist_middleware(request, next_handler):
client_ip = request.client.host
origin = request.headers.get("origin", "")
if client_ip not in ALLOWED_IPS and origin not in ALLOWED_ORIGINS:
return Response(status=403)
return await next_handler(request)
Streamable HTTP 的改进与隐患
2026 年 5 月 MCP 协议加入的 Streamable HTTP 传输层解决了一个实际问题:不再需要 SSE 的长连接,服务端可以跑在普通 HTTP server 上。这让 MCP Server 部署方便了很多——一个 fastapi/uvicorn 就能搞定。
但安全隐患也更大了。之前的 SSE 至少要求客户端维护一个持久连接,服务端能跟踪“谁开着连接”。Streamable HTTP 把 MCP 变成了纯请求-响应模式——服务端根本不知道谁在调用它(如果没加鉴权的话)。
更微妙的是,Streamable HTTP 支持 notifications(服务端主动发消息给客户端)和 progress(进度回调)。这些特性如果不加管控,可能被用来探测内网结构或做 SSRF 攻击的跳板。
我的检查清单
踩完坑之后,我现在每写一个 MCP Server 都会过一遍这个清单:
- 这个 Server 跑在本机还是网络上?网络可达就必须加鉴权。
- 每个工具的实际危险程度?至少分只读 / 读写两档。
- API Key 的存储方式?写死在代码里不行,用环境变量或密钥管理服务。
- 有没有加请求频率限制?防止被暴力枚举或 DoS。
- 日志记录了谁调了什么工具?出了事得有审计线索。
MCP 是个好协议,它让 AI agent 和工具之间的交互标准化了。但它还年轻,安全这块更多靠开发者自觉。别像我一样,等运维大叔找上门才想起来加鉴权。
更多推荐

所有评论(0)