From efe6fbe7702137d239fd72a9436a9cdcbd46a93c Mon Sep 17 00:00:00 2001 From: dongsk Date: Mon, 31 Aug 2026 10:24:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Etoken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 38 +++++++++++++++-------- mcp-for-crm/src/auth.py | 63 +++++++++++++++++++++++++++++++++++++++ mcp-for-crm/src/server.py | 8 ++++- mcp-for-erp/src/auth.py | 63 +++++++++++++++++++++++++++++++++++++++ mcp-for-erp/src/server.py | 8 ++++- 5 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 mcp-for-crm/src/auth.py create mode 100644 mcp-for-erp/src/auth.py diff --git a/docker-compose.yml b/docker-compose.yml index 0ae0e96..9b77805 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,14 @@ # 汽车零部件智能报价 — 两个 MCP 服务编排 -# 数据库为外部 PostgreSQL(192.168.1.206:5432),不在本编排内 -# 启动:docker compose up -d +# 生产环境:docker compose up -d +# 默认连生产库 192.168.1.206:5432,对外 192.168.1.119:8001/8002 +# 开发环境:本机直接 python server.py(不入容器) +# source .env.dev 后到 mcp-for-{erp,crm}/src 执行 python server.py +# 连开发库 47.101.220.40:5432,对外 localhost:8001/8002 +# 也可用 docker 跑开发:docker compose --env-file .env.dev up -d # 停止:docker compose down # 日志:docker compose logs -f # 网络模式 host:容器直接共享宿主网络栈,绕开 Docker 桥接 FORWARD 链, -# 便于局域网其他机器经 192.168.1.119:8001/8002 直接访问(无需 ports 映射) +# 便于局域网其他机器直接访问(无需 ports 映射) services: mcp-for-erp: build: @@ -16,11 +20,15 @@ services: container_name: mcp-for-erp network_mode: host environment: - - ERP_DB_HOST=192.168.1.206 - - ERP_DB_PORT=5432 - - ERP_DB_USER=postgres - - ERP_DB_PASSWORD=digiwin - - ERP_DB_NAME=smart_quotation_auto + - ERP_DB_HOST=${ERP_DB_HOST:-192.168.1.206} + - ERP_DB_PORT=${ERP_DB_PORT:-5432} + - ERP_DB_USER=${ERP_DB_USER:-postgres} + - ERP_DB_PASSWORD=${ERP_DB_PASSWORD:-digiwin} + - ERP_DB_NAME=${ERP_DB_NAME:-smart_quotation_auto} + # 对外地址(用于 OAuth 资源元数据),按实际部署机 IP 调整 + - MCP_PUBLIC_URL=${MCP_PUBLIC_URL_ERP:-http://192.168.1.119:8001} + # Bearer Token 表:token:client_id 逗号分隔,每个客户端一个独立 token + - MCP_AUTH_TOKENS=${MCP_AUTH_TOKENS_ERP:-erp-token-trae-2026:trae,erp-token-partner-a:partner-a} restart: unless-stopped mcp-for-crm: @@ -33,9 +41,13 @@ services: container_name: mcp-for-crm network_mode: host environment: - - CRM_DB_HOST=192.168.1.206 - - CRM_DB_PORT=5432 - - CRM_DB_USER=postgres - - CRM_DB_PASSWORD=digiwin - - CRM_DB_NAME=smart_quotation_auto + - CRM_DB_HOST=${CRM_DB_HOST:-192.168.1.206} + - CRM_DB_PORT=${CRM_DB_PORT:-5432} + - CRM_DB_USER=${CRM_DB_USER:-postgres} + - CRM_DB_PASSWORD=${CRM_DB_PASSWORD:-digiwin} + - CRM_DB_NAME=${CRM_DB_NAME:-smart_quotation_auto} + # 对外地址(用于 OAuth 资源元数据),按实际部署机 IP 调整 + - MCP_PUBLIC_URL=${MCP_PUBLIC_URL_CRM:-http://192.168.1.119:8002} + # Bearer Token 表:token:client_id 逗号分隔,每个客户端一个独立 token + - MCP_AUTH_TOKENS=${MCP_AUTH_TOKENS_CRM:-crm-token-trae-2026:trae,crm-token-partner-a:partner-a} restart: unless-stopped diff --git a/mcp-for-crm/src/auth.py b/mcp-for-crm/src/auth.py new file mode 100644 index 0000000..d07e2ad --- /dev/null +++ b/mcp-for-crm/src/auth.py @@ -0,0 +1,63 @@ +"""静态 Bearer Token 鉴权(Authorization)。 + +token → 客户端 映射来自环境变量 MCP_AUTH_TOKENS,格式(逗号分隔,每对 token:client_id): + MCP_AUTH_TOKENS=token-trae:trae,token-partner:partner-a + +每个客户端配置自己的 token 调用,服务端校验失败返回 401; +工具内可通过 get_caller() 获取当前调用方标识。 +""" + +import os + +from mcp.server.auth.middleware.auth_context import get_access_token +from mcp.server.auth.provider import AccessToken +from mcp.server.auth.settings import AuthSettings + + +class StaticTokenVerifier: + """静态 token 查表校验:命中返回 AccessToken(client_id 即客户端标识),未命中返回 None(401)。""" + + def __init__(self, tokens: dict[str, str]): + self._tokens = tokens + + async def verify_token(self, token: str) -> AccessToken | None: + client_id = self._tokens.get(token) + if client_id is None: + return None + return AccessToken(token=token, client_id=client_id, scopes=[], expires_at=None) + + +def _parse_tokens(raw: str) -> dict[str, str]: + """解析 'token1:client1,token2:client2' → {token: client_id}""" + tokens: dict[str, str] = {} + for item in raw.split(","): + item = item.strip() + if not item: + continue + token, _, client_id = item.partition(":") + token, client_id = token.strip(), client_id.strip() + if token and client_id: + tokens[token] = client_id + return tokens + + +def get_auth(port: int) -> tuple[AuthSettings, StaticTokenVerifier]: + """构建 MCPServer 的 (auth, token_verifier) 参数。 + + 服务对外地址默认 http://localhost:{port},部署时用 MCP_PUBLIC_URL 覆盖 + (如 http://192.168.1.119:8002),用于 OAuth 资源元数据发现。 + """ + url = os.getenv("MCP_PUBLIC_URL") or f"http://localhost:{port}" + tokens = _parse_tokens(os.getenv("MCP_AUTH_TOKENS", "")) + if not tokens: + raise RuntimeError("环境变量 MCP_AUTH_TOKENS 未配置,格式:token1:client1,token2:client2") + return ( + AuthSettings(issuer_url=url, resource_server_url=url), + StaticTokenVerifier(tokens), + ) + + +def get_caller() -> str: + """工具内获取当前调用方标识(未认证时返回 anonymous)。""" + access_token = get_access_token() + return access_token.client_id if access_token else "anonymous" diff --git a/mcp-for-crm/src/server.py b/mcp-for-crm/src/server.py index 96d0ef2..7a15a23 100644 --- a/mcp-for-crm/src/server.py +++ b/mcp-for-crm/src/server.py @@ -8,6 +8,7 @@ import asyncio import json from datetime import datetime, timedelta from mcp.server import MCPServer +from auth import get_auth from db import get_pool @@ -23,10 +24,15 @@ def _s(value): return value +# 鉴权:静态 Bearer Token,token → 客户端 映射来自环境变量 MCP_AUTH_TOKENS +_auth, _token_verifier = get_auth(port=8002) + app = MCPServer( name="mcp-for-crm-auto", description="汽车零部件智能报价 CRM 数据服务", - version="1.0.0" + version="1.0.0", + auth=_auth, + token_verifier=_token_verifier, ) diff --git a/mcp-for-erp/src/auth.py b/mcp-for-erp/src/auth.py new file mode 100644 index 0000000..d07e2ad --- /dev/null +++ b/mcp-for-erp/src/auth.py @@ -0,0 +1,63 @@ +"""静态 Bearer Token 鉴权(Authorization)。 + +token → 客户端 映射来自环境变量 MCP_AUTH_TOKENS,格式(逗号分隔,每对 token:client_id): + MCP_AUTH_TOKENS=token-trae:trae,token-partner:partner-a + +每个客户端配置自己的 token 调用,服务端校验失败返回 401; +工具内可通过 get_caller() 获取当前调用方标识。 +""" + +import os + +from mcp.server.auth.middleware.auth_context import get_access_token +from mcp.server.auth.provider import AccessToken +from mcp.server.auth.settings import AuthSettings + + +class StaticTokenVerifier: + """静态 token 查表校验:命中返回 AccessToken(client_id 即客户端标识),未命中返回 None(401)。""" + + def __init__(self, tokens: dict[str, str]): + self._tokens = tokens + + async def verify_token(self, token: str) -> AccessToken | None: + client_id = self._tokens.get(token) + if client_id is None: + return None + return AccessToken(token=token, client_id=client_id, scopes=[], expires_at=None) + + +def _parse_tokens(raw: str) -> dict[str, str]: + """解析 'token1:client1,token2:client2' → {token: client_id}""" + tokens: dict[str, str] = {} + for item in raw.split(","): + item = item.strip() + if not item: + continue + token, _, client_id = item.partition(":") + token, client_id = token.strip(), client_id.strip() + if token and client_id: + tokens[token] = client_id + return tokens + + +def get_auth(port: int) -> tuple[AuthSettings, StaticTokenVerifier]: + """构建 MCPServer 的 (auth, token_verifier) 参数。 + + 服务对外地址默认 http://localhost:{port},部署时用 MCP_PUBLIC_URL 覆盖 + (如 http://192.168.1.119:8002),用于 OAuth 资源元数据发现。 + """ + url = os.getenv("MCP_PUBLIC_URL") or f"http://localhost:{port}" + tokens = _parse_tokens(os.getenv("MCP_AUTH_TOKENS", "")) + if not tokens: + raise RuntimeError("环境变量 MCP_AUTH_TOKENS 未配置,格式:token1:client1,token2:client2") + return ( + AuthSettings(issuer_url=url, resource_server_url=url), + StaticTokenVerifier(tokens), + ) + + +def get_caller() -> str: + """工具内获取当前调用方标识(未认证时返回 anonymous)。""" + access_token = get_access_token() + return access_token.client_id if access_token else "anonymous" diff --git a/mcp-for-erp/src/server.py b/mcp-for-erp/src/server.py index 8ff905b..8eda01f 100644 --- a/mcp-for-erp/src/server.py +++ b/mcp-for-erp/src/server.py @@ -7,6 +7,7 @@ HTTP 模式,支持远程调用 import asyncio import json from mcp.server import MCPServer +from auth import get_auth from db import get_pool @@ -22,10 +23,15 @@ def _s(value): return value +# 鉴权:静态 Bearer Token,token → 客户端 映射来自环境变量 MCP_AUTH_TOKENS +_auth, _token_verifier = get_auth(port=8001) + app = MCPServer( name="mcp-for-erp-auto", description="汽车零部件智能报价 ERP 数据服务", - version="1.0.0" + version="1.0.0", + auth=_auth, + token_verifier=_token_verifier, )