MCP 安全与身份验证
使用 OAuth 2.0 令牌为 MCP 服务器添加身份验证,实现输入验证以防止注入攻击,并将最小权限原则应用于工具权限。
MCP 安全与身份验证 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。
MCP 安全为何重要
MCP 服务器是进入您系统的网关。如果缺乏适当的安全措施,被攻破的 AI 客户端或恶意提示可能会读取敏感数据、触发破坏性操作,或通过工具调用通道窃取信息。MCP 服务器的安全必须采用纵深防御:在传输层进行身份验证,在工具层进行授权,并对每次调用执行输入验证。
本地服务器与远程服务器的安全性
标准输入输出传输(Claude Desktop 使用的传输方式)具有固有的安全性:服务器作为本地进程运行,只有启动它的用户才能访问。通过 HTTP/SSE 暴露在网络上的 MCP 服务器则面临各种 Web 安全威胁:身份验证绕过、注入攻击和未授权访问。安全要求会因部署模式不同而有很大差异。
- 本地标准输入输出:信任本地用户;重点关注输入验证
- 远程 HTTP/SSE:完整的身份验证、TLS、速率限制和输入清理
远程服务器的 API 密钥身份验证
远程 MCP 服务器最简单的身份验证方式,是通过 HTTP 标头验证 API 密钥。请检查每个传入请求中的 Authorization: Bearer <token> 标头,并对未经过身份验证的请求返回 HTTP 401。请将有效的 API 密钥与用户元数据一起存储在数据库中,以便撤销单个密钥。
# For HTTP/SSE MCP servers using FastAPI or similar:
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import secrets
app_http = FastAPI()
# In production: store in database with user_id, created_at, last_used
VALID_KEYS = {'sk-mcp-abc123': {'user': 'alice', 'scopes': ['read']},
'sk-mcp-def456': {'user': 'bob', 'scopes': ['read', 'write']}}
async def verify_api_key(authorization: Optional[str] = Header(None)) -> dict:
if not authorization or not authorization.startswith('Bearer '):
raise HTTPException(status_code=401, detail='Missing API key')
key = authorization.removeprefix('Bearer ')
if key not in VALID_KEYS:
raise HTTPException(status_code=401, detail='Invalid API key')
return VALID_KEYS[key] # Returns user context
# Use in route handlers:
# @app_http.get('/sse')
# async def sse_endpoint(user=Depends(verify_api_key)):企业 MCP 服务器的 OAuth 2.0
对于企业部署,请使用OAuth 2.0,让用户通过企业身份提供商(Okta、Azure AD、Google Workspace)进行身份验证。MCP 客户端会获取 OAuth 访问令牌,并在工具调用请求中包含该令牌。您的服务器可以使用 python-jose 或 authlib,根据身份提供商的公钥验证令牌签名。
from jose import jwt, JWTError
import httpx
AUTH_DOMAIN = 'your-tenant.auth0.com'
AUDIENCE = 'https://api.your-mcp-server.com'
async def get_jwks():
async with httpx.AsyncClient() as client:
resp = await client.get(f'https://{AUTH_DOMAIN}/.well-known/jwks.json')
return resp.json()
async def verify_oauth_token(token: str) -> dict:
jwks = await get_jwks()
try:
payload = jwt.decode(
token,
jwks,
algorithms=['RS256'],
audience=AUDIENCE,
issuer=f'https://{AUTH_DOMAIN}/'
)
return payload # Contains sub (user ID), scope, exp, etc.
except JWTError as e:
raise ValueError(f'Invalid token: {e}')基于范围的授权
并非所有 MCP 工具都应向所有用户开放。请使用 OAuth 范围或 JWT 令牌中的角色声明,确定经过身份验证的用户可以调用哪些工具。请在每次工具执行开始时检查授权,并在进行任何数据库或 API 调用之前完成检查。
TOOL_REQUIRED_SCOPES = {
'list_products': ['read:products'],
'search_products': ['read:products'],
'create_order': ['write:orders'],
'delete_order': ['admin:orders']
}
def check_authorization(tool_name: str, token_payload: dict):
'''Raise ValueError if user lacks required scope for the tool.'''
required = TOOL_REQUIRED_SCOPES.get(tool_name, [])
if not required:
return # No scope required
user_scopes = set(token_payload.get('scope', '').split())
missing = [s for s in required if s not in user_scopes]
if missing:
raise ValueError(
f'Access denied. Tool "{tool_name}" requires scopes: {missing}. '
f'Your token has: {list(user_scopes)}'
)
# In call_tool handler:
# check_authorization(name, current_user_token)
# ... then execute the tool输入验证与注入防护
所有工具输入最终都是由 LLM 生成的字符串,因此请将其视为不可信数据。使用输入前,请根据预期类型和模式验证每一项输入。具体来说,请防范以下风险:SQL 注入(使用参数化查询,绝不要使用字符串插值构造 SQL)、命令注入(绝不要将用户输入传递给 Shell 命令)以及路径遍历(规范化并验证文件路径)。
import re
from pathlib import Path
BASE_DATA_DIR = Path('/data/mcp-files')
def safe_file_path(user_input: str) -> Path:
'''Validate and normalize a file path to prevent traversal attacks.'''
# Remove any path traversal sequences
clean = re.sub(r'\.\./', '', user_input)
clean = re.sub(r'\.\.\\\\', '', clean)
path = (BASE_DATA_DIR / clean).resolve()
# Ensure the resolved path is still within the allowed base directory
if not str(path).startswith(str(BASE_DATA_DIR)):
raise ValueError(f'Path traversal detected: {user_input}')
return path
def safe_identifier(value: str) -> str:
'''Validate a database identifier (table/column name).'''
if not re.match(r'^[a-z_][a-z0-9_]{0,63}$', value, re.IGNORECASE):
raise ValueError(f'Invalid identifier: {value}')
return value限制工具调用速率
循环运行的 LLM 代理每分钟可能调用高成本工具数百次,从而耗尽数据库连接、第三方 API 配额或计算预算。请使用 Redis 中的令牌桶算法,按用户实施速率限制。对于超出限制的工具调用,请返回描述性错误,以便代理知道需要等待。
import redis
import time
r = redis.Redis.from_url('redis://localhost:6379')
def check_rate_limit(user_id: str, tool_name: str, limit: int = 60, window: int = 60) -> None:
'''Allow at most `limit` calls per `window` seconds per user per tool.'''
key = f'rate:{user_id}:{tool_name}'
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
count, _ = pipe.execute()
if count > limit:
retry_after = r.ttl(key)
raise ValueError(
f'Rate limit exceeded for {tool_name}. '
f'Limit: {limit} calls/{window}s. '
f'Retry after {retry_after} seconds.'
)最小权限原则
请在 MCP 服务器的每一层应用最小权限原则。数据库用户应当仅对服务器所需的表拥有 SELECT 权限。服务器进程应以非 root 的 OS 用户身份运行。工具只能请求其所需的权限。API 密钥应具有满足其用途的最小范围。您限制的每项权限,都意味着一种无法成功实施的潜在攻击。
-- PostgreSQL: Create a dedicated read-only database user for your MCP server
CREATE ROLE mcp_reader LOGIN PASSWORD 'strong_random_password';
-- Grant SELECT on only the tables the server needs
GRANT SELECT ON products, categories, public_content TO mcp_reader;
-- Explicitly deny access to sensitive tables
REVOKE ALL ON users, api_keys, payment_methods FROM mcp_reader;
-- Never grant: INSERT, UPDATE, DELETE, TRUNCATE, or DDL permissionsTLS 与传输安全
远程 MCP 服务器必须使用 TLS 来保护传输中的数据。请将服务器配置为仅接受 HTTPS 连接。在生产环境中,请使用反向代理(nginx、Caddy)处理 TLS 终止,并通过自动续期(使用 Certbot 或 Caddy 内置的 ACME 支持实现 Let's Encrypt)确保凭证保持最新。
# Example Caddyfile for TLS-terminating MCP server at a subdomain:
#
# mcp.yourcompany.com {
# reverse_proxy localhost:8080
# encode gzip
# tls internal # Use Let's Encrypt in production
# header {
# Strict-Transport-Security 'max-age=31536000; includeSubDomains'
# X-Content-Type-Options nosniff
# X-Frame-Options DENY
# }
# }通过 MCP 资源进行提示注入
有一种隐蔽的攻击途径:如果您的 MCP 服务器从外部来源(网页、用户上传的文件或不可信的数据库)读取内容,并将其作为工具结果返回,攻击者可能会在这些内容中嵌入对抗性指令。模型可能会遵从检索数据中隐藏的指令——这就是间接提示注入。请清理检索到的内容,绝不要直接将不可信的原始文本作为工具输出返回。
import re
def sanitize_for_mcp_output(text: str) -> str:
'''Remove patterns that look like instructions to the LLM.'''
# Remove common injection patterns
dangerous_patterns = [
r'ignore previous instructions',
r'ignore all prior instructions',
r'system:',
r'<\|.*?\|>', # Special tokens
r'\[INST\]',
r'<s>',
]
for pattern in dangerous_patterns:
text = re.sub(pattern, '[FILTERED]', text, flags=re.IGNORECASE)
return text[:10000] # Also cap length to prevent context stuffing安全审计与监控
记录所有与安全相关的事件:身份验证成功与失败、违反速率限制、授权拒绝、验证错误以及异常的访问模式。请针对以下情况设置警报:多次身份验证失败(暴力破解)、单个用户快速调用破坏性工具,以及任何输入规模极大的工具调用。定期检查审计日志,并自动执行异常检测。
快速检查
测试您对 MCP 安全性和身份验证概念的理解。
课程回顾
在本课中,您了解到:远程 MCP 服务器要求每个请求都使用 OAuth 2.0 或 API 密钥进行身份验证,基于作用域的授权会控制每个已通过身份验证的用户可以调用哪些工具,并且必须验证所有工具输入,以防止注入攻击和路径遍历。MCP 模块到此结束——接下来我们将探索用于高精度 RAG 检索的高级分块策略。
常见问题解答
「MCP 安全与身份验证」课时是免费的吗?
是的 — 「MCP 安全与身份验证」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。
「MCP 安全与身份验证」这节课中我会学到什么?
使用 OAuth 2.0 令牌为 MCP 服务器添加身份验证,实现输入验证以防止注入攻击,并将最小权限原则应用于工具权限。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Engineering Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「MCP 安全与身份验证」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Engineering Academy 课中编写并运行代码吗?
能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 什么是 MCP,以及它为何重要
- 构建您的第一个 MCP 服务器
- 通过 MCP 暴露数据库资源
- MCP 安全与身份验证