企业将 AI Agent 接入生产系统时,认证授权是绕不过去的第一关。MCP Remote Server 通过 OAuth 2.1 协议为 AI Agent 提供标准化的工具调用认证机制,但从"能跑通 Demo"到"满足企业安全审计"之间隔着大量工程细节。本文将以端到端实战的方式,带你完成一个生产就绪的 MCP Remote Server OAuth 集成。
如果你还不熟悉 MCP 协议基础概念,建议先阅读 MCP 协议完整指南。
核心要点
- 认证服务器与资源服务器分离:MCP Server 作为 Resource Server 只验证 Token,不颁发 Token,职责边界清晰
- PKCE + S256 强制要求:所有公开客户端必须使用 Proof Key for Code Exchange,杜绝授权码拦截攻击
- JWKS 缓存策略决定可用性:错误的缓存配置会导致密钥轮转期间大面积 401 错误
- On-Behalf-Of 实现委托访问:Agent 以用户身份访问下游服务,权限不提升、审计可追溯
- RFC 9728 元数据发现:客户端通过
/.well-known/oauth-protected-resource自动获取认证参数 - mTLS 双向认证:在零信任网络中为服务间通信提供传输层安全保障
企业级认证为什么不能用简单方案
API Key 和 Basic Auth 在企业环境中存在根本性缺陷。当你的 MCP Server 暴露给数十个 Agent 客户端、服务数百名企业用户时,静态凭证的管理成本和安全风险呈指数增长。
攻击面分析
| 威胁类型 | API Key 方案 | OAuth 2.1 方案 |
|---|---|---|
| 凭证泄露 | 永久有效,需手动吊销 | 15 分钟自动过期,refresh_token 一次性使用 |
| 权限过宽 | 全局权限,无法细分 | scope 机制实现最小权限原则 |
| 中间人攻击 | 依赖传输层加密 | PKCE + state 参数双重防护 |
| 审计追溯 | 仅能追溯到 Key 所有者 | Token 包含用户身份(sub claim) |
| 横向移动 | 一个 Key 泄露影响所有资源 | scope 隔离,单点泄露影响有限 |
| 合规要求 | 不满足 SOC2/ISO27001 | 完全满足,提供完整审计链 |
合规驱动
企业部署 MCP Server 时需要满足:
- SOC 2 Type II:要求访问控制策略的强制执行和持续审计
- ISO 27001:要求身份认证的多因素支持和定期凭证轮转
- GDPR:要求对用户数据访问的最小权限和完整日志
OAuth 2.1 天然满足这些要求,而 API Key 方案需要大量额外建设才能达到同等合规水平。
架构设计:认证服务器与资源服务器分离
生产环境中的 MCP 认证架构遵循 OAuth 2.0 的经典分层——Authorization Server 负责颁发和管理 Token,MCP Server 作为 Resource Server 仅负责验证 Token 并执行业务逻辑。
这种分离架构的核心优势在于:MCP Server 无需管理用户密码、Session 状态或 Token 颁发逻辑,专注于工具能力的实现。当需要更换 IdP 时(例如从 Okta 迁移到 Azure AD),只需修改 Token 验证配置,业务逻辑完全不受影响。
OAuth 2.1 + PKCE 完整集成流程
OAuth 2.1 在 MCP 场景中的完整认证流程涉及客户端、授权服务器和 MCP Server 三方交互。以下时序图展示了从服务发现到 Token 刷新的端到端流程。
TypeScript 实现:MCP Server 认证中间件
以下是一个基于 Express.js 的生产级 Token 验证中间件实现,使用 OAuth Token 验证和 JWKS 自动发现:
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { Request, Response, NextFunction } from 'express';
import { LRUCache } from 'lru-cache';
// JWKS 缓存配置:5分钟 TTL,防止频繁请求 IdP
const jwksCache = new LRUCache<string, ReturnType<typeof createRemoteJWKSet>>({
max: 10,
ttl: 5 * 60 * 1000, // 5 minutes
});
interface MCPAuthConfig {
issuer: string;
audience: string;
jwksUri: string;
requiredScopes: string[];
maxTokenAge: string;
}
interface AuthenticatedRequest extends Request {
auth?: {
sub: string;
scopes: string[];
clientId: string;
payload: JWTPayload;
};
}
// JWKS 获取,带 miss-then-refetch 策略
function getJWKS(jwksUri: string, forceRefresh = false) {
const cacheKey = jwksUri;
if (!forceRefresh && jwksCache.has(cacheKey)) {
return jwksCache.get(cacheKey)!;
}
const jwks = createRemoteJWKSet(new URL(jwksUri));
jwksCache.set(cacheKey, jwks);
return jwks;
}
export function createMCPAuthMiddleware(config: MCPAuthConfig) {
// 刷新频率限制:每分钟最多 1 次强制刷新
let lastForceRefresh = 0;
const REFRESH_COOLDOWN = 60_000;
return async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: 'missing_token',
error_description: 'Authorization header with Bearer token required',
});
}
const token = authHeader.slice(7);
try {
let jwks = getJWKS(config.jwksUri);
let result;
try {
result = await jwtVerify(token, jwks, {
issuer: config.issuer,
audience: config.audience,
maxTokenAge: config.maxTokenAge,
});
} catch (err: any) {
// kid 未匹配时尝试刷新 JWKS(密钥轮转场景)
if (err.code === 'ERR_JWKS_NO_MATCHING_KEY') {
const now = Date.now();
if (now - lastForceRefresh > REFRESH_COOLDOWN) {
lastForceRefresh = now;
jwks = getJWKS(config.jwksUri, true);
result = await jwtVerify(token, jwks, {
issuer: config.issuer,
audience: config.audience,
maxTokenAge: config.maxTokenAge,
});
} else {
throw err; // 冷却期内不重试
}
} else {
throw err;
}
}
// Scope 校验:每个请求都必须校验,不只是登录时
const tokenScopes = (result.payload.scope as string || '').split(' ');
const hasRequiredScopes = config.requiredScopes.every(
(s) => tokenScopes.includes(s)
);
if (!hasRequiredScopes) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${config.requiredScopes.join(', ')}`,
});
}
req.auth = {
sub: result.payload.sub!,
scopes: tokenScopes,
clientId: result.payload.azp as string || result.payload.client_id as string || '',
payload: result.payload,
};
next();
} catch (error: any) {
return res.status(401).json({
error: 'invalid_token',
error_description: error.message,
});
}
};
}
// 使用示例
const mcpAuth = createMCPAuthMiddleware({
issuer: 'https://login.microsoftonline.com/{tenant-id}/v2.0',
audience: 'api://mcp-server-production',
jwksUri: 'https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys',
requiredScopes: ['mcp.tools.execute', 'mcp.resources.read'],
maxTokenAge: '15m',
});
这段代码的关键设计决策:使用 LRU 缓存存储 JWKS,设置 5 分钟 TTL;当遇到未知 kid 时触发强制刷新但有冷却期保护;每个请求都执行 scope 校验而非仅在登录时检查一次。你可以使用 JWT Generator 工具快速生成测试用的 Token 来验证中间件行为。
IdP 集成模式对比
选择合适的身份提供商(IdP)是企业集成的核心决策。以下对比基于实际接入经验:
| 维度 | Azure AD (Entra ID) | Okta | Auth0 |
|---|---|---|---|
| 最佳场景 | Microsoft 生态企业 | 多云/混合环境 | 快速集成/初创团队 |
| OBO 流程 | 原生支持,配置简单 | 需自定义 Token Exchange | 通过 Actions 实现 |
| 动态注册 | 支持(需管理员预批准) | 支持 | 支持(最灵活) |
| 条件访问 | 内置,策略丰富 | 需 Adaptive MFA 许可 | 需 Enterprise Plan |
| JWKS 轮转 | 约每 6 周自动轮转 | 约每 90 天轮转 | 可手动触发 |
| MCP scope 定义 | App Registration 中定义 | Authorization Server 配置 | API Settings 中定义 |
| 定价模型 | 按 MAU(P1/P2 许可) | 按用户数 | 按 MAU + 功能层级 |
| SDK 质量 | MSAL(优秀) | okta-auth-js(良好) | auth0-spa-js(优秀) |
| RFC 9728 支持 | 完整支持 | 部分支持 | 通过自定义域名支持 |
Azure AD 配置关键步骤
// Azure AD App Registration 配置
const azureADConfig = {
// App Registration 中定义 MCP API 权限
api: {
oauth2PermissionScopes: [
{
id: 'generated-uuid-1',
value: 'mcp.tools.execute',
type: 'User',
adminConsentDescription: 'Execute MCP tools on behalf of the user',
adminConsentDisplayName: 'Execute MCP Tools',
},
{
id: 'generated-uuid-2',
value: 'mcp.resources.read',
type: 'User',
adminConsentDescription: 'Read MCP resources on behalf of the user',
adminConsentDisplayName: 'Read MCP Resources',
},
],
},
// Federated Credential 配置(用于 MCP Client 注册)
federatedCredentials: {
name: 'mcp-client-federation',
issuer: 'https://mcp-client.example.com',
subject: 'mcp-client-service-principal',
audiences: ['api://mcp-server-production'],
},
};
Token 验证与 JWKS 中间件:Python 实现
对于使用 Python 构建 MCP Server 的团队(例如基于 FastAPI),以下是等价的 Token 验证实现:
import time
from functools import lru_cache
from typing import Optional
import httpx
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import jwt, JWTError, ExpiredSignatureError
from pydantic import BaseModel
security = HTTPBearer()
class JWKSManager:
"""JWKS 管理器:带缓存和自动刷新"""
def __init__(self, jwks_uri: str, cache_ttl: int = 300):
self.jwks_uri = jwks_uri
self.cache_ttl = cache_ttl
self._keys: dict = {}
self._last_fetch: float = 0
self._refresh_cooldown: float = 60 # 最小刷新间隔
self._last_forced_refresh: float = 0
async def get_signing_keys(self, force_refresh: bool = False) -> dict:
now = time.time()
if force_refresh:
if now - self._last_forced_refresh < self._refresh_cooldown:
return self._keys # 冷却期内返回缓存
self._last_forced_refresh = now
if not force_refresh and self._keys and (now - self._last_fetch < self.cache_ttl):
return self._keys
async with httpx.AsyncClient() as client:
resp = await client.get(self.jwks_uri, timeout=10)
resp.raise_for_status()
jwks_data = resp.json()
self._keys = {
key["kid"]: key for key in jwks_data.get("keys", [])
}
self._last_fetch = now
return self._keys
async def get_key_by_kid(self, kid: str) -> Optional[dict]:
keys = await self.get_signing_keys()
if kid not in keys:
# kid 未命中,尝试强制刷新(密钥轮转场景)
keys = await self.get_signing_keys(force_refresh=True)
return keys.get(kid)
class MCPTokenPayload(BaseModel):
sub: str
scopes: list[str]
client_id: str
exp: int
iss: str
aud: str
class MCPAuthVerifier:
"""MCP Server Token 验证器"""
def __init__(
self,
issuer: str,
audience: str,
jwks_uri: str,
required_scopes: list[str],
):
self.issuer = issuer
self.audience = audience
self.required_scopes = required_scopes
self.jwks_manager = JWKSManager(jwks_uri)
async def verify(
self, credentials: HTTPAuthorizationCredentials = Security(security)
) -> MCPTokenPayload:
token = credentials.credentials
# 解码 Header 获取 kid
try:
unverified_header = jwt.get_unverified_header(token)
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token format")
kid = unverified_header.get("kid")
if not kid:
raise HTTPException(status_code=401, detail="Token missing kid header")
# 获取对应的签名密钥
key = await self.jwks_manager.get_key_by_kid(kid)
if not key:
raise HTTPException(status_code=401, detail="Unknown signing key")
# 验证 Token
try:
payload = jwt.decode(
token,
key,
algorithms=["RS256"],
audience=self.audience,
issuer=self.issuer,
options={"verify_exp": True, "leeway": 30},
)
except ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except JWTError as e:
raise HTTPException(status_code=401, detail=f"Token validation failed: {e}")
# Scope 校验
token_scopes = payload.get("scope", "").split()
if not all(s in token_scopes for s in self.required_scopes):
raise HTTPException(
status_code=403,
detail=f"Insufficient scope. Required: {self.required_scopes}",
)
return MCPTokenPayload(
sub=payload["sub"],
scopes=token_scopes,
client_id=payload.get("azp", payload.get("client_id", "")),
exp=payload["exp"],
iss=payload["iss"],
aud=payload["aud"],
)
# FastAPI 集成示例
mcp_auth = MCPAuthVerifier(
issuer="https://login.microsoftonline.com/{tenant-id}/v2.0",
audience="api://mcp-server-production",
jwks_uri="https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys",
required_scopes=["mcp.tools.execute"],
)
这个 Python 实现与 TypeScript 版本遵循相同的设计原则:缓存 JWKS 避免每次请求都访问 IdP、miss-then-refetch 策略处理密钥轮转、每次请求都执行 scope 校验。
On-Behalf-Of 委托访问模式
OBO(On-Behalf-Of)流程解决了一个核心问题:当用户通过 AI Agent 调用 MCP Tool,而该 Tool 需要访问用户在下游服务中的资源时,如何在不提升权限的前提下完成委托访问。
典型场景:用户让 Agent "查询我本月的 Azure 费用"。Agent 调用 MCP 的 query_billing Tool,该 Tool 需要以用户身份访问 Azure Cost Management API。
import { ConfidentialClientApplication } from '@azure/msal-node';
interface OBOConfig {
clientId: string;
clientSecret: string;
tenantId: string;
downstreamScopes: string[];
}
class MCPOnBehalfOfHandler {
private msalClient: ConfidentialClientApplication;
private config: OBOConfig;
constructor(config: OBOConfig) {
this.config = config;
this.msalClient = new ConfidentialClientApplication({
auth: {
clientId: config.clientId,
clientSecret: config.clientSecret,
authority: `https://login.microsoftonline.com/${config.tenantId}`,
},
});
}
/**
* 使用用户的 access_token 交换下游服务的委托 token
* 权限边界:下游 token 的权限 <= 用户原始 token 的权限
*/
async acquireTokenOnBehalfOf(userAccessToken: string): Promise<string> {
const result = await this.msalClient.acquireTokenOnBehalfOf({
oboAssertion: userAccessToken,
scopes: this.config.downstreamScopes,
});
if (!result?.accessToken) {
throw new Error('OBO token acquisition failed - user may lack consent');
}
return result.accessToken;
}
/**
* 在 MCP Tool 执行中使用 OBO
*/
async executeWithDelegation(
userToken: string,
toolFn: (delegatedToken: string) => Promise<any>
) {
const delegatedToken = await this.acquireTokenOnBehalfOf(userToken);
return toolFn(delegatedToken);
}
}
// MCP Tool 注册中使用 OBO
const oboHandler = new MCPOnBehalfOfHandler({
clientId: process.env.MCP_CLIENT_ID!,
clientSecret: process.env.MCP_CLIENT_SECRET!,
tenantId: process.env.AZURE_TENANT_ID!,
downstreamScopes: ['https://management.azure.com/.default'],
});
// Tool 实现
async function queryBillingTool(params: any, context: { userToken: string }) {
return oboHandler.executeWithDelegation(context.userToken, async (token) => {
const response = await fetch(
'https://management.azure.com/subscriptions/{sub-id}/providers/Microsoft.CostManagement/query?api-version=2023-11-01',
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params.query),
}
);
return response.json();
});
}
OBO 的关键安全特性:下游 Token 的权限不会超过用户原始 Token 的权限范围。即使 MCP Server 拥有 admin 权限的 Service Principal,通过 OBO 获取的 Token 也只能执行用户被授权的操作。
安全加固清单
将 MCP Server 部署到生产环境时,以下安全配置缺一不可:
mTLS 双向认证
在零信任网络架构中,仅凭 OAuth Token 不够——传输层也需要身份验证:
# Nginx 配置:mTLS for MCP Server
server {
listen 443 ssl;
server_name mcp.enterprise.com;
# 服务端证书
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# 客户端证书验证
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
# TLS 1.3 强制
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
# 传递客户端证书信息到上游
location /mcp {
proxy_pass http://mcp-server:3000;
proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;
proxy_set_header X-Client-Cert-Fingerprint $ssl_client_fingerprint;
}
}
完整安全加固矩阵
| 层级 | 措施 | 实现方式 | 优先级 |
|---|---|---|---|
| 传输层 | TLS 1.3 Only | Nginx/Envoy 配置 | P0 |
| 传输层 | mTLS 客户端认证 | 证书管理 + CRL/OCSP | P1 |
| 认证层 | PKCE (S256) | 所有公开客户端强制 | P0 |
| 认证层 | Token 15min 过期 | Authorization Server 配置 | P0 |
| 认证层 | Refresh Token 一次性 | Token Rotation 策略 | P0 |
| 授权层 | 请求级 Scope 校验 | 中间件每次请求验证 | P0 |
| 授权层 | Resource Indicators | RFC 8707 资源标识 | P1 |
| 网络层 | CORS 严格白名单 | 仅允许已注册的 Client Origin | P0 |
| 网络层 | 速率限制 | Token 端点: 10 req/min/IP | P0 |
| 应用层 | RFC 9728 元数据 | /.well-known 端点暴露 | P1 |
| 审计层 | 结构化日志 | 每次 Tool 调用记录 sub + scope + action | P0 |
RFC 9728:OAuth Protected Resource Metadata
让 MCP Client 能够自动发现认证要求,而非硬编码配置:
// MCP Server 暴露的 /.well-known/oauth-protected-resource 端点
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
resource: 'https://mcp.enterprise.com',
authorization_servers: [
'https://login.microsoftonline.com/{tenant-id}/v2.0'
],
scopes_supported: [
'mcp.tools.execute',
'mcp.resources.read',
'mcp.prompts.use',
],
bearer_methods_supported: ['header'],
resource_documentation: 'https://docs.enterprise.com/mcp-api',
});
});
生产部署配置
以下是完整的 Docker Compose 部署配置,包含 MCP Server、反向代理和监控:
version: "3.9"
services:
mcp-server:
image: enterprise/mcp-server:latest
environment:
- NODE_ENV=production
- MCP_AUTH_ISSUER=https://login.microsoftonline.com/{tenant-id}/v2.0
- MCP_AUTH_AUDIENCE=api://mcp-server-production
- MCP_AUTH_JWKS_URI=https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys
- MCP_REQUIRED_SCOPES=mcp.tools.execute,mcp.resources.read
- MCP_TOKEN_MAX_AGE=15m
- MCP_RATE_LIMIT_WINDOW=60000
- MCP_RATE_LIMIT_MAX=100
- LOG_LEVEL=info
- LOG_FORMAT=json
ports:
- "3000:3000"
deploy:
replicas: 3
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
networks:
- mcp-internal
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
ports:
- "443:443"
depends_on:
- mcp-server
networks:
- mcp-internal
- mcp-external
# 结构化日志收集
fluent-bit:
image: fluent/fluent-bit:latest
volumes:
- ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
depends_on:
- mcp-server
networks:
- mcp-internal
networks:
mcp-internal:
driver: bridge
internal: true
mcp-external:
driver: bridge
部署时的关键注意事项:MCP Server 运行在内部网络中,仅通过 Nginx 反向代理暴露 443 端口;所有敏感配置通过环境变量注入,不写入镜像;日志以 JSON 格式输出便于结构化分析。
常见踩坑与解决方案
以下是实际企业集成中频繁遇到的问题及其根因分析:
| 问题现象 | 根因分析 | 解决方案 |
|---|---|---|
| 密钥轮转后大面积 401 | JWKS 缓存 TTL 过长(>1h) | 设置 5min TTL + kid miss 时强制刷新 |
| Token 刷新后旧 Token 仍可用 | 未启用 Refresh Token Rotation | IdP 配置中启用 one-time refresh token |
| CORS 错误导致 preflight 失败 | OPTIONS 请求未正确处理 | Nginx 层添加 OPTIONS 响应,不转发到后端 |
| OBO 获取 Token 报 AADSTS50013 | Client assertion 与 reply URL 不匹配 | 确认 Federated Credential 的 audience 配置 |
| 动态注册返回 invalid_client_metadata | redirect_uri 包含非 HTTPS 地址 | 生产环境强制 HTTPS,仅本地开发允许 localhost |
| Token 包含的 scope 少于请求的 | 管理员未对 scope 进行 admin consent | Azure AD 中执行 Grant admin consent 操作 |
| JWKS 端点响应慢导致超时 | 网络层未配置 DNS 缓存 | 添加本地 DNS 缓存 + JWKS 预热机制 |
| 多租户场景 issuer 验证失败 | 使用了 common 端点但验证单租户 issuer | 配置 issuer 为 common 或使用 tenant-specific 端点 |
要调试 Token 内容和结构,可以使用 JSON Formatter 格式化 JWT 的 payload 部分,快速定位 claim 缺失问题。
从规范解读到生产落地的距离
如果你已经阅读过 MCP 2025 规范中的 OAuth 章节解读,会发现规范层面的定义相对简洁——但从规范到企业生产之间的鸿沟往往被低估。规范不会告诉你 JWKS 缓存应该设多少秒、Okta 的 Token Exchange 扩展如何映射到标准 OBO 流程、或者当 IdP 的 JWKS 端点响应延迟从 50ms 飙升到 2s 时你的中间件该如何降级。
本文覆盖的实战经验可以帮助你跨越这段距离。更多 MCP Server 开发最佳实践,参见 MCP Tools 设计最佳实践。对于 Node.js 环境的快速入门,推荐阅读 MCP Server Node.js 快速启动教程。
常见问题
MCP Remote Server 为什么必须使用 OAuth 2.1 而非简单 API Key?
API Key 是静态凭证,一旦泄露无法自动失效,且无法实现细粒度的 scope 权限控制和用户级别的操作审计。OAuth 2.1 提供短生命周期 Token(15 分钟)、自动刷新、scope 隔离和完整审计链,满足 SOC2/ISO27001 等企业合规要求。在多 Agent 客户端共享 MCP Server 的场景中,OAuth 还能通过 sub claim 区分每个操作的实际发起人。
如何选择 Azure AD、Okta 和 Auth0 作为 MCP 认证服务器?
Azure AD 适合已使用 Microsoft 生态的企业,原生支持 OBO 流程和条件访问策略;Okta 适合多云环境,其 Universal Directory 可聚合多个身份源;Auth0 适合需要快速集成的初创团队,其 Actions 管道可灵活注入自定义逻辑。决策时还需考虑:现有基础设施(企业已有哪个 IdP 的许可)、下游服务分布(Azure 资源多则 Azure AD 优势明显)、以及团队的 OAuth 经验水平。
JWKS 缓存应该如何设置才能兼顾性能和密钥轮转安全?
推荐设置 5 分钟 TTL 缓存,配合 miss-then-refetch 策略:当收到未知 kid 的 Token 时立即刷新缓存,但限制刷新频率为每分钟最多 1 次。这种策略既能覆盖正常密钥轮转窗口(Azure AD 约 6 周、Okta 约 90 天),又能防御恶意的缓存击穿攻击。同时建议在服务启动时进行 JWKS 预热,避免冷启动的第一个请求因 JWKS 获取延迟而超时。
On-Behalf-Of 流程在 MCP 场景中有什么具体作用?
当用户通过 AI Agent 调用 MCP Tool 访问下游 API(如 Microsoft Graph、企业内部服务)时,OBO 流程让 MCP Server 以用户身份而非服务身份请求下游资源。这确保了两个关键安全属性:权限不提升(下游 Token 的权限 ≤ 用户原始权限)和审计可追溯(下游服务的日志中记录的是实际操作人,而非 MCP Server 的 Service Principal)。
MCP Server 部署到生产环境时最容易忽略哪些安全配置?
五个高频遗漏点:1)未将 TLS 协议版本限制为 1.3(仍允许 TLS 1.2 的弱密码套件);2)refresh_token 未配置为一次性使用(攻击者可重放窃取的 refresh_token);3)CORS 配置使用 * 通配符(允许任何来源的跨域请求);4)scope 校验仅在登录时执行一次而非每次请求都检查;5)Token 端点缺少速率限制(暴露于暴力攻击和凭证填充)。使用 UUID Generator 为每次 Token 请求生成唯一的 correlation ID,有助于事后审计和问题排查。