MCP Server怎么做权限控制?5分钟教会你快速给MCP 服务成功添加授权 原创

发布于 2025-6-5 06:15
浏览
0收藏

2024 年,Anthropic[1] 发布 Claude 3[2] 系列的同时推出了 MCP[3](Model Context Protocol),定位更底层:不是“调函数”,而是“定义模型理解系统的结构协议”。

在企业级应用场景下,对于一些私有环境,MCP服务器可能不需要严格的身份认证。但如果在企业级别进行部署,对这些接口的安全性和权限管理就显得至关重要。MCP服务器可以通过两种方式运行:

  1. stdio
  2. http + sse

这两种运行方式的权限控制策略各不相同。在深入讨论MCP服务器的权限控制之前,我们先简单回顾一下MCP的基本原理。

MCP组成和执行流程

MCP 架构分为以下 3 部分:

  • 客户端:大模型应用(如 DeepSeek、ChatGPT)发起 MCP 协议请求。
  • 服务器端:服务器端响应客户端的请求,并查询自己的业务实现请求处理和结果返回。

运行流程

  1. 用户提问 LLM。
  2. LLM 查询 MCP 服务列表。
  3. 找到需要调用 MCP 服务,调用 MCP 服务器端。
  4. MCP 服务器接收到指令。
  5. 调用对应工具(如数据库)执行。
  6. 返回结果给 LLM。

stdio模式

在 stdio 这种模式下,mcp server是作为mcp client 的一个子进程运行的。在这种模式下,可以在mcp client 中配置环境变量,mcp server读取环境变量。

环境变量是操作系统为每个进程维护的一组键值对(如​​PATH​​​、​​PYTHONPATH​​等),存储在进程的内存空间中。子进程启动时,会继承其父进程的环境变量(如 Shell 启动的程序会继承 Shell 的环境变量)。

下面是cursor中mcp的配置:

{
    "mcpServers": [
      {
        "name": "my-mcp-server",
        "command": "/home/cy/Downloads/anaconda3/envs/deep-searcher/bin/python3",
        "args": ["/home/cy/Desktop/git/deep-searcher/mcp_server.py"],
        "env": {"token": "value"}
      }
    ]
}

在生产中,我们一般会把mcp server单独部署成一个服务,所以对这个服务的权限验证才是重点。

HTTP + SSE

2025 年 3 月发布的最新 MCP 规范引入了安全基础,借助了广泛使用的 OAuth2 框架[4]

MCP Server怎么做权限控制?5分钟教会你快速给MCP 服务成功添加授权-AI.x社区

  • 作为资源托管中心,MCP的职责在于审查所有请求中的Authorization头部信息。这个头部信息必须携带一个OAuth2 access_token(代表客户端权限的令牌)。通常情况下,这个令牌是一个JWT(JSON Web Token),或者只是一串无法解读的随机字符。如果令牌丢失或无效(例如无法解析、已经过期、不属于该服务器等),请求将会被驳回。
  • 同时,作为授权中心,MCP还需要具备为客户端安全签发access_token的能力。在签发令牌之前,服务器会验证客户端的证明,有时还需要确认访问用户的身份。授权中心会确定令牌的有效时间、权限范围、目标听众等属性。

在我们的内部应用体系中,token 的签发并非由我们的 MCP 服务器完成,而是由其他服务提供。因此,MCP服务器并不完全实现了OAuth2服务器的所有功能,它主要承担着授权校验的职责。为了实现这一目标,我们使用了一个专门处理token相关操作的 JWT类。

# pip install python-jose[cryptography]
from jose import jwt, JWTError
class JWTManager:
    def __init__(
            self,
            secret_key: str,
            algorithm: str = "HS256",
            access_token_expire_minutes: int = 30,
            refresh_token_expire_days: int = 30
    ):
        """
        初始化JWT管理器

        Args:
            secret_key: 用于签名JWT的密钥
            algorithm: 加密算法,默认HS256
            access_token_expire_minutes: 访问令牌过期时间(分钟)
            refresh_token_expire_days: 刷新令牌过期时间(天)
        """
        self.secret_key = secret_key
        self.algorithm = algorithm
        self.access_token_expire_minutes = access_token_expire_minutes
        self.refresh_token_expire_days = refresh_token_expire_days

    def create_access_token(
            self,
            data: Dict[str, Any],
            expires_delta: Optional[timedelta] = None
    ) -> str:
        """
        创建访问令牌

        Args:
            data: 要编码到令牌中的数据
            expires_delta: 可选的过期时间增量

        Returns:
            str: 编码后的JWT令牌
        """
        to_encode = data.copy()
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(minutes=self.access_token_expire_minutes)

        to_encode.update({"exp": expire})
        encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
        return encoded_jwt

    def create_refresh_token(
            self,
            data: Dict[str, Any],
            expires_delta: Optional[timedelta] = None
    ) -> str:
        """
        创建刷新令牌

        Args:
            data: 要编码到令牌中的数据
            expires_delta: 可选的过期时间增量

        Returns:
            str: 编码后的JWT令牌
        """
        to_encode = data.copy()
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(days=self.refresh_token_expire_days)

        to_encode.update({"exp": expire})
        encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
        return encoded_jwt

    def verify_token(self, token: str) -> Dict[str, Any]:
        """
        验证并解码JWT令牌

        Args:
            token: 要验证的JWT令牌

        Returns:
            Dict[str, Any]: 解码后的令牌数据

        Raises:
            HTTPException: 当令牌无效或过期时
        """
        try:
            payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
            return payload
        except JWTError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Could not validate credentials",
                headers={"WWW-Authenticate": "Bearer"},
            )

    def is_token_expired(self, token: str) -> bool:
        """
        检查令牌是否过期

        Args:
            token: 要检查的JWT令牌

        Returns:
            bool: 如果令牌过期返回True,否则返回False
        """
        try:
            payload = self.verify_token(token)
            exp = payload.get("exp")
            if exp isNone:
                returnTrue
            return datetime.utcnow() > datetime.fromtimestamp(exp)
        except HTTPException:
            returnTrue

在这个环境下,我们采用了中间件的方法。通过深入研究mcp这个package的源代码,我们了解到其底层实际上还是利用starlette创建的app来提供服务。因此,我们需要对这个app添加自定义的中间件。

class JwtMiddleware(BaseHTTPMiddleware):
    def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None:
        super().__init__(app, dispatch)
        self.jwt_manager = JWTManager(
            secret_key="your-secret-key",
            access_token_expire_minutes=30,
            refresh_token_expire_days=30
        )

    def verify_token(self, token):
        try:
            payload = self.jwt_manager.verify_token(token)
            return payload
        except Exception:
            raise



    asyncdef dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        token = request.headers.get("Authorization")

        ifnot token:
            return JSONResponse({"error": "unauthorized"}, status_code=401)
        token = token.lstrip("Bearer").strip()
        try:
            payload = self.verify_token(token)
            logging.info(payload)
        except Exception as e:
            logging.error(e)
            return JSONResponse({"error": "unauthorized"}, status_code=401)
        else:
            response = await call_next(request)
            return response

然而,在MCP这个包的源代码中,并没有提供直接添加中间件的入口。因此,我们需要自行进行扩展。为了实现这一目标,我们决定继承原有的FastMCP类来完成这个扩展。

class CustomFastMCP(FastMCP):
    def __init__(
            self, name: str | None = None, instructions: str | None = None, **settings: Any
    ):
        super().__init__(name, instructions, **settings)
        self.app = self.sse_app()

# 添加的方法
    def add_middleware(
        self,
        middleware_class
    ) -> None:
        self.app.add_middleware(middleware_class)

    asyncdef run_sse_async(self) -> None:
        """Run the server using SSE transport."""
        starlette_app = self.app

        config = uvicorn.Config(
            starlette_app,
            host=self.settings.host,
            port=self.settings.port,
            log_level=self.settings.log_level.lower(),
        )
        server = uvicorn.Server(config)
        await server.serve()

在此新类中,我们对run_sse_async和init方法进行了重写,并添加了一个新的函数add_middleware来支持中间件的添加。现在,我们可以通过使用这个自定义的CustomFastMCP类来运行MCP server。如果请求头中未带有Authorization或其值错误,系统会返回一个错误信息:

{"error":"unauthorized"}

一旦正确地携带了Authorization,请求将正常处理。

curl -X GET "http://127.0.0.1:8000/sse" \
>      -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3NDg5NDExMzd9.GadUzsaiJYK215KIzl1c3ACQYiwQAmWaheb2o4ZI8d8"
event: endpoint
data: /messages/?session_id=eb18beb2ff4e4e70a61066d74f59a064

: ping - 2025-06-03 08:29:47.996746+00:00

: ping - 2025-06-03 08:30:02.997993+00:00

: ping - 2025-06-03 08:30:17.998910+00:00

总结

至此,我们已成功地为MCP服务加入了认证机制。现在,只有经过授权的用户才能获取我们MCP服务的接入权,进一步确保了我们服务的安全性和管理效率。尽管存在多种实现MCP服务权限认证的方法,但值得注意的是,当前MCP在这个领域的研究还处于草案阶段,我们会继续关注并优化这一块的工作。

参考资料

[1] Anthropic: ​​https://zhida.zhihu.com/search?content_id=256440304&content_type=Article&match_order=1&q=Anthropic&zhida_source=entity​

[2] Claude 3: ​​https://zhida.zhihu.com/search?content_id=256440304&content_type=Article&match_order=1&q=Claude+3&zhida_source=entity​

[3] MCP: ​​https://zhida.zhihu.com/search?content_id=256440304&content_type=Article&match_order=1&q=MCP&zhida_source=entity​

[4] OAuth2 框架: ​​​https://modelcontextprotocol.info/specification/draft/basic/authorization/​


本文转载自AI 博物院 作者:longyunfeigu


©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2025-6-5 06:15:34修改
收藏
回复
举报
回复
相关推荐