服务器发送事件(Server-Sent Events, SSE)是一种基于HTTP的技术,允许服务器向客户端推送实时更新。与WebSocket不同,SSE是单向通信机制,非常适合于需要服务器实时推送数据但不需要客户端频繁发送消息的场景。
在构建现代Web应用程序时,安全性至关重要。本文将探讨如何在SSE通信中实现基于Header的认证机制,确保只有经过授权的客户端才能接收服务器推送的实时数据。我们将使用Python和Model Context Protocol(MCP)SDK实现一个完整的示例。
SSE使用标准的HTTP连接,客户端通过一个持久连接从服务器接收自动更新。与WebSocket相比,SSE具有以下特点:
SSE连接的典型工作流程如下:
Accept: text/event-stream
将SSE应用于生产环境时,关键的安全问题包括:
本文将重点关注认证问题,展示如何使用HTTP Header实现SSE连接的认证机制。
HTTP Header认证是一种简单有效的认证方式,常见的实现包括基本认证(Basic Auth)和Bearer令牌认证。在我们的实现中,将采用Bearer令牌方式,API密钥将通过Authorization头部传递:
Authorization: Bearer api_key_value
这种方式有以下优势:
我们的示例基于以下技术栈:
首先,我们来看SSE服务端的实现。核心是扩展MCP SDK的SseServerTransport
类,添加认证中间件和错误处理:
import anyio
import click
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from starlette.requests import Request
from starlette.responses import Response
import uvicorn
import logging
from uuid import UUID
# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 模拟用户API密钥字典
API_KEYS = {
"user1": "api_key_1",
"user2": "api_key_2",
}
# 鉴权中间件函数
async def auth_middleware(request, scope, receive, send):
# 从请求头获取 Authorization 信息
auth_header = request.headers.get("Authorization")
# 验证 Authorization 头部是否存在且格式正确
if not auth_header or not auth_header.startswith("Bearer "):
logger.warning("无效的 Authorization 头部")
response = Response("Unauthorized: Invalid Authorization header", status_code=401)
return await response(scope, receive, send)
# 提取 API Key
api_key = auth_header.replace("Bearer ", "")
# 验证 API Key 是否有效
is_valid = any(key == api_key for key in API_KEYS.values())
if not is_valid:
logger.warning(f"无效的 API Key: {api_key}")
response = Response("Unauthorized: Invalid API Key", status_code=401)
return await response(scope, receive, send)
# 通过验证,继续处理请求
logger.info(f"API Key 验证通过")
return None # 返回 None 表示验证通过
我们定义了一个认证中间件函数,该函数从请求头中提取Authorization
头部,验证其格式并检查API密钥是否有效。
接下来,我们扩展SseServerTransport
类,添加健壮的错误处理:
# 对SseServerTransport进行扩展以处理错误
class SafeSseServerTransport(SseServerTransport):
"""扩展SSE服务端传输以处理连接错误"""
async def handle_post_message(self, scope, receive, send):
"""重写处理POST消息的方法,添加错误处理"""
request = Request(scope, receive)
session_id_param = request.query_params.get("session_id")
if session_id_param is None:
logger.warning("收到没有session_id的请求")
response = Response("session_id is required", status_code=400)
return await response(scope, receive, send)
try:
session_id = UUID(hex=session_id_param)
logger.debug(f"解析会话ID: {session_id}")
except ValueError:
logger.warning(f"收到无效的会话ID: {session_id_param}")
response = Response("Invalid session ID", status_code=400)
return await response(scope, receive, send)
writer = self._read_stream_writers.get(session_id)
if not writer:
logger.warning(f"找不到会话ID: {session_id}")
response = Response("Could not find session", status_code=404)
return await response(scope, receive, send)
# 获取请求体
body = await request.body()
logger.debug(f"收到JSON: {body}")
try:
# 解析JSON消息
message = types.JSONRPCMessage.model_validate_json(body)
logger.debug(f"验证客户端消息: {message}")
except Exception as err:
logger.error(f"无法解析消息: {err}")
response = Response("Could not parse message", status_code=400)
await response(scope, receive, send)
# 尝试安全发送错误
try:
await writer.send(err)
except anyio.BrokenResourceError:
logger.warning("连接已断开,无法发送错误消息")
except Exception as e:
logger.error(f"发送错误消息时出错: {e}")
return
# 尝试安全发送消息
logger.debug(f"发送消息到写入器: {message}")
response = Response("Accepted", status_code=202)
await response(scope, receive, send)
# 使用try-except块处理可能的连接错误
try:
await writer.send(message)
except anyio.BrokenResourceError:
logger.warning(f"发送消息时连接已断开,会话ID: {session_id}")
except Exception as e:
logger.error(f"发送消息时发生未知错误: {e}")
在这个扩展类中,我们特别关注了错误处理,确保即使在客户端连接断开的情况下,服务器也能够优雅地处理异常情况。
然后,我们创建SSE连接处理函数和POST消息处理中间件,都集成了认证机制。注意我们使用扩展的SafeSseServerTransport
而不是原始的SseServerTransport
:
# 创建安全SSE服务端传输
sse = SafeSseServerTransport("/messages/")
# 处理 SSE 连接的函数,添加鉴权
async def handle_sse(request):
# 进行鉴权检查
auth_result = await auth_middleware(request, request.scope, request.receive, request._send)
if auth_result is not None:
return auth_result # 如果鉴权失败,返回鉴权结果
# 鉴权成功,建立 SSE 连接
try:
logger.info("建立新的SSE连接")
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
# 记录SSE连接成功
logger.info("SSE连接建立成功,开始运行应用")
await app.run(
streams[0], streams[1], app.create_initialization_options()
)
except Exception as e:
logger.error(f"SSE连接处理错误: {e}")
return Response(f"Internal Server Error: {str(e)}", status_code=500)
# 处理 POST 消息的自定义中间件
async def handle_post_with_auth(scope, receive, send):
request = Request(scope, receive)
# 进行鉴权检查
auth_result = await auth_middleware(request, scope, receive, send)
if auth_result is not None:
return # 如果鉴权失败,直接返回
try:
# 鉴权成功,继续处理 POST 消息
await sse.handle_post_message(scope, receive, send)
except anyio.BrokenResourceError:
logger.warning("处理POST消息时遇到连接断开错误")
response = Response("Connection closed", status_code=410)
await response(scope, receive, send)
except Exception as e:
logger.error(f"处理POST消息时发生错误: {e}")
response = Response("Internal Server Error", status_code=500)
await response(scope, receive, send)
最后,我们设置Starlette应用并实现优雅退出机制:
# 创建 Starlette 应用
starlette_app = Starlette(
debug=True,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=handle_post_with_auth),
],
on_shutdown=[shutdown]
)
# 启动 Uvicorn 服务器
logger.info(f"启动带有鉴权的 SSE 服务器在端口 {port}")
客户端需要在建立SSE连接和发送POST请求时都提供认证头。以下是一个简化的客户端实现:
import asyncio
import json
import httpx
import urllib.parse
import re
import logging
from urllib.parse import urljoin
class AuthMcpClient:
"""带有鉴权的MCP客户端实现"""
def __init__(self, server_url: str, api_key: str):
"""
初始化客户端
Args:
server_url: SSE服务器地址,如 http://localhost:8000/sse
api_key: 用于鉴权的API密钥
"""
self.server_url = server_url
self.api_key = api_key
self.session_url = None
self.base_url = self._get_base_url(server_url)
self.event_queue = asyncio.Queue()
self.client = None # HTTP客户端
self.sse_task = None # SSE连接任务
def _get_base_url(self, url):
"""从URL中提取基础URL (scheme + host + port)"""
parsed = urllib.parse.urlparse(url)
return f"{parsed.scheme}://{parsed.netloc}"
async def connect(self):
"""连接到SSE服务器并处理消息"""
# 创建持久的HTTP客户端
self.client = httpx.AsyncClient()
# 添加鉴权头部
headers = {
"Authorization": f"Bearer {self.api_key}"
}
# 使用异步方式启动SSE监听器
self.sse_task = asyncio.create_task(self._listen_sse(headers))
# 等待获取会话URL
response = await self.event_queue.get()
# 将相对URL转换为绝对URL
relative_url = response
self.session_url = urljoin(self.base_url, relative_url)
# 等待一段时间,让服务器完成初始化
await asyncio.sleep(1.0)
SSE监听和消息发送也需要包含认证头:
async def _listen_sse(self, headers):
"""监听SSE事件"""
try:
# 使用httpx直接实现SSE客户端
async with self.client.stream("GET", self.server_url, headers=headers) as response:
response.raise_for_status()
# 处理SSE流
event_type = None
data_buffer = []
# 按行读取响应
async for line in response.aiter_lines():
line = line.rstrip()
# 空行表示事件结束
if not line:
if data_buffer and event_type:
# 合并数据
data = "".join(data_buffer)
# 处理事件
if event_type == "endpoint":
await self.event_queue.put(data)
elif event_type == "message":
try:
message = json.loads(data)
await self.event_queue.put(message)
except json.JSONDecodeError:
pass
# 重置缓冲区
event_type = None
data_buffer = []
continue
# 解析SSE行
if line.startswith("event:"):
event_type = line[6:].strip()
elif line.startswith("data:"):
data_buffer.append(line[5:].strip())
except Exception as e:
# 在连接中断的情况下通知主线程
await self.event_queue.put({"error": str(e)})
async def send_message(self, method, params=None):
"""发送消息到服务器"""
if not self.session_url:
raise ValueError("客户端尚未连接,请先调用connect()")
# 创建JSON-RPC消息
message = {
"jsonrpc": "2.0",
"id": "1", # 示例使用固定ID
"method": method
}
if params:
message["params"] = params
# 添加鉴权头
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
# 发送POST请求
try:
response = await self.client.post(
self.session_url,
json=message,
headers=headers
)
response.raise_for_status()
return response
except Exception as e:
raise
在实现基于Header的SSE认证时,以下是一些安全最佳实践:
本实现可以进一步扩展:
在实现过程中,可能遇到以下常见问题:
基于Header的SSE认证是一种简单有效的方式,可以保护实时数据流免受未授权访问。本文展示了如何使用Python和MCP SDK实现一个完整的SSE认证系统,包括服务端和客户端。
通过合理的错误处理和优雅退出机制,我们的实现不仅安全,而且健壮。这种模式适用于各种需要实时单向数据推送的场景,如通知系统、实时监控和聊天应用。
随着Web应用变得越来越实时和交互式,安全的SSE实现将在前后端通信中发挥越来越重要的作用。