登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

MCP 服务端授权范围与会话隔离的配置

来源:17golang原创

时间:2026-10-10 18:14:50 161浏览 收藏

MCP 服务端的授权范围与会话隔离,核心不是给每个工具再加一层模糊的“是否允许”判断,而是把边界提前固定在请求入口、身份上下文和工具策略上。远程 MCP 服务应先验证 Bearer 令牌,再把主体、租户和 scope 绑定到本次请求,最后由工具策略决定可见能力。

官方资料:https://modelcontextprotocol.io/

还要注意一个容易混淆的点:MCP 规范的新版本正在移除传输层的协议级 session 标识。本文所说的“会话隔离”,指业务侧的授权上下文隔离,例如主体、租户、scope 集合和审计关联键,而不是把权限寄托在一条长连接或某个进程内对象上。

先划清用户、会话与工具权限三层边界

可以把一次远程调用拆成三个相互关联但不应混为一谈的边界:

  • 身份与令牌:令牌代表哪个主体,签发者是谁,资源受众是否匹配,以及令牌拥有哪些 scope。
  • 请求与会话:本次请求属于哪个租户、哪个业务会话或哪个调用关联键;这些字段必须由受信来源构造,不能直接相信客户端随意提交的租户 ID。
  • 工具授权面:scope 只表达能力范围,工具策略再把能力范围映射到只读、写入或管理工具。
MCP Host、OAuth 访问令牌、资源服务器、会话上下文、Scope 策略与工具之间的静态关系说明图
图1:MCP 授权范围的三层边界说明图,展示身份令牌、请求会话与工具策略之间的静态依赖;这是说明图,不是截图或运行证据。

这张结构图里,资源服务器是边界收口点,不能让只读工具自行解析原始令牌,也不能让写入工具自行决定租户。统一入口校验后,工具只消费已经整理过的授权上下文。

在 HTTP 入口校验 Bearer 令牌

服务端首先要成为 OAuth resource server,而不是在 MCP 工具内部自己“发 token”或直接信任客户端传来的用户字段。以 MCP TypeScript SDK 的远程 HTTP 入口为例,验证器负责验签、签发者、受众、过期时间和 scope,路由中间件负责把拒绝请求挡在 MCP handler 之前。

import {
  createMcpExpressApp,
  getOAuthProtectedResourceMetadataUrl,
  requireBearerAuth,
} from "@modelcontextprotocol/express";
import { createMcpHandler } from "@modelcontextprotocol/server";

const resourceUrl = new URL("https://api.example.com/mcp");

const verifier = {
  async verifyAccessToken(token: string) {
    // 这里应调用身份提供商的 JWKS 验签,并同时检查 iss、aud、exp。
    const claims = await verifyJwtWithProviderKeys(token);
    if (claims.aud !== resourceUrl.origin) {
      // 令牌属于其他资源时直接拒绝,避免跨资源复用访问令牌。
      throw new Error("token audience does not match MCP resource");
    }
    return {
      clientId: String(claims.sub),
      scopes: Array.isArray(claims.scope) ? claims.scope : [],
      extra: { tenantId: String(claims.tenant_id) },
    };
  },
};

const auth = requireBearerAuth({
  verifier,
  // 先要求进入 MCP 资源的基础 scope,再在工具策略层细分权限。
  requiredScopes: ["mcp"],
  expectedResource: resourceUrl,
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resourceUrl),
});

const app = createMcpExpressApp({
  host: "0.0.0.0",
  allowedHosts: ["api.example.com"],
});
const handler = createMcpHandler(buildServer);

// 认证中间件必须位于 MCP handler 之前,避免未授权请求进入工具层。
app.all("/mcp", auth, (req, res) => void handler(req, res, req.body));

缺少令牌、令牌格式错误或令牌过期时,应返回带有 WWW-Authenticate 挑战的 401;令牌有效但缺少所需 scope 时,应返回 403。不要把这两种情况包装成普通工具返回值,否则客户端无法按照 OAuth 资源发现规则继续处理。

把 scope 映射到明确的工具授权面

基础的 mcp scope 只说明调用方可以接触这个资源,不代表它可以执行所有工具。实际配置可以把能力拆成 data:read、data:write 和 admin:manage,再由每个工具声明所需 scope。

type ToolPolicy = {
  requiredScopes: string[];
  // 是否允许该工具读取或改变租户数据,便于审计和发布检查。
  dataEffect: "read" | "write" | "admin";
};

const toolPolicies: Record = {
  list_records: { requiredScopes: ["mcp", "data:read"], dataEffect: "read" },
  update_record: { requiredScopes: ["mcp", "data:write"], dataEffect: "write" },
  rotate_keys: { requiredScopes: ["mcp", "admin:manage"], dataEffect: "admin" },
};

function canCallTool(toolName: string, scopes: Set): boolean {
  const policy = toolPolicies[toolName];
  if (!policy) return false;

  // 所有声明的 scope 都必须存在,不能用任意一个 scope 放行。
  return policy.requiredScopes.every((scope) => scopes.has(scope));
}

function authorizeTool(toolName: string, authInfo: AuthInfo) {
  const scopes = new Set(authInfo.scopes);
  if (!canCallTool(toolName, scopes)) {
    // 拒绝时只记录工具名和主体标识,绝不把原始 token 写入日志。
    throw new ForbiddenError("insufficient_scope");
  }
  return toolPolicies[toolName];
}

工具策略要保持单向关系:令牌 scope 可以收敛出工具集合,但工具名称不能反向扩大主体权限。对于写入或管理类工具,宁可要求更窄的 scope,也不要因为调用方拥有基础访问权就自动开放。

把会话隔离落到请求上下文

会话隔离的关键是每次请求都重新构造授权上下文。可以使用经过验证的主体作为根,结合服务端生成或可信上游传入的会话键和租户上下文;不要以客户端提交的 tenantId 覆盖令牌中的租户声明,也不要在全局单例里缓存“当前用户”。

HTTP 请求、已验证主体、会话键、租户上下文、Scope 集合、工具注册表与审计记录的静态关系说明图
图2:请求级授权上下文说明图,展示主体、会话键、租户、scope、工具注册表和审计记录的归属关系;这是说明图,不是截图或运行证据。
type RequestContext = {
  subject: string;
  tenantId: string;
  sessionKey: string;
  scopes: ReadonlySet;
};

function buildRequestContext(authInfo: AuthInfo, requestId: string): RequestContext {
  const tenantId = String(authInfo.extra?.tenantId || "");
  if (!tenantId) {
    // 没有租户归属时拒绝进入租户数据工具,避免形成跨租户默认空间。
    throw new ForbiddenError("tenant_context_required");
  }

  return {
    subject: authInfo.clientId,
    tenantId,
    // 会话键只用于关联同一业务上下文,不把它当成授权凭据。
    sessionKey: `${tenantId}:${requestId}`,
    scopes: new Set(authInfo.scopes),
  };
}

async function callToolSafely(
  toolName: string,
  authInfo: AuthInfo,
  requestId: string,
  args: unknown,
) {
  const context = buildRequestContext(authInfo, requestId);
  const policy = authorizeTool(toolName, authInfo);

  // 所有数据访问都显式带上租户上下文,避免依赖进程级“当前租户”。
  const result = await toolRegistry[toolName]({
    context,
    policy,
    args,
  });
  await auditLog({ requestId, subject: context.subject, tenantId: context.tenantId, toolName, effect: policy.dataEffect });
  return result;
}

如果服务部署在多实例环境,不应把隔离正确性建立在单机内存 session 上。需要跨请求保留状态时,使用明确的服务端句柄或受保护的状态存储,并让句柄与主体、租户和过期策略绑定;只需要无状态调用时,则让每个请求携带足够的已验证上下文。

日志审计要记录什么,哪些内容不能记录

授权审计的目标是回答“谁在什么租户下,以什么权限调用了哪个工具,结果是什么”,而不是把完整请求和访问令牌复制到日志系统。建议至少记录:

  • request_id、时间、资源路径和工具名;
  • 经过验证的主体标识、租户标识、授权结果和拒绝原因;
  • 使用的 scope 名称集合、工具的数据影响级别和耗时区间;
  • 下游错误类别与重试次数,不记录原始 Bearer token、refresh token 或完整敏感参数。

对拒绝事件要区分 401、403 和业务数据层的拒绝:401 表示认证材料不可用,403 表示身份存在但权限不足,数据层拒绝则表示工具已经通过授权但业务规则不允许操作。这样既方便告警,也避免把权限配置问题误判成 MCP 协议故障。

发布前的配置核对清单

  1. 资源 URL、令牌的 aud 和服务端期望资源完全一致。
  2. 校验器检查签发者、签名、过期时间和必要的主体/租户声明。
  3. HTTP 认证中间件位于 /mcp handler 之前,缺失或无效令牌不会进入工具注册表。
  4. 工具策略按最小 scope 划分,读、写、管理能力没有共用一个宽泛 scope。
  5. 每次请求重新建立主体、租户、会话键和 scope 集合,进程级全局变量不保存当前调用方。
  6. 审计记录只保留必要的关联字段,令牌和敏感参数经过脱敏或完全不落盘。
  7. 负载均衡和多实例部署不会依赖某个实例的隐式连接状态;需要状态时使用显式、过期且绑定主体的服务端句柄。

完成这些核对后,MCP 服务端的授权范围和会话隔离就形成了可解释的链路:HTTP 入口负责确认身份,授权上下文负责确认归属,工具策略负责确认能力,审计记录负责保留证据。

常见问题

基础的 mcp scope 能否直接访问所有工具?

不建议。基础 scope 只适合表示进入 MCP 资源的资格,实际工具应继续要求更细的读、写或管理 scope,并在服务端统一映射。

为什么不能把租户 ID 放在工具参数里直接使用?

工具参数属于调用方输入,不能自动视为可信身份。租户归属应来自经过验证的令牌声明或可信上游上下文,再由服务端将其注入数据访问层。

协议层没有 session ID 后还需要做会话隔离吗?

需要。移除传输层 session 标识,只是减少对连接状态的依赖;业务侧仍然可能需要把请求和主体、租户、scope、审计关联键绑定起来,而且这种绑定应由服务端显式设计。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>