1. 自研 MCP 服务为什么必须补上认证这一层MCP模型上下文协议让 AI 客户端能直接调用你写的工具函数听起来很爽但一旦这个服务暴露在公网或者团队内网里没有认证就等于把数据库操作、文件读写、内部 API 全部敞开。我见过太多自研 MCP 服务在本地跑通后直接扔到服务器上结果被扫描到端口后被人批量调用轻则消耗额度重则数据被拖走。核心问题有三个第一请求到底来自谁是可信客户端还是随便一个 curl第二同一个 Key 能不能区分权限比如只读工具和写入工具应该有不同的准入级别第三请求体在传输过程中有没有被篡改尤其是 SSE 长连接场景下中间人改一个参数你可能完全无感。这篇面向的是用 FastAPI 写自研 MCP 服务、准备接入 AI 工具链的开发者。目标很明确用 OAuth 2.0 做身份层用 HMAC 做请求完整性层再通过 TaoToken 统一 Key 通道把模型调用和工具调用串起来最后用 curl 验证整条认证链路真的生效。你不需要先成为安全专家跟着配置和代码走就能落地。2. TaoToken 统一 Key 通道在认证链路里的位置TaoToken 在这里扮演的是模型侧统一入口的角色。你的 MCP 服务本身负责工具执行和认证校验但工具内部如果要调用大模型做推理、总结或者路由决策就需要一个稳定的 API 通道。TaoToken 提供统一的 Key 管理和 API 接入官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。为什么要在 MCP 认证文章里提这个因为很多自研 MCP 服务的认证设计只考虑了「客户端到 MCP 服务」这一段忽略了「MCP 服务到模型」这一段。如果模型调用用的是硬编码 Key 或者散落在各处的环境变量一旦泄露攻击者可以绕过你的 MCP 认证直接刷模型额度。把模型调用收敛到 TaoToken 统一 Key 通道后你只需要在一个地方管理凭证MCP 服务内部通过环境变量读取配合 HMAC 签名做请求级校验。实际操作上你可以在 TaoToken 控制台创建 API Key然后把它注入到 MCP 服务的运行环境里。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 管理页是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。如果你后面要做长期编码或者 Agent 场景可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。需要验证模型对话行为时用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite Claude Code 相关配置参考 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。注意TaoToken 的 Key 只放在服务端环境变量或密钥管理服务里绝对不要写进 MCP 客户端的 settings.json 或者前端代码。3. 可复制的配置骨架settings.json 与 config.toml先给一份 MCP 客户端侧的 settings.json 骨架适用于 Claude Desktop 或类似支持 MCP 的客户端。这里的关键是把认证头通过 env 注入而不是明文写在 args 里。{ mcpServers: { my-secure-mcp: { command: python, args: [-m, my_mcp_server.main], env: { MCP_OAUTH_ISSUER: https://auth.example.com, MCP_OAUTH_AUDIENCE: mcp-api, MCP_HMAC_SECRET: ${MCP_HMAC_SECRET}, TAOTOKEN_API_KEY: ${TAOTOKEN_API_KEY}, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }服务端用 config.toml 管理认证参数避免散落在代码里[server] host 0.0.0.0 port 8080 tls_cert /etc/mcp/cert.pem tls_key /etc/mcp/key.pem [oauth] issuer https://auth.example.com audience mcp-api jwks_url https://auth.example.com/.well-known/jwks.json token_ttl_seconds 3600 [hmac] enabled true header_name X-MCP-Signature timestamp_header X-MCP-Timestamp nonce_header X-MCP-Nonce max_clock_skew_seconds 300 [taotoken] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY timeout_seconds 30OAuth 2.0 这边建议用授权码模式客户端拿到的 access token 是 JWT服务端通过 JWKS 验签不需要每次请求都去 introspection 端点。HMAC 这边负责请求体防篡改和防重放timestamp 加 nonce 组合校验超过 300 秒的请求直接拒绝。4. FastAPI 侧认证中间件与 HMAC 校验实现下面这段代码可以直接放进你的 FastAPI 项目。核心思路是先用 HTTPBearer 提取 OAuth token验签并解析出 app_id 和 scopes然后对请求体做 HMAC 校验签名不匹配直接 401。import hmac import hashlib import base64 import time from fastapi import FastAPI, Depends, HTTPException, Request from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import jwt, jwk import httpx app FastAPI() bearer HTTPBearer() JWKS_URL https://auth.example.com/.well-known/jwks.json AUDIENCE mcp-api ISSUER https://auth.example.com HMAC_SECRET 从环境变量读取 MAX_SKEW 300 async def get_jwks(): async with httpx.AsyncClient() as client: resp await client.get(JWKS_URL) return resp.json() async def verify_oauth(cred: HTTPAuthorizationCredentials Depends(bearer)): token cred.credentials jwks await get_jwks() try: payload jwt.decode( token, jwks, algorithms[RS256], audienceAUDIENCE, issuerISSUER ) except Exception as e: raise HTTPException(status_code401, detailftoken invalid: {e}) return payload def verify_hmac(request: Request, body: bytes, timestamp: str, nonce: str, signature: str): now int(time.time()) if abs(now - int(timestamp)) MAX_SKEW: raise HTTPException(status_code401, detailtimestamp expired) body_hash hashlib.sha256(body).hexdigest() sign_str f{timestamp}{nonce}{body_hash} expected base64.b64encode( hmac.new(HMAC_SECRET.encode(), sign_str.encode(), hashlib.sha256).digest() ).decode() if not hmac.compare_digest(expected, signature): raise HTTPException(status_code401, detailsignature mismatch) app.post(/mcp) async def mcp_handler(request: Request, claims: dict Depends(verify_oauth)): body await request.body() timestamp request.headers.get(X-MCP-Timestamp, ) nonce request.headers.get(X-MCP-Nonce, ) signature request.headers.get(X-MCP-Signature, ) verify_hmac(request, body, timestamp, nonce, signature) scopes claims.get(scope, ).split() if mcp:write not in scopes: raise HTTPException(status_code403, detailinsufficient scope) return {status: ok, sub: claims.get(sub)}这里有几个细节值得注意。hmac.compare_digest用来防时序攻击不要用比较签名。timestamp 校验窗口设 300 秒既能容忍一定时钟偏差又能限制重放窗口。nonce 建议在 Redis 里存 5 分钟做去重防止同一请求被重复提交。如果你在 MCP 工具内部要调用模型可以这样接 TaoTokenimport os import httpx TAOTOKEN_BASE os.environ[TAOTOKEN_BASE_URL] TAOTOKEN_KEY os.environ[TAOTOKEN_API_KEY] async def call_model(prompt: str): async with httpx.AsyncClient(timeout30) as client: resp await client.post( f{TAOTOKEN_BASE}/v1/chat/completions, headers{Authorization: fBearer {TAOTOKEN_KEY}}, json{model: gpt-4o-mini, messages: [{role: user, content: prompt}]} ) resp.raise_for_status() return resp.json()这样模型调用的凭证和 MCP 认证凭证分离即使 MCP 客户端侧的 HMAC secret 泄露攻击者也无法直接刷模型额度。5. 用 curl 验证认证链路是否真的生效配置写完了不代表生效必须用 curl 逐层验证。先测无 token 的情况应该返回 401curl -i -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:tools/list,id:1}预期输出里 HTTP 状态码是 401detail 提示 token invalid 或缺少 Authorization 头。然后带上 OAuth token 但故意不签名应该被 HMAC 层拦下curl -i -X POST http://localhost:8080/mcp \ -H Authorization: Bearer 你的access_token \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:tools/list,id:1}预期 401detail 是 signature mismatch 或 timestamp expired。最后构造完整签名请求。先用 Python 生成签名import hmac, hashlib, base64, time, uuid, json secret 你的HMAC_SECRET body json.dumps({jsonrpc:2.0,method:tools/list,id:1}, separators(,,:)) timestamp str(int(time.time())) nonce uuid.uuid4().hex body_hash hashlib.sha256(body.encode()).hexdigest() sign_str f{timestamp}{nonce}{body_hash} signature base64.b64encode( hmac.new(secret.encode(), sign_str.encode(), hashlib.sha256).digest() ).decode() print(ftimestamp{timestamp}) print(fnonce{nonce}) print(fsignature{signature}) print(fbody{body})拿到输出后执行curl -i -X POST http://localhost:8080/mcp \ -H Authorization: Bearer 你的access_token \ -H Content-Type: application/json \ -H X-MCP-Timestamp: timestamp \ -H X-MCP-Nonce: nonce \ -H X-MCP-Signature: signature \ -d body成功时返回 200 和{status:ok,sub:...}。如果返回 403 insufficient scope说明 token 的 scope 里没有mcp:write去授权服务器调整 scope 配置。验证模型通道是否通可以单独跑一条curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:ping}]}返回正常 JSON 就说明 TaoToken 通道没问题。6. 本篇常见错排查签名一直 mismatch最常见的原因是 body 被 FastAPI 重新序列化了。你在生成签名时用的 body 字符串必须和实际发送的字节完全一致。建议在客户端侧先json.dumps(..., separators(,,:))固定格式服务端用await request.body()拿原始字节不要用 Pydantic 模型反序列化后再重新序列化。timestamp expired 频繁出现检查服务器和客户端的系统时间是否同步用ntpdate或 chrony 对齐。如果客户端在容器里容器时间可能和宿主机有偏差。JWKS 拉取失败确认jwks_url可访问且返回的 key 里有kid和use: sig。如果授权服务器用了自签证书httpx 需要配置 verify 或者挂载 CA。scope 校验不生效JWT 里的 scope 可能是字符串也可能是数组解析时要兼容。另外注意 OAuth 授权码流程里请求的 scope 和授权服务器实际签发的 scope 可能不一致去 token 端点确认。TaoToken 调用返回 401检查TAOTOKEN_API_KEY是否从环境变量正确读取以及 base_url 是否带了/api后缀。如果是在 Docker 里跑确认 env 传递没有遗漏。SSE 长连接下 HMAC 校验失败SSE 场景下请求体可能分块传输HMAC 应该只对初始请求体签名后续事件流不再重复校验。如果你的实现把每个 chunk 都拿去算签名必然失败。排障时优先看 API Keys 和接入文档 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。需要快速验证模型行为是否正常用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 。长期跑编码 Agent 的话Coding Plan 的配置方式在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。整套链路跑通后建议把 HMAC secret 和 TaoToken Key 都放进密钥管理服务设置定期轮换。MCP 服务的日志里记录 app_id、timestamp、nonce 和操作类型方便审计。速率限制用 FastAPI 的 slowapi 或者网关层做单 Key 每分钟 100 次起步高风险工具单独限流。