Skip to content
MCP 文档版本 2026-07-28 更新于 2026-10-14

理解 MCP 中的授权

了解如何使用 OAuth 2.1 为 MCP 服务端实现安全授权,以保护敏感资源与操作

Model Context Protocol (MCP) 中的授权用于保护对 MCP 服务端所暴露的敏感资源与操作的访问。如果你的 MCP 服务端处理用户数据或管理操作,授权可确保只有被允许的用户才能访问其端点。

MCP 使用标准化的授权流程,在 MCP 客户端(连接器)与 MCP 服务端之间建立信任。其设计并不聚焦于某一特定的授权或身份系统,而是遵循为 OAuth 2.1 所制定的约定。详细信息请参见授权规范。

何时应使用授权?

虽然 MCP 服务端的授权是可选的,但在以下情况下强烈建议使用:

  • 你的服务端访问用户特定数据(电子邮件、文档、数据库)
  • 你需要审计谁执行了哪些操作
  • 你的服务端授予对其需要用户同意的 API 的访问权限
  • 你正在为具有严格访问控制的企业环境进行构建
  • 你希望按用户实施速率限制或使用量跟踪

TIP

本地 MCP 服务端的授权

对于使用 STDIO 传输的 MCP 服务端,你可以改用基于环境的凭据,或由直接嵌入 MCP 服务端的第三方库提供的凭据。由于基于 STDIO 构建的 MCP 服务端在本地运行,在获取用户凭据方面它有一系列灵活的选择,这些选择可能依赖也可能不依赖浏览器内的认证与授权流程。

而 OAuth 流程则是为基于 HTTP 的传输而设计的,在这种场景下 MCP 服务端是远程托管的,客户端使用 OAuth 来确认某个用户已被授权访问该远程服务端。

授权流程:分步说明

让我们逐步了解当客户端想要连接到你所保护的 MCP 服务端时会发生什么:

初始握手

当你的 MCP 客户端首次尝试连接时,你的服务端会以 `401 Unauthorized` 响应,并告知客户端在哪里可以找到授权信息,这些信息记录在[受保护资源元数据 (PRM) 文档](https://datatracker.ietf.org/doc/html/rfc9728)中。该文档由 MCP 服务端托管,遵循可预测的路径模式,并通过 `WWW-Authenticate` 头中的 `resource_metadata` 参数提供给客户端。

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
  resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
```

这告诉客户端该 MCP 服务端需要授权,以及从哪里获取启动授权流程所需的信息。

受保护资源元数据发现

有了指向 PRM 文档的 URI 指针,客户端将获取该元数据,以了解授权服务器、支持的 scope 以及其他资源信息。这些数据通常封装在一个 JSON blob 中,类似于下面这个。

```json
{
  "resource": "https://your-server.com/mcp",
  "authorization_servers": ["https://auth.your-server.com"],
  "scopes_supported": ["mcp:tools", "mcp:resources"]
}
```

你可以在 [RFC 9728 第 3.2 节](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r)中看到一个更完整的示例。

授权服务器发现

接下来,客户端通过获取授权服务器的元数据来发现它能做什么。如果 PRM 文档列出了多个授权服务器,客户端可以决定使用哪一个。

选定授权服务器后,客户端将构造一个标准元数据 URI,并向 [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) 或 [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) 端点发起请求(取决于授权服务器的支持情况),并获取另一组元数据属性,从而了解完成授权流程所需的各个端点。

```json
{
  "issuer": "https://auth.your-server.com",
  "authorization_endpoint": "https://auth.your-server.com/authorize",
  "token_endpoint": "https://auth.your-server.com/token",
  "registration_endpoint": "https://auth.your-server.com/register"
}
```

客户端注册

所有元数据都处理完毕后,客户端现在需要确保自己已在授权服务器上注册。这可以通过两种方式完成。

首先,客户端可以**预先注册**到某个给定的授权服务器,在这种情况下,它可以内置客户端注册信息,用于完成授权流程。

或者,客户端可以使用**动态客户端注册** (DCR) 将自己动态注册到授权服务器。后一种场景要求授权服务器支持 DCR。如果授权服务器确实支持 DCR,客户端将向其 `registration_endpoint` 发送包含自身信息的请求:

```json
{
  "client_name": "My MCP Client",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}
```

如果注册成功,授权服务器将返回一个包含客户端注册信息的 JSON blob。

TIP

不使用 DCR 或预注册

如果 MCP 客户端连接的 MCP 服务端所使用的授权服务器不支持 DCR,且该客户端未在所述授权服务器上预注册,则客户端开发者有责任提供一种方式,让最终用户手动输入客户端信息。

用户授权

客户端现在需要打开浏览器并访问 `/authorize` 端点,用户可以在其中登录并授予所需的权限。随后,授权服务器将重定向回客户端,并附带一个授权码,客户端用它来换取令牌:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "def502...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

访问令牌是客户端用来向 MCP 服务端发送请求时进行身份验证的凭证。此步骤遵循标准的 [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) 约定。

发起已认证请求

最后,客户端可以使用嵌入在 `Authorization` 头中的访问令牌向你的 MCP 服务端发起请求:

```http
GET /mcp HTTP/1.1
Host: your-server.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
```

MCP 服务端需要验证该令牌,并在令牌有效且具有所需权限时处理该请求。

实现示例

为了开始一个实际的实现,我们将使用一个托管在 Docker 容器中的 Keycloak 授权服务器。Keycloak 是一个开源授权服务器,可以轻松地在本地部署以进行测试和实验。

请确保你下载并安装了 Docker Desktop。我们需要它来在开发机器上部署 Keycloak。

Keycloak 设置

在终端应用程序中,运行以下命令以启动 Keycloak 容器:

bash
docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak start-dev

此命令将在本地拉取 Keycloak 容器镜像并引导基本配置。它将在端口 8080 上运行,并拥有一个用户名为 admin、密码为 admin 的用户。

WARNING

不适用于生产环境

上述配置可能适用于测试和实验;但是,你绝不应在生产环境中使用它。有关在需要可靠性、安全性和高可用性的场景中如何部署授权服务器的更多详细信息,请参阅 Configuring Keycloak for production 指南。

你将能够通过浏览器在 http://localhost:8080 访问 Keycloak 授权服务器。

Keycloak admin dashboard authentication dialog.

在使用默认配置运行时,Keycloak 已经支持我们为 MCP 服务端所需的许多能力,包括动态客户端注册。你可以通过查看 OIDC 配置来验证这一点,该配置位于:

http
http://localhost:8080/realms/master/.well-known/openid-configuration

我们还需要设置 Keycloak 以支持我们的作用域,并允许我们的宿主(本地机器)动态注册客户端,因为默认策略限制了匿名动态客户端注册。

转到 Keycloak 仪表板中的 Client scopes,并创建一个新的 mcp:tools 作用域。我们将使用它来访问 MCP 服务端上的所有工具。

Configuring Keycloak scopes.

创建作用域后,请确保将其类型分配为 Default,并打开 Include in token scope 开关,因为令牌验证将需要它。

现在,让我们为 Keycloak 签发的令牌设置一个 audience。配置 audience 很重要,因为它将预期目标直接嵌入到签发的访问令牌中。这有助于你的 MCP 服务端验证它所收到的令牌确实是发给它的,而不是发给其他 API 的。这是帮助避免 Token 透传场景的关键。

为此,打开你的 mcp:tools 客户端作用域,点击 Mappers,然后点击 Configure a new mapper。选择 Audience。

Configuring an audience for a token in Keycloak.

对于 Name,使用 audience-config。为 Included Custom Audience 添加一个值,设置为 http://localhost:3000。这将是我们测试服务端的 URI。

WARNING

不适用于生产环境

上述 audience 配置仅用于测试。对于生产场景,将需要额外的设置和配置,以确保签发的令牌的 audience 受到适当约束。具体来说,audience 需要基于从客户端传递的 resource 参数,而不是固定值。

现在,导航到 Clients,然后 Client registration,再然后 Trusted Hosts。禁用 Client URIs Must Match 设置,并添加你正在测试的宿主。你可以通过在 Linux 或 macOS 上运行 ifconfig 命令,或在 Windows 上运行 ipconfig 命令来获取当前宿主 IP。你可以通过查看 keycloak 日志中类似 Failed to verify remote host : 192.168.215.1 的行来看到需要添加的 IP 地址。检查该 IP 地址是否与你的宿主关联。根据你的 docker 设置,这可能是一个桥接网络。

Setting up client registration details in Keycloak.

WARNING

获取 Host

如果你从容器中运行 Keycloak,你也可以从容器日志的终端中看到 host IP。

最后,我们需要注册一个新客户端,用于与 MCP 服务端本身 配合,以便与 Keycloak 通信,例如进行 token introspection。为此:

  1. 前往 Clients。
  2. 点击 Create client。
  3. 为你的客户端指定一个唯一的 Client ID,然后点击 Next。
  4. 启用 Client authentication,然后点击 Next。
  5. 点击 Save。

值得注意的是,token introspection 只是验证 token 的可用方法之一。这也可以借助特定于每种语言和平台的独立库来完成。

当你打开客户端详情时,前往 Credentials 并记下 Client Secret。

Creating a new client in Keycloak.

WARNING

处理密钥

切勿将客户端凭据直接嵌入到你的代码中。我们建议使用环境变量或专门的密钥存储解决方案。

配置好 Keycloak 后,每次触发授权流程时,你的 MCP 服务端都会收到一个类似这样的 token:

text
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI1TjcxMGw1WW5MWk13WGZ1VlJKWGtCS3ZZMzZzb3JnRG5scmlyZ2tlTHlzIn0.eyJleHAiOjE3NTU1NDA4MTcsImlhdCI6MTc1NTU0MDc1NywiYXV0aF90aW1lIjoxNzU1NTM4ODg4LCJqdGkiOiJvbnJ0YWM6YjM0MDgwZmYtODQwNC02ODY3LTgxYmUtMTIzMWI1MDU5M2E4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJzdWIiOiIzM2VkNmM2Yi1jNmUwLTQ5MjgtYTE2MS1mMmY2OWM3YTAzYjkiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiI3OTc1YTViNi04YjU5LTRhODUtOWNiYS04ZmFlYmRhYjg5NzQiLCJzaWQiOiI4ZjdlYzI3Ni0zNThmLTRjY2MtYjMxMy1kYjA4MjkwZjM3NmYiLCJzY29wZSI6Im1jcDp0b29scyJ9.P5xCRtXORly0R0EXjyqRCUx-z3J4uAOWNAvYtLPXroykZuVCCJ-K1haiQSwbURqfsVOMbL7jiV-sD6miuPzI1tmKOkN_Yct0Vp-azvj7U5rEj7U6tvPfMkg2Uj_jrIX0KOskyU2pVvGZ-5BgqaSvwTEdsGu_V3_E0xDuSBq2uj_wmhqiyTFm5lJ1WkM3Hnxxx1_AAnTj7iOKMFZ4VCwMmk8hhSC7clnDauORc0sutxiJuYUZzxNiNPkmNeQtMCGqWdP1igcbWbrfnNXhJ6NswBOuRbh97_QraET3hl-CNmyS6C72Xc0aOwR_uJ7xVSBTD02OaQ1JA6kjCATz30kGYg

解码后,它看起来会是这样:

json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "5N710l5YnLZMwXfuVRJXkBKvY36sorgDnlrirgkeLys"
}.{
  "exp": 1755540817,
  "iat": 1755540757,
  "auth_time": 1755538888,
  "jti": "onrtac:b34080ff-8404-6867-81be-1231b50593a8",
  "iss": "http://localhost:8080/realms/master",
  "aud": "http://localhost:3000",
  "sub": "33ed6c6b-c6e0-4928-a161-f2f69c7a03b9",
  "typ": "Bearer",
  "azp": "7975a5b6-8b59-4a85-9cba-8faebdab8974",
  "sid": "8f7ec276-358f-4ccc-b313-db08290f376f",
  "scope": "mcp:tools"
}.[Signature]

WARNING

内嵌的 Audience

注意 token 中内嵌的 aud 声明——它当前被设置为测试 MCP 服务端的 URI,并且是从我们之前配置的 scope 推断出来的。这在我们的实现中对于验证非常重要。

MCP 服务端设置

我们现在将设置 MCP 服务端,使其使用本地运行的 Keycloak 授权服务器。根据你的编程语言偏好,你可以使用受支持的 MCP SDKs 之一。

出于测试目的,我们将创建一个极其简单的 MCP 服务端,它暴露两个工具——一个用于加法,另一个用于乘法。该服务端将要求授权才能访问这些工具。

你可以在 [sample repository](https://github.com/localden/min-ts-mcp-auth) 中查看完整的 TypeScript 项目。

在运行下面的代码之前,请确保你有一个包含以下内容的 `.env` 文件:

```env
# Server host/port
HOST=localhost
PORT=3000

# Auth server location
AUTH_HOST=localhost
AUTH_PORT=8080
AUTH_REALM=master

# Keycloak OAuth client credentials
OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
```

`OAUTH_CLIENT_ID` 和 `OAUTH_CLIENT_SECRET` 与我们之前创建的 MCP 服务端客户端相关联。

除了实现 MCP 授权规范外,下面的服务端还通过 Keycloak 进行 token introspection,以确保它从客户端收到的 token 是有效的。它还实现了基本的日志记录,以便你轻松诊断任何问题。

```typescript
import "dotenv/config";
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import cors from "cors";
import {
  mcpAuthMetadataRouter,
  getOAuthProtectedResourceMetadataUrl,
} from "@modelcontextprotocol/sdk/server/auth/router.js";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
const CONFIG = {
  host: process.env.HOST || "localhost",
  port: Number(process.env.PORT) || 3000,
  auth: {
    host: process.env.AUTH_HOST || process.env.HOST || "localhost",
    port: Number(process.env.AUTH_PORT) || 8080,
    realm: process.env.AUTH_REALM || "master",
    clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
    clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
  },
};

function createOAuthUrls() {
  const authBaseUrl = new URL(
    `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
  );
  return {
    issuer: authBaseUrl.toString(),
    introspection_endpoint: new URL(
      "protocol/openid-connect/token/introspect",
      authBaseUrl,
    ).toString(),
    authorization_endpoint: new URL(
      "protocol/openid-connect/auth",
      authBaseUrl,
    ).toString(),
    token_endpoint: new URL(
      "protocol/openid-connect/token",
      authBaseUrl,
    ).toString(),
  };
}

function createRequestLogger() {
  return (req: any, res: any, next: any) => {
    const start = Date.now();
    res.on("finish", () => {
      const ms = Date.now() - start;
      console.log(
        `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
      );
    });
    next();
  };
}

const app = express();

app.use(
  express.json({
    verify: (req: any, _res, buf) => {
      req.rawBody = buf?.toString() ?? "";
    },
  }),
);

app.use(
  cors({
    origin: "*",
    exposedHeaders: ["Mcp-Session-Id"],
  }),
);

app.use(createRequestLogger());

const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
const oauthUrls = createOAuthUrls();

const oauthMetadata: OAuthMetadata = {
  ...oauthUrls,
  response_types_supported: ["code"],
};

const tokenVerifier = {
  verifyAccessToken: async (token: string) => {
    const endpoint = oauthMetadata.introspection_endpoint;

    if (!endpoint) {
      console.error("[auth] no introspection endpoint in metadata");
      throw new Error("No token verification endpoint available in metadata");
    }

    const params = new URLSearchParams({
      token: token,
      client_id: CONFIG.auth.clientId,
    });

    if (CONFIG.auth.clientSecret) {
      params.set("client_secret", CONFIG.auth.clientSecret);
    }

    let response: Response;
    try {
      response = await fetch(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: params.toString(),
      });
    } catch (e) {
      console.error("[auth] introspection fetch threw", e);
      throw e;
    }

    if (!response.ok) {
      const txt = await response.text();
      console.error("[auth] introspection non-OK", { status: response.status });

      try {
        const obj = JSON.parse(txt);
        console.log(JSON.stringify(obj, null, 2));
      } catch {
        console.error(txt);
      }
      throw new Error(`Invalid or expired token: ${txt}`);
    }

    let data: any;
    try {
      data = await response.json();
    } catch (e) {
      const txt = await response.text();
      console.error("[auth] failed to parse introspection JSON", {
        error: String(e),
        body: txt,
      });
      throw e;
    }

    if (data.active === false) {
      throw new Error("Inactive token");
    }

    if (!data.aud) {
      throw new Error("Resource indicator (aud) missing");
    }

    const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
    const allowed = audiences.some((a) => {
      try {
        return checkResourceAllowed({
          requestedResource: a,
          configuredResource: mcpServerUrl,
        });
      } catch {
        // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
        // Those are never our resource, so treat them as "no match" instead of crashing.
        return false;
      }
    });
    if (!allowed) {
      throw new Error(
        `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
      );
    }

    return {
      token,
      clientId: data.client_id,
      scopes: data.scope ? data.scope.split(" ") : [],
      expiresAt: data.exp,
    };
  },
};
app.use(
  mcpAuthMetadataRouter({
    oauthMetadata,
    resourceServerUrl: mcpServerUrl,
    scopesSupported: ["mcp:tools"],
    resourceName: "MCP Demo Server",
  }),
);

const authMiddleware = requireBearerAuth({
  verifier: tokenVerifier,
  requiredScopes: [],
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
});

const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

function createMcpServer() {
  const server = new McpServer({
    name: "example-server",
    version: "1.0.0",
  });

  server.registerTool(
    "add",
    {
      title: "Addition Tool",
      description: "Add two numbers together",
      inputSchema: {
        a: z.number().describe("First number to add"),
        b: z.number().describe("Second number to add"),
      },
    },
    async ({ a, b }) => ({
      content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
    }),
  );

  server.registerTool(
    "multiply",
    {
      title: "Multiplication Tool",
      description: "Multiply two numbers together",
      inputSchema: {
        x: z.number().describe("First number to multiply"),
        y: z.number().describe("Second number to multiply"),
      },
    },
    async ({ x, y }) => ({
      content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
    }),
  );

  return server;
}

const mcpPostHandler = async (req: express.Request, res: express.Response) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  let transport: StreamableHTTPServerTransport;

  if (sessionId && transports[sessionId]) {
    transport = transports[sessionId];
  } else if (!sessionId && isInitializeRequest(req.body)) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (sessionId) => {
        transports[sessionId] = transport;
      },
    });

    transport.onclose = () => {
      if (transport.sessionId) {
        delete transports[transport.sessionId];
      }
    };

    const server = createMcpServer();
    await server.connect(transport);
  } else {
    res.status(400).json({
      jsonrpc: "2.0",
      error: {
        code: -32000,
        message: "Bad Request: No valid session ID provided",
      },
      id: null,
    });
    return;
  }

  await transport.handleRequest(req, res, req.body);
};

const handleSessionRequest = async (
  req: express.Request,
  res: express.Response,
) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  if (!sessionId || !transports[sessionId]) {
    res.status(400).send("Invalid or missing session ID");
    return;
  }

  const transport = transports[sessionId];
  await transport.handleRequest(req, res);
};

app.post("/", authMiddleware, mcpPostHandler);
app.get("/", authMiddleware, handleSessionRequest);
app.delete("/", authMiddleware, handleSessionRequest);

app.listen(CONFIG.port, CONFIG.host, () => {
  console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
  console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
  console.log(
    `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
  );
});
```

运行服务端后,你可以通过提供 MCP 服务端端点,将其添加到你的 MCP 客户端,例如 Visual Studio Code。

有关在 TypeScript 中实现 MCP 服务端的更多细节,请参阅 TypeScript SDK 文档。

测试 MCP 服务端

出于测试目的,我们将使用 Visual Studio Code,但任何支持 MCP 以及新授权规范的客户端都可以。

按 Cmd + Shift + P,然后选择 MCP: Add server...。选择 HTTP 并输入 http://localhost:3000。为该服务端指定一个在 Visual Studio Code 内使用的唯一名称。此时在 mcp.json 中你应该会看到类似这样的条目:

json
"my-mcp-server-18676652": {
  "url": "http://localhost:3000",
  "type": "http"
}

连接时,你将被带到浏览器,在那里系统会提示你同意 Visual Studio Code 访问 mcp:tools 作用域。

Keycloak consent form for VS Code.

同意后,你会在 mcp.json 中服务端条目的正上方看到列出的工具。

Tools listed in VS Code.

你可以借助聊天视图中的 # 符号来调用各个工具。

Invoking MCP tools in VS Code.

常见陷阱及如何避免

有关全面的安全指导,包括攻击向量、缓解策略和实现最佳实践,请务必阅读安全最佳实践。下面列出了几个关键问题。

  • 不要自行实现 Token 校验或授权逻辑。对于 Token 校验或授权决策之类的事情,请使用现成的、经过充分测试且安全的库。除非你是安全专家,否则一切从零实现意味着你更有可能实现错误。
  • 使用短期访问令牌。根据所使用的授权服务器,此设置可能是可自定义的。我们建议不要使用长期令牌——如果被恶意行为者窃取,他们将能够在更长时间内保持其访问权限。
  • 始终校验令牌。你的服务端收到令牌并不意味着该令牌有效,也不意味着它是为你的服务端准备的。始终验证你的 MCP 服务端从客户端获取的内容符合所需约束。
  • 将令牌存储在安全、加密的存储中。在某些场景下,你可能需要在服务端缓存令牌。如果是这种情况,请确保该存储具有正确的访问控制,并且无法被有权访问你服务端的恶意方轻易窃取。你还应实现健壮的缓存淘汰策略,以确保你的 MCP 服务端不会重复使用已过期或其他无效的令牌。
  • 在生产环境中强制使用 HTTPS。除开发期间的 localhost 外,不要通过纯 HTTP 接受令牌或重定向回调。
  • 最小权限作用域。不要使用包罗万象的作用域。尽可能按工具或能力拆分访问权限,并在资源服务器上按路由/工具验证所需作用域。
  • 不要记录凭据。绝不要记录 Authorization 头、令牌、授权码或密钥。清理查询字符串和头。在结构化日志中脱敏敏感字段。
  • 区分应用与资源服务器凭据。不要将 MCP 服务端的客户端密钥复用于最终用户流程。将所有密钥存储在适当的密钥管理器中,而不是源代码管理中。
  • 返回恰当的质询。在 401 时,包含带有 Bearer、realm 和 resource_metadata 的 WWW-Authenticate,以便客户端能够发现如何进行身份验证。
  • DCR(动态客户端注册)控制。如果启用,请注意你所在组织的特定约束,例如受信任主机、必需的审查和经过审计的注册。未认证的 DCR 意味着任何人都可以在你的授权服务器上注册任何客户端。
  • 多租户/领域混淆。除非明确为多租户,否则固定到单个签发者/租户。即使令牌由同一授权服务器签名,也要拒绝来自其他领域的令牌。
  • 受众/资源指示符误用。不要配置或接受通用受众(如 api)或不相关的资源。要求受众/资源与你配置的服务端匹配。
  • 错误细节泄露。向客户端返回通用消息,但在内部记录带有关联 ID 的详细原因,以帮助排查问题而不暴露内部信息。
  • 会话标识符加固。将 Mcp-Session-Id 视为不可信输入;绝不要将授权绑定到它。在授权变更时重新生成,并在服务端验证其生命周期。

相关标准与文档

MCP 授权建立在这些成熟标准之上:

有关更多详细信息,请参阅:

理解这些标准将帮助你正确实现授权,并在出现问题时进行排查。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。