MCPServer对接 OAuth 授权---AI翻译系列

 

MCP Server 对接 OAuth 授权服务器 — 架构指南

基于 MCP 协议规范 + OAuth 2.1,从原理到完整调用流程,涵盖多客户端多服务端场景。


目录

  1. 核心角色与概念

  2. 为什么 Token 给 Server 而不是 Client

  3. 完整调用流程(从零开始)

  4. Session 的生命周期与状态变化

  5. 多 Client 连同一个 Server

  6. 同一个 Client 连多个 Server

  7. Token 过期与刷新机制

  8. 安全防护机制

  9. 与 CSDN 参考文章的对比


1. 核心角色与概念

1.1 四个角色

角色说明类比
用户(Resource Owner) 资源的所有者 保险柜的主人
MCP Client 调用工具的 AI 应用(ChatBot 等) 需要取资料的朋友
MCP Server 提供工具能力,代理用户访问资源 银行前台
OAuth Server 验证用户身份,发放 token 银行安保系统

1.2 通信协议

  • 协议:JSON-RPC 2.0

  • 传输方式

    • stdio:子进程 stdin/stdout(本地场景)

    • Streamable HTTP:HTTP POST + 可选 SSE 流(远程场景)

  • 请求响应匹配:靠 JSON-RPC 的 id 字段,即使乱序也能匹配

1.3 关键标识

标识生成方用途
Mcp-Session-Id Server 生成,通过 Response Header 发给 Client 标识一个连接会话
access_token OAuth Server 发放 访问真实资源 API(短期,1小时)
refresh_token OAuth Server 发放 换新 token(长期,30天)
code OAuth Server 生成 一次性授权码,用于换 token
mcp_token Server 自己签发 Client 访问 Server 的受限凭证(可选,简化版可省略)

2. 为什么 Token 给 Server 而不是 Client

2.1 传统 OAuth vs MCP OAuth

传统 OAuth(如"用微信登录第三方网站"):
用户授权 → Client 拿到 token → Client 直接拿 token 去调 API
问题:Client 手里有"万能钥匙",能反复直接访问

MCP OAuth:
用户授权 → MCP Server 拿到 token → Client 拿到的不是 token,而是"访问 MCP Server 的凭证"
好处:Client 永远接触不到真正的 OAuth token

2.2 设计原因

  1. 安全:Client 不可信(可能是第三方写的、可能被反编译),token 给 Server 才能控制权限范围

  2. 最小权限:OAuth token 可能权限很大,Server 只给 Client 暴露"搜索工具"、"读取文件"等有限能力

  3. 可审计:所有真实 API 调用都经过 Server,Server 知道谁在什么时候调了什么


3. 完整调用流程(从零开始)

阶段 1:建立连接 + 握手(initialize)

没有任何前置条件,Client 直接发请求。

Client 发送:

POST /mcp HTTP/1.1
Host: mcp-server.com
Content-Type: application/json
Accept: application/json, text/event-stream

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
  "protocolVersion": "2024-11-05",
  "capabilities": {},
  "clientInfo": {
    "name": "my-chatbot",
    "version": "1.0.0"
  }
}
}

Server 响应:

HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: sess-aaa-111             ← Server 分配

{
"jsonrpc": "2.0",
"id": 1,
"result": {
  "protocolVersion": "2024-11-05",
  "capabilities": {
    "tools": {},
    "resources": {}
  },
  "serverInfo": {
    "name": "file-search-server",
    "version": "2.0.0"
  }
}
}

Server 内部操作:

sessions = {}

def handle_initialize(request):
   session_id = generate_uuid()  # → "sess-aaa-111"

   sessions[session_id] = {
       "id":              "sess-aaa-111",
       "created_at":      "2025-07-01T10:00:00Z",
       "user":            None,           # 还不知道是谁
       "oauth_token":     None,           # 还没授权
       "refresh_token":   None,
       "status":          "connected",    # 已连接但未授权
       "protocol_version": "2024-11-05"
  }

   return Response(
       json={"result": { ... }},
       headers={"Mcp-Session-Id": session_id}
  )

Client 内部操作:

class MCPClient:
   def __init__(self, server_url):
       self.server_url = server_url
       self.session_id = None

   def connect(self):
       response = http_post(self.server_url, json={
           "jsonrpc": "2.0",
           "id": 1,
           "method": "initialize",
           "params": {
               "protocolVersion": "2024-11-05",
               "capabilities": {},
               "clientInfo": {"name": "my-chatbot", "version": "1.0.0"}
          }
      })
       self.session_id = response.headers["Mcp-Session-Id"]
       # → "sess-aaa-111"

此时 Session 表状态:

Server 的 Session 表:
┌────────────────┬──────────┬─────────┬───────────┬────────────┐
│ Session-Id     │ 状态     │ 用户   │ OAuth Token│ Refresh   │
├────────────────┼──────────┼─────────┼───────────┼────────────┤
│ sess-aaa-111   │ connected│ (无)   │ (无)     │ (无)       │
└────────────────┴──────────┴─────────┴───────────┴────────────┘

Client 存储:
session_id = "sess-aaa-111"

阶段 2:Client 尝试调用工具(未授权)

Client 发送:

POST /mcp HTTP/1.1
Mcp-Session-Id: sess-aaa-111

{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
  "name": "search_drive",
  "arguments": { "query": "年度报告" }
}
}

Server 响应:

HTTP/1.1 401 Unauthorized
Mcp-Session-Id: sess-aaa-111

{
"jsonrpc": "2.0",
"id": 2,
"error": {
  "code": 401,
  "message": "未授权,请先完成 OAuth 授权",
  "data": {
    "authorization_url": "https://oauth.server.com/authorize?client_id=mcp-srv&redirect_uri=http://mcp-server.com/callback&scope=read:files&state=sess-aaa-111"
  }
}
}

此时 Session 表状态(未变化):

┌────────────────┬──────────┬─────────┬───────────┬────────────┐
│ Session-Id     │ 状态     │ 用户   │ OAuth Token│ Refresh   │
├────────────────┼──────────┼─────────┼───────────┼────────────┤
│ sess-aaa-111   │ connected│ (无)   │ (无)     │ (无)       │
│               │ 未授权   │         │           │           │
└────────────────┴──────────┴─────────┴───────────┴────────────┘

阶段 3:OAuth 授权(浏览器参与)

步骤 1:Client 打开浏览器
═══════════════════════════════════════════════════════

auth_url = "https://oauth.server.com/authorize?" + urlencode({
    "client_id":     "mcp-srv",
    "redirect_uri": "http://mcp-server.com/callback", ← 提前注册的白名单地址
    "scope":         "read:files",
    "state":         "sess-aaa-111",     ← 绑定 Session
    "response_type": "code"
})

webbrowser.open(auth_url)


步骤 2:用户在 OAuth Server 登录 + 点同意
═══════════════════════════════════════════════════════

浏览器 ──GET──▶ https://oauth.server.com/authorize?...&redirect_uri=http://mcp-server.com/callback

OAuth Server 验证:
  ✓ client_id 存在
  ✓ redirect_uri 在白名单中
  ✓ scope 合法


步骤 3:OAuth Server 302 重定向
═══════════════════════════════════════════════════════

OAuth Server ──302──▶ 浏览器
  Location: http://mcp-server.com/callback?code=auth-code-xyz&state=sess-aaa-111
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            就是 Client 之前写在 redirect_uri 里的地址


步骤 4:浏览器访问 callback
═══════════════════════════════════════════════════════

浏览器 ──GET──▶ http://mcp-server.com/callback?code=auth-code-xyz&state=sess-aaa-111


步骤 5:MCP Server 拿 code 换 token(后端对后端)
═══════════════════════════════════════════════════════

MCP Server ──POST──▶ https://oauth.server.com/token
  {
    "grant_type":   "authorization_code",
    "code":         "auth-code-xyz",
    "client_id":     "mcp-srv",
    "client_secret": "my-secret-key",
    "redirect_uri": "http://mcp-server.com/callback"
  }

OAuth Server ──▶ MCP Server
  {
    "access_token": "eyJhbG...",
    "token_type":   "Bearer",
    "expires_in":   3600,
    "refresh_token": "refresh-xyz"
  }


步骤 6:Server 更新 Session
═══════════════════════════════════════════════════════

session = sessions["sess-aaa-111"]
session["oauth_token"] = "eyJhbG..."
session["refresh_token"] = "refresh-xyz"
session["user"]         = decode_token(...)["user"]
session["status"]       = "authorized"

授权后 Session 表状态:

┌────────────────┬────────────┬──────────────┬───────────────┬──────────────┐
│ Session-Id     │ 状态        │ 用户          │ OAuth Token   │ Refresh      │
├────────────────┼────────────┼──────────────┼───────────────┼──────────────┤
│ sess-aaa-111   │ authorized │ 张三(user-001)│ eyJhbG...     │ refresh-xyz  │
└────────────────┴────────────┴──────────────┴───────────────┴──────────────┘

阶段 4:Client 怎么知道授权成功了

方式 1:轮询(最常用)

def call_tool_with_auth_retry(self, tool_name, args, max_retries=20):
    for i in range(max_retries):
        response = http_post(self.server_url,
            headers={"Mcp-Session-Id": self.session_id},
            json={"method": "tools/call", "params": {"name": tool_name, "arguments": args}}
        )

        if response.status == 200:
            return response.json()  # 成功了

        if response.status == 401:
            if i == 0:
                auth_url = response.json()["error"]["data"]["authorization_url"]
                webbrowser.open(auth_url)
                print("请在浏览器中完成授权...")
            time.sleep(3)  # 等 3 秒再试
            continue

    raise Exception("授权超时")
时间线:
  t=0s    Client 发请求 → 401 → 打开浏览器 → 开始轮询
  t=3s    Client 发请求 → 401 → 继续等
  t=6s    Client 发请求 → 401 → 继续等
  t=8s    用户在浏览器点同意 → token 存入 Session
  t=9s    Client 发请求 → 200 → 成功!停止轮询

方式 2:WebSocket/回调通知(适合 Web 应用)

  MCP Server ──WebSocket──▶ Client: { "type": "auth_success", "session_id": "sess-aaa-111" }

阶段 5:Client 调用工具(已授权)

Client 发送:

POST /mcp HTTP/1.1
Mcp-Session-Id: sess-aaa-111

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "search_drive",
    "arguments": { "query": "年度报告" }
  }
}

Server 处理:

def handle_tool_call(request):
    session_id = request.headers["Mcp-Session-Id"]
    session = sessions[session_id]

    if session["status"] == "authorized":
        # 用 token 去调真正的资源 API
        api_response = http_get(
            "https://api.drive.com/v3/files?q=年度报告",
            headers={"Authorization": f"Bearer {session['oauth_token']}"}
        )

        return Response(json={
            "jsonrpc": "2.0",
            "id": 3,
            "result": {
                "content": [{"type": "text", "text": "找到文件:年度报告.pdf"}]
            }
        })

Server 响应:

HTTP/1.1 200 OK
Mcp-Session-Id: sess-aaa-111

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{"type": "text", "text": "找到文件:年度报告.pdf"}]
  }
}

完整时序图

Client                Browser              MCP Server             OAuth Server
  │                      │                     │                      │
  │  1.initialize        │                     │                      │
  │─────────────────────▶│(经过Server)         │                      │
  │                      │                     │                      │
  │  2.200 + Session-Id  │                     │                      │
  │◀─────────────────────│                     │                      │
  │                      │                     │                      │
  │  3.tools/call        │                     │                      │
  │──sess-aaa───────────▶│                     │                      │
  │                      │                     │                      │
  │  4.401 + auth_url    │                     │                      │
  │◀─────────────────────│                     │                      │
  │                      │                     │                      │
  │  5.打开浏览器         │                     │                      │
  │─────────────────────▶│                     │                      │
  │                      │  6.登录+授权         │                      │
  │                      │────────────────────────────────────────────▶│
  │                      │                     │                      │
  │                      │  7.302 → callback   │                      │
  │                      │◀────────────────────────────────────────────│
  │                      │                     │                      │
  │  8.轮询(等3秒)        │  9.访问 callback     │                      │
  │─────────────────────▶│─────────────────────▶│                      │
  │                      │                     │                      │
  │                      │                     │  10.code换token        │
  │                      │                     │─────────────────────▶│
  │                      │                     │                      │
  │                      │                     │  11.access_token       │
  │                      │                     │◀─────────────────────│
  │                      │                     │                      │
  │                      │  12.HTML 成功页面    │  ← Session 状态变为    │
  │                      │◀────────────────────│    authorized         │
  │                     │                     │                      │
  │  13.tools/call       │                     │                      │
  │──sess-aaa───────────▶│                     │                      │
  │                      │                     │  14.用token调API       │
  │                      │                     │─────────────────────▶│
  │                      │                     │                      │
  │                      │                     │  15.返回数据           │
  │                      │                     │◀─────────────────────│
  │                      │                     │                      │
  │  16.200 + 结果       │                     │                      │
  │◀─────────────────────│                     │                      │

4. Session 的生命周期与状态变化

4.1 Session 状态机

                    Client 第一次连接
        ┌──────────────────────────────────────┐
        │                                      ▼
   ┌─────────┐     OAuth 授权成功      ┌─────────────┐      Token 过期       ┌──────────┐
   │ 已连接   │ ──────────────────▶   │  已授权       │ ──────────────────▶  │ 需刷新    │
   │ connected│                        │  authorized   │                     │ refreshing│
   └─────────┘                        └─────────────┘                     └──────────┘
        │                                    │                                  │
        │ 用户主动撤销                        │ refresh_token 也过期              │ 刷新成功
        ▼                                    ▼                                  ▼
   ┌─────────┐                        ┌─────────────┐                     ┌─────────────┐
   │ 已撤销   │                        │  需重新授权   │                     │  已授权      │
   │ revoked  │                        │ re-auth      │                     │ authorized   │
   └─────────┘                        └─────────────┘                     └─────────────┘

4.2 各阶段 Session 表内容变化

阶段 0:初始(无连接)
┌──────────┬──────┬────┬───────┬────────┐
│ Session  │ 状态  │用户│ Token │ Refresh│
├──────────┼──────┼────┼───────┼────────┤
│ (空)    │      │    │       │        │
└──────────┴──────┴────┴───────┴────────┘

阶段 1:Client 连接,分配 Session-Id
┌────────────┬───────────┬──────┬───────┬────────┐
│ Session-Id │ 状态      │ 用户  │ Token │ Refresh│
├────────────┼───────────┼──────┼───────┼────────┤
│ sess-aaa   │ connected │ (无) │ (无)  │ (无)   │
└────────────┴───────────┴──────┴───────┴────────┘

阶段 2:OAuth 授权成功
┌────────────┬────────────┬─────────────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户         │ Token    │ Refresh    │
├────────────┼────────────┼─────────────┼──────────┼────────────┤
│ sess-aaa   │ authorized │ 张三(user-001)│ tok-aaa │ ref-aaa    │
└────────────┴────────────┴─────────────┴──────────┴────────────┘

阶段 3:access_token 过期,用 refresh_token 刷新
┌────────────┬────────────┬─────────────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户         │ Token    │ Refresh    │
├────────────┼────────────┼─────────────┼──────────┼────────────┤
│ sess-aaa   │ authorized │ 张三(user-001)│ tok-bbb★ │ ref-bbb★   │
└────────────┴────────────┴─────────────┴──────────┴────────────┘
                                              ↑ 新的      ↑ 新的(轮换)

阶段 4:refresh_token 也过期
┌────────────┬────────────┬─────────────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户         │ Token    │ Refresh    │
├────────────┼────────────┼─────────────┼──────────┼────────────┤
│ sess-aaa   │ re-auth    │ 张三(user-001)│ (过期)   │ (过期)     │
└────────────┴────────────┴─────────────┴──────────┴────────────┘
  → 需要用户重新打开浏览器走一遍 OAuth 流程

阶段 5:用户主动撤销
┌────────────┬────────────┬─────────────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户         │ Token    │ Refresh    │
├────────────┼────────────┼─────────────┼──────────┼────────────┤
│ sess-aaa   │ revoked    │ 张三(user-001)│ (作废)   │ (作废)     │
└────────────┴────────────┴─────────────┴──────────┴────────────┘
  → 需要重新走一遍完整的连接+授权流程

5. 多 Client 连同一个 Server

5.1 场景

  Client A(张三的 ChatBot)──┐
  Client B(李四的 ChatBot)──┼──▶ 同一个 MCP Server(localhost:3000)
  Client C(王五的 ChatBot)──┘

5.2 Server 靠 Mcp-Session-Id 区分

每个 Client 第一次连接时,Server 各自分配不同的 Session-Id:

Client A ──POST /mcp──▶ Server → 分配 sess-aaa-111
Client B ──POST /mcp──▶ Server → 分配 sess-bbb-222
Client C ──POST /mcp──▶ Server → 分配 sess-ccc-333

后续请求各自带自己的 Session-Id:

Client A ──sess-aaa-111──▶ Server → 查表 → 张三的 token
Client B ──sess-bbb-222──▶ Server → 查表 → 李四的 token
Client C ──sess-ccc-333──▶ Server → 查表 → 王五的 token

5.3 Server 的 Session 表

┌────────────────┬────────────┬──────────────┬───────────────┬──────────────┐
│ Session-Id     │ 状态        │ 用户          │ OAuth Token   │ Refresh      │
├────────────────┼────────────┼──────────────┼───────────────┼──────────────┤
│ sess-aaa-111   │ authorized │ 张三(user-A)  │ tok-aaa       │ ref-aaa      │
│ sess-bbb-222   │ authorized │ 李四(user-B)  │ tok-bbb       │ ref-bbb      │
│ sess-ccc-333   │ connected  │ (无)          │ (无)          │ (无)         │
└────────────────┴────────────┴──────────────┴───────────────┴──────────────┘

说明:
  张三、李四已授权 → 可以正常调工具
  王五还未授权 → 调工具会收到 401
  王五未授权不影响张三和李四

5.4 各 Client 互不影响

场景:张三的 token 过期了
═══════════════════════════════════════
  → 只影响 sess-aaa-111
  → 李四的 sess-bbb-222 不受影响,继续正常工作
  → 张三用 refresh_token 刷新后,sess-aaa-111 恢复正常

场景:李四撤销授权
═══════════════════════════════════════
  → 只影响 sess-bbb-222,状态变为 revoked
  → 张三、王五不受影响
  → 李四需要重新走 OAuth 流程才能恢复

场景:王五还没授权
═══════════════════════════════════════
  → sess-ccc-333 状态为 connected
  → 王五的请求会收到 401 + 授权 URL
  → 张三、李四正常工作

5.5 各 Client 存储的内容

Client A 存储:
  session_id = "sess-aaa-111"

Client B 存储:
  session_id = "sess-bbb-222"

Client C 存储:
  session_id = "sess-ccc-333"

每个 Client 只知道自己的 Session-Id
不知道其他 Client 的存在

6. 同一个 Client 连多个 Server

6.1 场景

  Client A ──▶ Server 1(文件服务)   Session-Id: sess-A1
  Client A ──▶ Server 2(邮件服务)   Session-Id: sess-A2
  Client A ──▶ Server 3(日历服务)   Session-Id: sess-A3

  Client B ──▶ Server 1(文件服务)   Session-Id: sess-B1
  Client B ──▶ Server 2(邮件服务)   Session-Id: sess-B2

6.2 Client 内部存储

class MCPClient:
    def __init__(self):
        # 每个 Server 对应一个连接对象
        self.connections = {
            "文件服务": {
                "server_url":  "http://file-srv:3001/mcp",
                "session_id":  "sess-A1"   # Server 1 分配的
            },
            "邮件服务": {
                "server_url":  "http://mail-srv:3002/mcp",
                "session_id":  "sess-A2"   # Server 2 分配的
            },
            "日历服务": {
                "server_url":  "http://cal-srv:3003/mcp",
                "session_id":  "sess-A3"   # Server 3 分配的
            }
        }

    def call_tool(self, server_name, tool, args):
        conn = self.connections[server_name]
        return http_post(conn["server_url"],
            headers={"Mcp-Session-Id": conn["session_id"]},
            json={"method": "tools/call", "params": {"name": tool, "arguments": args}}
        )

6.3 各 Server 各自的 Session 表

文件服务(Server 1)的 Session 表:
┌────────────────┬────────────┬──────────────┬───────────────┐
│ Session-Id     │ 状态        │ 用户          │ Token         │
├────────────────┼────────────┼──────────────┼───────────────┤
│ sess-A1        │ authorized │ 张三          │ tok-文件-A    │
│ sess-B1        │ authorized │ 李四          │ tok-文件-B    │
└────────────────┴────────────┴──────────────┴───────────────┘

邮件服务(Server 2)的 Session 表:
┌────────────────┬────────────┬──────────────┬───────────────┐
│ Session-Id     │ 状态        │ 用户          │ Token         │
├────────────────┼────────────┼──────────────┼───────────────┤
│ sess-A2        │ authorized │ 张三          │ tok-邮件-A    │
│ sess-B2        │ authorized │ 李四          │ tok-邮件-B    │
└────────────────┴────────────┴──────────────┴───────────────┘

日历服务(Server 3)的 Session 表:
┌────────────────┬────────────┬──────────────┬───────────────┐
│ Session-Id     │ 状态        │ 用户          │ Token         │
├────────────────┼────────────┼──────────────┼───────────────┤
│ sess-A3        │ connected  │ (无)          │ (无)          │
│ sess-B3        │ authorized │ 李四          │ tok-日历-B    │
└────────────────┴────────────┴──────────────┴───────────────┘

6.4 各 Server 之间完全独立

  → 文件服务不知道张三还连着邮件服务
  → 邮件服务不知道张三还连着日历服务
  → 张三在文件服务的 token 过期,不影响邮件服务的访问
  → 张三还没授权日历服务,不影响文件和邮件的使用

6.5 全景图

┌─────────────────────────────────────────────────────────────────┐
│  Client A(张三)                                                │
│                                                                 │
│  ┌──────────┬─────────────────┬──────────────────┐             │
│  │ 服务      │ 地址             │ Session-Id       │             │
│  ├──────────┼─────────────────┼──────────────────┤             │
│  │ 文件服务  │ file-srv:3001   │ sess-A1          │             │
│  │ 邮件服务  │ mail-srv:3002   │ sess-A2          │             │
│  │ 日历服务  │ cal-srv:3003    │ sess-A3          │             │
│  └──────────┴─────────────────┴──────────────────┘             │
└───────┬──────────────┬──────────────┬───────────────────────────┘
        │              │              │
        │ sess-A1      │ sess-A2      │ sess-A3
        ▼              ▼              ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│  文件服务      │ │  邮件服务      │ │  日历服务      │
│               │ │               │ │               │
│ Session 表:   │ │ Session 表:   │ │ Session 表:   │
│ sess-A1 张三   │ │ sess-A2 张三   │ │ sess-A3 未授权 │
│ sess-B1 李四   │ │ sess-B2 李四   │ │ sess-B3 李四   │
│               │ │               │ │               │
│ 不知道其他连接 │ │ 不知道其他连接 │ │ 不知道其他连接 │
└───────────────┘ └───────────────┘ └───────────────┘

7. Token 过期与刷新机制

7.1 两种 Token

Token用途有效期谁持有
access_token 调 API 用 短(1 小时) MCP Server
refresh_token 换新 token 用 长(30 天) MCP Server

7.2 刷新流程

def call_api_with_auto_refresh(session):
    token = session["oauth_token"]

    response = http_get(api_url, headers={"Authorization": f"Bearer {token}"})

    if response.status == 401:
        # access_token 过期 → 用 refresh_token 换新
        new_tokens = http_post("https://oauth.server.com/token", data={
            "grant_type":    "refresh_token",
            "refresh_token": session["refresh_token"],
            "client_id":     "mcp-srv",
            "client_secret": "my-secret-key"
        })

        # 更新 Session(token 轮换:新的 refresh_token 替换旧的)
        session["oauth_token"]  = new_tokens["access_token"]
        session["refresh_token"] = new_tokens["refresh_token"]

        # 用新 token 重新请求
        response = http_get(api_url,
            headers={"Authorization": f"Bearer {new_tokens['access_token']}"}
        )

    return response

7.3 时间线

00:00  用户授权 → 拿到 access_token (管1小时) + refresh_token (管30天)
00:30  access_token 有效,正常使用
01:00  access_token 过期 → 用 refresh_token 换新 → 新的 access_token 又管1小时
02:00  新的 access_token 又过期 → 再用 refresh_token 换新
...
30天后  refresh_token 也过期 → 用户需要重新打开浏览器登录

7.4 refresh_token 过期后的 Session 状态

刷新前:
┌────────────┬────────────┬──────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户  │ Token    │ Refresh    │
├────────────┼────────────┼──────┼──────────┼────────────┤
│ sess-aaa   │ authorized │ 张三  │ tok-old  │ ref-old    │
└────────────┴────────────┴──────┴──────────┴────────────┘

刷新后:
┌────────────┬────────────┬──────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户  │ Token    │ Refresh    │
├────────────┼────────────┼──────┼──────────┼────────────┤
│ sess-aaa   │ authorized │ 张三  │ tok-new★ │ ref-new★   │
└────────────┴────────────┴──────┴──────────┴────────────┘

refresh_token 也过期:
┌────────────┬────────────┬──────┬──────────┬────────────┐
│ Session-Id │ 状态       │ 用户  │ Token    │ Refresh    │
├────────────┼────────────┼──────┼──────────┼────────────┤
│ sess-aaa   │ re-auth    │ 张三  │ (过期)   │ (过期)     │
└────────────┴────────────┴──────┴──────────┴────────────┘
  → Client 收到 401 → 重新打开浏览器走 OAuth 流程

8. 安全防护机制

8.1 为什么拦截 callback 没用

攻击者拦截 callback → 拿到 code
但 code 换 token 需要 client_secret(只有 Server 知道)
→ 没有 client_secret → 换不了 token

额外防线:
  ✓ code 一次性(用过作废)
  ✓ code 短有效期(5-10 分钟)
  ✓ redirect_uri 二次验证(换 token 时验证是否一致)
  ✓ state 参数绑定 Session(防止 CSRF)

8.2 redirect_uri 白名单

部署时注册:
  OAuth Server 后台 → 设置回调白名单
    ✓ http://mcp-server.com/callback
    ✓ http://localhost:3000/callback

运行时:
  Client 构造的 redirect_uri 必须在白名单里
  否则 OAuth Server 直接拒绝

8.3 state 防 CSRF

Client 生成随机 state = "sess-aaa-111"
→ 发给 OAuth Server
→ OAuth Server 原样带回
→ Client 验证 callback 里的 state 和自己发出去的是否一致
→ 不一致 → 拒绝(可能是攻击者伪造的回调)

感谢:LongCat-AI友情赞助


附录:关键问题速查

Q1:Session-Id 什么时候产生?

Client 第一次 initialize 握手时就分配了,不需要等认证。一开始是"空壳 Session",OAuth 完成后才被填入用户身份和 token。

Q2:Session-Id 放在哪?

Session 表只在 Server 端。Client 只存自己的 Session-Id 一个字符串。

Q3:多个 Client 连同一个 Server 怎么区分?

Server 给每个 Client 分配不同的 Session-Id,靠 Mcp-Session-Id Header 区分。

Q4:一个 Client 连多个 Server 怎么管理?

Client 按服务名分开存:connections["文件服务"].session_id = "sess-A1",各 Server 各自管自己的 Session 表。

Q5:Client 怎么知道授权成功了?

轮询:打开浏览器后每隔几秒用同一个 Session-Id 试一次,试到 200 就说明授权成功了。

Q6:token 过期了怎么办?

Server 用 refresh_token 自动换新,用户无感知。refresh_token 也过期了才需要用户重新登录。

Q7:为什么不直接给 Client token?

Client 不可信。token 给 Server 才能让 Server 控制权限范围,即使 Client 被攻破,真 token 也不会泄露。

Q8:拦截 callback 地址能盗取 token 吗?

不能。拦截只能拿到 code,但 code 换 token 需要 client_secret(只有 Server 知道),且 code 一次性、短有效期、绑定 redirect_uri。

posted @ 2026-07-01 21:40  凌界  阅读(4)  评论(0)    收藏  举报