MCP 服务端授权范围与会话隔离的配置
来源:17golang原创
时间:2026-10-10 18:14:50 161浏览 收藏
MCP 服务端的授权范围与会话隔离,核心不是给每个工具再加一层模糊的“是否允许”判断,而是把边界提前固定在请求入口、身份上下文和工具策略上。远程 MCP 服务应先验证 Bearer 令牌,再把主体、租户和 scope 绑定到本次请求,最后由工具策略决定可见能力。
官方资料:https://modelcontextprotocol.io/
还要注意一个容易混淆的点:MCP 规范的新版本正在移除传输层的协议级 session 标识。本文所说的“会话隔离”,指业务侧的授权上下文隔离,例如主体、租户、scope 集合和审计关联键,而不是把权限寄托在一条长连接或某个进程内对象上。
先划清用户、会话与工具权限三层边界
可以把一次远程调用拆成三个相互关联但不应混为一谈的边界:
- 身份与令牌:令牌代表哪个主体,签发者是谁,资源受众是否匹配,以及令牌拥有哪些 scope。
- 请求与会话:本次请求属于哪个租户、哪个业务会话或哪个调用关联键;这些字段必须由受信来源构造,不能直接相信客户端随意提交的租户 ID。
- 工具授权面:scope 只表达能力范围,工具策略再把能力范围映射到只读、写入或管理工具。

这张结构图里,资源服务器是边界收口点,不能让只读工具自行解析原始令牌,也不能让写入工具自行决定租户。统一入口校验后,工具只消费已经整理过的授权上下文。
在 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 覆盖令牌中的租户声明,也不要在全局单例里缓存“当前用户”。

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 协议故障。
发布前的配置核对清单
- 资源 URL、令牌的
aud和服务端期望资源完全一致。 - 校验器检查签发者、签名、过期时间和必要的主体/租户声明。
- HTTP 认证中间件位于
/mcphandler 之前,缺失或无效令牌不会进入工具注册表。 - 工具策略按最小 scope 划分,读、写、管理能力没有共用一个宽泛 scope。
- 每次请求重新建立主体、租户、会话键和 scope 集合,进程级全局变量不保存当前调用方。
- 审计记录只保留必要的关联字段,令牌和敏感参数经过脱敏或完全不落盘。
- 负载均衡和多实例部署不会依赖某个实例的隐式连接状态;需要状态时使用显式、过期且绑定主体的服务端句柄。
完成这些核对后,MCP 服务端的授权范围和会话隔离就形成了可解释的链路:HTTP 入口负责确认身份,授权上下文负责确认归属,工具策略负责确认能力,审计记录负责保留证据。
常见问题
基础的 mcp scope 能否直接访问所有工具?
不建议。基础 scope 只适合表示进入 MCP 资源的资格,实际工具应继续要求更细的读、写或管理 scope,并在服务端统一映射。
为什么不能把租户 ID 放在工具参数里直接使用?
工具参数属于调用方输入,不能自动视为可信身份。租户归属应来自经过验证的令牌声明或可信上游上下文,再由服务端将其注入数据访问层。
协议层没有 session ID 后还需要做会话隔离吗?
需要。移除传输层 session 标识,只是减少对连接状态的依赖;业务侧仍然可能需要把请求和主体、租户、scope、审计关联键绑定起来,而且这种绑定应由服务端显式设计。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
486 收藏
-
154 收藏
-
373 收藏
-
300 收藏
-
356 收藏
-
449 收藏
-
科技周边 · 人工智能 | 1天前 | 缓存 · 人工智能 · 提示词工程 · 提示词缓存 cache_control Prompt Caching 静态前缀 cache_read_input_tokens453 收藏
-
232 收藏
-
215 收藏
-
236 收藏
-
196 收藏
-
404 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习