Agent 工程体系 · 第 76/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 认证与授权:Actor、租户、对象权限、Scope 和二次校验
Agent 的安全问题,通常不是“有没有登录”这么简单。
一个 Agent 可能同时代表用户、组织、自动化任务和自身的运行时身份;它可能访问多个租户的数据、调用多个 MCP Server、读取记忆、生成代码、执行写操作,还可能在用户离开页面后继续运行。如果系统只在入口处校验一次 JWT,或者仅凭 scope=write 判断权限,最终往往会出现以下问题:
- 用户 A 的 Agent 读到了租户 B 的文档;
- 普通成员通过 Agent 修改了自己无权修改的订单;
- 低风险的“查询工具”被组合成高风险的数据导出能力;
- 用户授权给 Agent 的令牌被转发到错误的下游服务;
- 用户在审批后权限被撤销,但后台任务仍然继续执行;
- Agent 使用管理员身份创建了一个普通用户无法直接创建的资源;
- 所有权校验通过后,资源在实际写入前已经被转移,形成竞态漏洞。
因此,Agent 的授权不能被简化为一个布尔值:
authorized = true
更可靠的模型是:
一次操作是否允许
= 身份真实性
× Actor 语义
× 租户边界
× Scope
× 对象权限
× 当前状态
× 风险等级
× 二次校验
其中任意一个条件不成立,操作都应被拒绝或转入人工确认。
一、先区分认证、授权和责任归属
1. 认证回答“你是谁”
认证(Authentication,简称 AuthN)验证一个请求携带的凭据是否能证明某个身份。例如:
- 用户通过密码、Passkey 或企业单点登录完成认证;
- Agent Runtime 使用工作负载身份获取令牌;
- MCP Client 使用 OAuth Authorization Code + PKCE 获取访问令牌;
- 后台任务使用短期的服务身份令牌调用内部 API。
认证的结果通常是一个经过验证的主体:
{
"subject": "user_123",
"issuer": "https://id.example.com",
"auth_time": "2026-09-01T08:30:00Z",
"amr": ["webauthn"]
}
这里的 subject 只能说明“这个令牌代表谁”,并不能直接说明“这个主体可以对哪个对象做什么”。
2. 授权回答“你能做什么”
授权(Authorization,简称 AuthZ)判断一个已经认证的主体是否有权执行特定动作:
主体 user_123 是否可以对对象 document_456 执行 update?
授权至少包含四个输入:
(actor, action, resource, context)
例如:
(user_123, update, document_456, 当前租户、对象状态、认证强度)
如果只检查 actor,而不检查 resource,就是典型的水平越权风险;如果只检查 action,而不检查当前状态,可能把已归档订单重新修改;如果不检查 context,则无法表达“只能在工作时间操作”或“超过金额必须二次审批”。
3. 责任归属回答“谁发起、谁执行、谁负责”
Agent 系统还需要记录责任链。一个请求可能同时包含:
- Human User:实际使用系统的人;
- Agent:根据用户意图进行规划和决策的软件主体;
- Agent Runtime:承载 Agent 执行循环的运行时;
- Tool Client:调用工具协议的客户端;
- Tool Server / MCP Server:暴露工具能力的服务;
- Service Account:用于机器间调用的服务身份;
- Resource Owner:拥有数据或资源的用户、组织或租户。
“谁登录了”和“谁执行了”不一定是同一个主体。例如用户 Alice 授权 Agent 修改合同,但实际发起 HTTP 请求的是 agent-runtime-prod。审计记录不能只写:
{
"subject": "agent-runtime-prod",
"action": "update_contract"
}
至少应包含:
{
"initiator": {
"type": "human",
"id": "user_alice"
},
"delegate": {
"type": "agent",
"id": "agent_contract_ops"
},
"executor": {
"type": "workload",
"id": "agent-runtime-prod"
},
"tenant": "tenant_acme",
"action": "update",
"resource": "contract_928",
"decision": "allow"
}
这三个身份分别用于授权和审计:
| 身份 | 主要问题 |
|---|---|
initiator |
谁提出或批准了意图 |
delegate |
哪个 Agent 被授予了代理能力 |
executor |
哪个运行时、服务或工具实际执行了动作 |
如果把这三者压缩成一个 sub,系统很难判断一个动作究竟是用户直接完成、Agent 代办,还是后台任务自行扩展了权限。
二、Actor:Agent 不是用户,用户也不等于 Actor
1. Actor 的定义
Actor 是授权决策中的“行动主体”。在传统 Web 应用中,Actor 常常就是当前登录用户;在 Agent 系统中,Actor 可能是:
用户
Agent
工作负载
服务账号
后台任务
外部系统
用户与 Agent 的组合
一个完整的 Actor 表示可以写成:
Actor = {
type,
id,
tenant,
authority,
delegation,
authentication_strength,
execution_context
}
例如:
{
"type": "agent",
"id": "agent_finance_assistant",
"tenant_id": "tenant_acme",
"authority": {
"subject_id": "user_alice",
"delegated_by": "user_alice"
},
"authentication_strength": "user_session_with_mfa",
"execution_context": {
"run_id": "run_20260901_001",
"interactive": true
}
}
这里的 Agent 不是凭提示词“声称”自己代表 Alice,而是由可信的身份系统建立代理关系。
2. “代表用户”不等于“拥有用户全部权限”
设用户 Alice 的权限集合为:
P(user_alice) = {
read:invoice,
update:invoice,
approve:invoice,
export:invoice
}
Agent 被授权的权限集合应是一个受限子集:
P(agent) ⊆ P(user_alice)
例如:
P(agent) = {
read:invoice,
update:invoice
}
即使 Alice 自己拥有 approve:invoice,Agent 也不能因此自动获得审批权限。
更严格地说,代理权限应满足:
EffectivePermission(agent, user)
=
UserPermission(user)
∩
DelegationPolicy(agent, user)
∩
RuntimePolicy(agent)
其中:
UserPermission:用户本身拥有的权限;DelegationPolicy:用户允许 Agent 代行的权限;RuntimePolicy:平台针对该 Agent 类型、工具或环境配置的限制。
因此,如果 Alice 允许 Agent 读写发票,但组织规定 Agent 永远不能审批付款,则:
approve ∉ RuntimePolicy(agent)
最终仍然必须拒绝。
3. 代理链不能无限透明传递
考虑如下调用链:
用户 Alice
→ Agent A
→ MCP Client
→ MCP Server
→ ERP API
错误实现可能把 Alice 的原始访问令牌一路转发:
Authorization: Bearer alice_token
这会导致下游服务误以为令牌是为自己签发的,或者误以为上游已经完成了所有授权检查。MCP 的授权规范明确要求,MCP Server 必须验证收到的访问令牌确实是发给自己的,并且不得把客户端令牌直接透传到上游 API;调用上游时应使用面向上游资源、由对应授权服务器签发的另一个令牌。(modelcontextprotocol.io)
正确模型是令牌交换或重新获取:
alice_token
--验证 audience=mcp.example.com-->
mcp_session
--按最小权限申请-->
erp_token(audience=erp.example.com, scope=invoice.read)
这不仅是令牌格式问题,也是责任边界问题:
MCP Server 对客户端负责
ERP API 对 MCP Server 的服务身份负责
若下游需要知道原始用户,可以通过受控的委托声明传递:
{
"sub": "mcp-service",
"act": {
"sub": "agent_finance_assistant"
},
"may_act": {
"sub": "user_alice"
},
"aud": "erp-api",
"scope": "invoice.read"
}
但下游不能因为看到 may_act.user_alice 就跳过自身授权检查。
三、租户:身份隔离之外,还要隔离授权上下文
1. 租户不是用户组字段
租户(Tenant)是多租户系统中资源、策略、配额和密钥的隔离边界。一个用户可能属于多个租户:
user_alice ∈ {tenant_acme, tenant_beta}
因此,不能只把 tenant_id 存在用户资料里,然后在请求中默认使用第一个租户。授权请求必须明确当前租户:
{
"subject": "user_alice",
"active_tenant": "tenant_acme"
}
“用户属于租户”与“当前请求作用于哪个租户”是两个不同事实。
2. 租户边界必须进入每一次授权判断
可将资源表示为:
Resource = {
id,
tenant_id,
owner_id,
type,
state,
attributes
}
最基本的授权条件是:
resource.tenant_id = actor.active_tenant
但这只是必要条件,不是充分条件。
例如 Alice 属于 tenant_acme,对 document_1 的请求如下:
{
"actor": {
"subject": "user_alice",
"tenant": "tenant_acme"
},
"resource": {
"id": "document_1",
"tenant": "tenant_beta"
},
"action": "read"
}
即使 Alice 在 tenant_beta 也有同名角色,只要本次请求的活动租户是 tenant_acme,该对象仍然不能被访问,除非系统显式支持跨租户委托,并且该委托经过独立授权。
3. 数据库查询必须同时表达租户条件
以下查询是不安全的:
SELECT *
FROM documents
WHERE id = :document_id;
因为只要攻击者猜中或获得另一个租户的 document_id,查询就可能返回越权数据。
安全查询至少应是:
SELECT *
FROM documents
WHERE id = :document_id
AND tenant_id = :active_tenant_id;
更新和删除也必须带租户条件:
UPDATE documents
SET content = :content,
version = version + 1
WHERE id = :document_id
AND tenant_id = :active_tenant_id
AND version = :expected_version;
这里的 version 用于防止对象在授权检查和写入之间发生变化。单独在应用层先查询、再更新,无法避免下面的竞态:
T1: 查询 document_1,发现属于 tenant_acme,允许更新
T2: 资源所有权或租户归属发生变化
T3: T1 使用旧判断结果继续写入
把租户、对象版本和必要的状态条件放入同一条条件更新,可以让数据库参与最后一道原子性约束。
4. Agent 的租户上下文不能由模型生成
以下做法不可接受:
tenant_id = tool_arguments["tenant_id"]
因为工具参数可能来自模型,而模型输出不是可信安全边界。租户应从经过验证的请求上下文中获得:
tenant_id = request.auth.active_tenant_id
如果业务确实支持跨租户操作,则应要求调用方拥有明确的跨租户权限,并对目标租户执行重新授权:
if target_tenant != auth.active_tenant_id:
require_permission(auth, "tenant.cross_read", target_tenant)
require_step_up(auth, reason="cross_tenant_access")
四、对象权限:RBAC 只能回答一半问题
1. Scope、角色和对象权限处于不同层次
三者不能互相替代:
- 角色(Role):主体属于什么权限集合,例如
editor、finance_admin; - Scope:令牌被允许访问哪类能力,例如
invoice.read; - 对象权限(Object Permission):对某一个具体对象是否允许某个动作,例如 Alice 能否修改发票
inv_1001。
一个角色可能产生一组权限:
Role(editor)
→ {document.read, document.update}
Scope 则限制当前令牌:
Token.scope = {document.read}
对象策略进一步限制资源:
document_1001.owner_id = user_alice
document_1001.state = draft
最终决策可以写成:
Allow =
Authenticated(actor)
∧ TenantMatch(actor, resource)
∧ ScopeAllows(token, action)
∧ RoleAllows(actor, action)
∧ ObjectPolicyAllows(actor, action, resource)
∧ StateAllows(resource, action)
缺一不可。
2. 一个完整算例
假设:
Actor: Agent A,代表 Alice
Tenant: acme
Action: update
Resource: invoice_1001
系统状态:
{
"user": {
"id": "alice",
"tenant": "acme",
"roles": ["finance_editor"]
},
"agent": {
"id": "agent_finance",
"delegated_scopes": ["invoice.read", "invoice.update"]
},
"token": {
"aud": "invoice-api",
"scope": "invoice.read invoice.update",
"auth_time": "2026-09-01T08:30:00Z"
},
"invoice": {
"id": "invoice_1001",
"tenant": "acme",
"owner": "alice",
"state": "draft",
"amount": 8000
}
}
逐步判断:
第一步:认证
token.signature_valid = true
token.exp > now = true
token.aud = invoice-api = true
身份认证通过。
第二步:租户匹配
actor.tenant = acme
invoice.tenant = acme
通过。
第三步:Scope
invoice.update ∈ token.scope
通过。
第四步:用户角色
finance_editor → invoice.update
通过。
第五步:代理范围
invoice.update ∈ agent.delegated_scopes
通过。
第六步:对象策略
invoice.owner = alice
actor.delegation.subject = alice
通过。
第七步:状态策略
invoice.state = draft
update 允许作用于 draft
通过。
所以本次请求允许。
但如果发票状态是:
state = "approved"
即使前六步都通过,最终仍应拒绝:
approved 发票不可直接修改
如果金额为 800000,还可能进入二次校验,而不是直接允许。
3. 反例:只检查 Scope
错误代码:
def update_invoice(request):
if "invoice.update" not in request.token.scopes:
raise Forbidden()
invoice = db.get_invoice(request.json["invoice_id"])
invoice.content = request.json["content"]
db.save(invoice)
攻击者只需获得一个具有 invoice.update 的令牌,就可能修改:
- 其他用户的发票;
- 其他部门的发票;
- 其他租户的发票;
- 已审批或已结算的发票。
Scope 证明的是:
这个令牌可以请求 invoice.update 这类能力
它没有证明:
这个令牌可以修改任意 invoice
五、Scope:令牌能力的上限,而不是完整授权结论
1. Scope 的语义
Scope 是 OAuth 访问令牌携带的权限范围,通常是空格分隔的字符串:
scope = "invoice.read invoice.update"
资源服务器应将 Scope 解释为令牌可以尝试访问的能力集合,而不是最终对象权限。
可以把 Scope 看成一个上界:
RequestedAction ⊆ TokenScope
如果请求动作不在 Scope 中,必定拒绝;如果动作在 Scope 中,还必须继续执行对象和上下文判断。
2. Scope 应表达能力,不应表达所有业务条件
适合放入 Scope 的内容:
document.read
document.update
invoice.read
invoice.pay
calendar.write
不适合把所有动态条件编码成 Scope:
invoice.update:tenant_acme:invoice_1001:until_10:30
这样会造成令牌数量、缓存、撤销和策略管理的复杂度爆炸。对象归属、金额、状态、审批人等条件应由授权服务根据当前资源状态判断。
3. Scope 设计的两个陷阱
陷阱一:Scope 过宽
scope = "admin"
Agent 获得一个过宽 Scope 后,模型可能通过工具组合完成原本不希望它完成的动作。OWASP 将 LLM 应用中的过度代理能力列为 Excessive Agency 风险,并强调应限制功能、权限和自主行动范围。(genai.owasp.org)
陷阱二:Scope 名称看似细粒度,实际实现仍然全开放
例如系统定义了:
customer.read
customer.update
customer.delete
但代码只是:
if "customer.update" in scopes:
db.execute("UPDATE customers SET ... WHERE id = ?", [id])
如果没有租户、对象所有权和状态条件,这仍然是全局对象写权限。
4. Scope 的收缩原则
令牌申请时,客户端请求的 Scope 应不超过它实际需要的范围:
scope_requested ⊆ scope_allowed_by_client_policy
Agent 调用不同工具时,也不应长期持有整个会话的最大权限。可以采用按工具、按任务或按动作申请的短期令牌:
执行搜索:
scope = document.read
执行草稿修改:
scope = document.read document.update
执行发布:
scope = document.publish
+ step-up authentication
这里的关键是把“发现信息”和“产生不可逆副作用”分离。
六、二次校验:高风险动作不能只依赖初始登录
1. 二次校验是什么
二次校验(Step-up Authentication / Re-authentication)是在已有会话或令牌基础上,针对高风险操作再次验证用户或授权事实。
它不一定等于“再次输入密码”,可以是:
- Passkey / WebAuthn;
- MFA;
- 短信或硬件令牌;
- 企业审批;
- 对高风险参数进行用户确认;
- 重新确认目标对象、金额和收款方;
- 对后台任务重新签发一次性执行凭证。
关键在于:初始认证证明“用户登录过”,二次校验证明“用户现在愿意批准这个具体高风险动作”。
2. 风险分级
可以定义风险函数:
Risk(action, resource, context)
例如:
Risk =
action_weight
+ amount_weight
+ reversibility_weight
+ external_effect_weight
+ tenant_scope_weight
并设置阈值:
Risk < 30 → 直接允许
30 ≤ Risk < 70 → 用户确认
Risk ≥ 70 → 强认证 + 审批
这不是通用标准公式,而是一个便于工程实现的决策模型。真正的安全要求来自组织策略、业务风险和合规边界。
适合要求二次校验的动作通常包括:
- 删除数据;
- 发布或公开内容;
- 修改权限和成员;
- 支付、退款、转账;
- 导出大量敏感数据;
- 跨租户访问;
- 修改密钥、Webhook 或 OAuth Client;
- 执行高权限代码或生产环境命令。
3. 用户确认必须绑定具体动作
错误的确认方式:
“Agent 要执行一个操作,是否继续?”
这种确认没有告诉用户:
- 操作对象是谁;
- 将发生什么变化;
- 是否不可逆;
- 影响金额是多少;
- 哪些外部系统会被调用。
更安全的确认凭证应绑定操作摘要:
{
"approval_id": "approval_789",
"actor": "user_alice",
"tenant": "acme",
"action": "invoice.pay",
"resource": "invoice_1001",
"amount": 8000,
"destination": "supplier_77",
"request_hash": "sha256:...",
"expires_at": "2026-09-01T09:00:00Z",
"used": false
}
执行时重新计算请求摘要:
hash(current_request) == approval.request_hash
只有一致时才能使用审批。
如果用户批准的是:
支付 invoice_1001,金额 8,000 元
Agent 随后把金额改成 80,000 元,审批必须失效,而不能被视为对“支付发票”这个抽象动作的永久授权。
4. 二次校验不是模型确认
以下内容不能作为安全确认:
模型输出:“用户已经确认,可以执行。”
模型生成的自然语言不是可信凭据。可信确认必须来自:
- 前端用户交互;
- 认证服务;
- 审批系统;
- 一次性签名;
- 服务器端产生且不可伪造的审批记录。
七、完整授权决策模型
可以将一次 Agent 工具调用形式化为:
Request = (a, t, s, r, op, c)
其中:
a:Actor;t:活动租户;s:访问令牌及 Scope;r:资源对象;op:动作;c:上下文,例如时间、认证强度、任务状态和审批信息。
定义授权函数:
Permit(Request) =
AuthN(a, s)
∧ Tenant(a, t, r)
∧ Scope(s, op)
∧ Delegation(a, op)
∧ ObjectPolicy(a, op, r)
∧ StatePolicy(op, r.state)
∧ RiskPolicy(op, r, c)
∧ StepUpSatisfied(op, r, c)
1. 推导过程
假设请求是:
Agent A 代表 Alice
在租户 acme
对 invoice_1001 执行 pay
初始事实:
AuthN(A, token) = true
Tenant(A, acme, invoice_1001) = true
Scope(token, invoice.pay) = true
但:
invoice_1001.amount = 80,000
Risk(invoice.pay, invoice_1001) = high
StepUpSatisfied(pay, invoice_1001) = false
根据合取逻辑:
true ∧ true ∧ true ∧ false = false
所以必须拒绝或返回“需要二次校验”,而不能因为前三项为真就执行支付。
2. 拒绝原因应可诊断但不泄露敏感信息
对调用方可以返回:
{
"error": "step_up_required",
"required_action": "reauthenticate",
"approval_scope": {
"action": "invoice.pay",
"resource": "invoice_1001"
}
}
不应返回:
“用户 Alice 没有 finance_admin 角色,但 Bob 有。”
因为错误信息可能泄露组织成员、角色和对象存在性。服务端日志可以记录更详细的策略命中情况,但日志本身必须脱敏并防止令牌泄露。
八、Agent 调用链中的认证与授权时序
sequenceDiagram
participant U as 用户
participant UI as Agent UI
participant AS as 授权服务器
participant AR as Agent Runtime
participant PDP as 策略决策点
participant MCP as MCP Server
participant ERP as ERP API
U->>UI: 登录并选择租户 acme
UI->>AS: Authorization Code + PKCE
AS-->>UI: Access Token(scope=invoice.read invoice.update)
UI->>AR: 用户任务 + 令牌上下文
AR->>PDP: 请求 invoice_1001 的 update 授权
PDP-->>AR: Allow
AR->>MCP: Bearer token + 工具参数
MCP->>MCP: 验证签名、issuer、audience、exp、scope
MCP->>PDP: 重新执行对象权限和状态检查
PDP-->>MCP: Allow
MCP->>ERP: 使用 ERP 专用令牌
ERP-->>MCP: 更新结果
MCP-->>AR: 工具结果
AR-->>UI: 返回结果和审计引用
关键路径有两次授权判断:
- Agent Runtime 的预检查:避免模型调用明显不允许的工具;
- MCP Server 的强制检查:防止绕过 Agent Runtime 直接调用工具。
第一次检查是优化和用户体验的一部分,第二次检查才是资源服务必须承担的安全边界。不能因为 Runtime 已经检查过,就让 MCP Server 信任来自请求体的 authorized=true。
九、MCP 中的 OAuth、客户端身份、Scope 和令牌
1. MCP 的角色映射
在基于 HTTP 的 MCP 授权模型中,可以将角色映射为:
| MCP / OAuth 角色 | 典型实体 |
|---|---|
| Resource Owner | 用户或组织 |
| OAuth Client | Agent Host、MCP Client |
| Authorization Server | 企业 IdP 或 MCP 授权服务 |
| Resource Server | 受保护的 MCP Server |
| Upstream Resource Server | ERP、CRM、云平台 API |
MCP 授权规范基于 OAuth 机制,并规定 HTTP 传输场景下的发现、令牌使用和资源绑定。MCP Server 作为资源服务器,MCP Client 作为客户端,授权服务器负责向客户端签发用于访问 MCP Server 的令牌。(modelcontextprotocol.io)
2. Authorization Code + PKCE 适合用户代理
用户参与的典型流程是:
MCP Client
→ 发现 MCP Server 的授权服务器
→ 生成 code_verifier 和 code_challenge
→ 打开授权页面
→ 用户登录并批准
→ 返回 authorization code
→ 使用 code_verifier 换取 token
PKCE 的作用是防止授权码被截获后被其他客户端兑换。MCP 授权规范要求客户端使用 PKCE,并要求严格校验重定向 URI 和状态参数。(modelcontextprotocol.io)
伪代码:
verifier = random_urlsafe_string()
challenge = base64url(sha256(verifier))
state = random_urlsafe_string()
authorize_url = build_url(
authorization_endpoint,
response_type="code",
client_id=client_id,
redirect_uri=registered_redirect_uri,
code_challenge=challenge,
code_challenge_method="S256",
state=state,
resource="https://mcp.example.com"
)
# 用户完成授权后返回 code 和 state
if returned_state != state:
raise SecurityError("state mismatch")
token = post(token_endpoint, {
"grant_type": "authorization_code",
"code": returned_code,
"redirect_uri": registered_redirect_uri,
"client_id": client_id,
"code_verifier": verifier,
"resource": "https://mcp.example.com"
})
这里的 resource 不是普通业务参数,而是 OAuth Resource Indicators 机制中的资源标识。MCP 客户端应在授权请求和令牌请求中指定目标 MCP Server,并使用 MCP Server 的规范 URI;MCP Server 需要验证令牌确实面向自己签发。(modelcontextprotocol.io)
3. Client Credentials 适合纯机器身份
没有用户参与时,可以使用客户端凭据模式或对应 MCP 扩展:
Agent Runtime
--client_id + client_secret / JWT assertion-->
Authorization Server
--access_token(scope=inventory.read)-->
Agent Runtime
该模式表达的是:
这个应用或工作负载有权访问资源
它不天然表达:
某个具体用户已经批准这次操作
因此,使用 Client Credentials 调用支付、删除或权限变更工具时,仍需要额外的业务审批、服务端策略或人类确认。MCP 官方提供了面向机器间场景的 OAuth Client Credentials 扩展,但它是对用户交互式授权流程的补充,不是把应用身份自动变成用户身份。(modelcontextprotocol.io)
4. 401 和 403 必须区分
MCP Server 或普通资源服务器应区分认证失败和授权失败:
401 Unauthorized
= 没有有效身份,或者令牌无效、过期、签发者错误、audience 不匹配
403 Forbidden
= 身份有效,但 Scope、角色、对象权限或策略不足
例如:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
适用于:
令牌不存在
令牌签名错误
令牌已过期
令牌不是发给当前 MCP Server
而:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error":"insufficient_scope"}
适用于:
令牌有效,但没有 invoice.pay
MCP 授权规范同时要求令牌放在 Authorization: Bearer ... 请求头中,不得放在 URL 查询字符串中;无效或过期令牌应返回 401,Scope 不足应返回 403。(modelcontextprotocol.io)
十、MCP 授权服务器发现与版本边界
截至 2026 年 9 月,MCP 的授权规范仍然是版本敏感的,不能把不同版本页面中的要求混写。
2025 年 6 月的 MCP HTTP 授权规范要求:
- MCP Server 提供 OAuth 2.0 Protected Resource Metadata;
- MCP Client 根据资源元数据发现授权服务器;
- 客户端获取 OAuth Authorization Server Metadata;
- 支持 PKCE;
- 对令牌执行 audience 校验;
- 禁止令牌透传。(modelcontextprotocol.io)
官方 MCP 项目在 2026 年 7 月 28 日发布了 2026-07-28 版本,并引入了无会话协议核心、基于请求头的工具路由和授权强化。其中授权相关变化包括更严格的 issuer 校验、客户端身份元数据文档方向,以及逐步从 Dynamic Client Registration 转向 Client Metadata Documents。(blog.modelcontextprotocol.io)
因此,工程实现应明确写出支持的 MCP 版本:
supported_protocol_versions:
- 2025-06-18
- 2026-07-28
并为每个版本测试:
- 授权服务器发现路径;
WWW-Authenticate处理;resource参数;issuer校验;- PKCE 支持;
- 客户端注册方式;
- 令牌 audience;
- 401 / 403 行为。
不能因为某个客户端支持新版本,就假定所有 MCP Server 都支持相同的发现机制和客户端注册机制。
十一、对象授权的实现方式
1. 集中式策略决策
可以把授权拆成两个组件:
PEP(Policy Enforcement Point)
= 执行策略,拒绝或放行请求
PDP(Policy Decision Point)
= 根据 Actor、动作、资源和上下文计算决策
调用结构:
decision = policy_engine.check(
actor=auth.actor,
tenant_id=auth.active_tenant_id,
action="invoice.update",
resource=invoice,
context={
"token_scopes": auth.scopes,
"auth_time": auth.auth_time,
"run_id": run_id
}
)
if decision.effect != "allow":
raise Forbidden(decision.reason)
PDP 返回结果可以包含:
{
"effect": "deny",
"reason": "resource_state_forbids_update",
"obligations": [],
"policy_version": "invoice-policy-42"
}
策略版本写入审计日志后,可以回答:
这次请求是在什么策略版本下被允许或拒绝的?
2. 数据库行级安全
如果数据库支持 Row-Level Security,可以让租户边界靠近数据层:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY invoice_tenant_isolation
ON invoices
USING (tenant_id = current_setting('app.tenant_id'));
事务开始时设置上下文:
BEGIN;
SELECT set_config(
'app.tenant_id',
'tenant_acme',
true
);
SELECT *
FROM invoices
WHERE id = 'invoice_1001';
COMMIT;
但数据库行级安全不能替代动作授权。它可以防止读取其他租户的行,却不能独立判断:
当前 Actor 是否能执行 pay?
发票状态是否允许 update?
是否需要二次审批?
因此应组合使用:
应用层:动作、Scope、角色、对象策略、风险
数据库层:租户行隔离、最终写入条件
3. 能力令牌
对后台 Agent,可以使用不可转移或范围受限的能力令牌:
{
"capability_id": "cap_123",
"subject": "agent_finance",
"tenant": "acme",
"action": "invoice.update",
"resource": "invoice_1001",
"constraints": {
"max_amount": 10000,
"expires_at": "2026-09-01T09:00:00Z",
"max_uses": 1
}
}
能力令牌适合表达一次明确授权,但必须防止:
- 被复制到其他租户;
- 被用于其他对象;
- 被重复使用;
- 过期后仍有效;
- 从低风险对象升级到高风险对象。
一次性能力令牌的消费应当是原子操作:
UPDATE capabilities
SET used_at = CURRENT_TIMESTAMP
WHERE id = :capability_id
AND used_at IS NULL
AND expires_at > CURRENT_TIMESTAMP
AND tenant_id = :tenant_id;
检查受影响行数:
1 → 成功消费
0 → 不存在、已使用、过期或租户不匹配
如果先 SELECT 再 UPDATE,并发请求可能同时观察到 used_at IS NULL,从而造成重复执行。
十二、后台任务、记忆和缓存中的授权失效
1. 后台任务不能永久继承前台权限
Agent 经常会把任务放入队列:
用户请求
→ Agent 创建任务
→ 队列
→ Worker 延迟执行
错误实现是把原始 Access Token 整个存入任务:
{
"job_id": "job_1",
"access_token": "eyJ..."
}
问题包括:
- 令牌可能在队列、日志和死信队列中暴露;
- 用户权限撤销后,任务仍使用旧令牌;
- 任务执行时租户、对象状态和审批状态可能已经变化;
- 任务可能跨越很长时间,初始上下文已不再有效。
更合理的任务载荷是授权引用:
{
"job_id": "job_1",
"run_id": "run_1",
"initiator_id": "user_alice",
"agent_id": "agent_finance",
"tenant_id": "tenant_acme",
"action": "invoice.update",
"resource_id": "invoice_1001",
"approval_id": "approval_789"
}
Worker 执行时重新获取或交换短期令牌,并重新执行授权:
读取任务
→ 检查任务未取消
→ 查询当前租户和对象
→ 检查用户或代理授权是否仍有效
→ 检查对象版本和状态
→ 检查审批未过期且摘要匹配
→ 获取短期执行令牌
→ 原子写入
2. 记忆必须携带租户和可见性
Agent 记忆不能只有文本:
{
"content": "客户希望下周续费"
}
至少应包含:
{
"memory_id": "mem_1",
"tenant_id": "tenant_acme",
"owner_type": "user",
"owner_id": "alice",
"visibility": "private",
"source_run_id": "run_1",
"created_at": "2026-09-01T08:30:00Z"
}
向量检索也必须带租户过滤:
similarity(query, embedding)
WHERE tenant_id = active_tenant
AND visibility IN (allowed_visibility)
不能先全局召回,再在模型上下文中“提醒模型不要使用其他租户的数据”。一旦越权内容进入上下文,后续输出、摘要和记忆写入都可能扩大泄露范围。OWASP 将敏感信息披露、向量和嵌入弱点列为生成式 AI 应用的重要风险类别。(genai.owasp.org)
3. 授权缓存必须绑定完整键
不安全的缓存键:
authz:{user_id}:{action}
它缺少:
- 租户;
- 对象;
- 对象版本;
- Agent;
- Scope;
- 策略版本;
- 二次校验状态。
更完整的缓存键可以是:
authz:v42:
tenant_acme:
actor_agent_finance:
subject_alice:
invoice.update:
invoice_1001:
object_version_7:
scope_hash_abc:
stepup_false
权限撤销、租户切换、对象转移、策略发布后,都必须使相关缓存失效。对于高风险写操作,宁可不缓存最终决策,也不要用过期的 allow 结果执行不可逆动作。
十三、常见失败表现与诊断方法
1. 出现“偶发跨租户数据”
重点检查:
请求租户是否来自可信上下文?
数据库查询是否始终带 tenant_id?
向量检索是否带租户过滤?
缓存键是否包含 tenant_id?
后台任务是否保存并校验 tenant_id?
工具参数中的 tenant_id 是否覆盖了认证上下文?
审计日志应能关联:
request_id
run_id
actor_id
initiator_id
tenant_id
resource_id
action
policy_version
decision
denial_reason
如果日志只有:
user=alice, tool=search, success=true
就无法判断 Alice 搜索的是哪个租户、哪个对象范围,也无法重建 Agent 的行为链。
2. 出现“Scope 正确但仍越权”
通常说明 Scope 被误当成对象权限。检查工具实现是否存在:
if "document.read" in scopes:
return get_document(document_id)
应改为:
document = get_document(
id=document_id,
tenant_id=auth.active_tenant_id
)
require_object_permission(
actor=auth.actor,
action="document.read",
resource=document
)
3. 出现“用户撤销后任务仍执行”
检查:
- 队列是否存储了长期令牌;
- Worker 是否重新查询授权;
- 任务是否可取消;
- 令牌撤销是否能传播到资源服务;
- 审批是否绑定对象版本;
- 任务重试是否会绕过原始授权。
4. 出现“用户说没有批准过”
检查审批记录是否绑定:
actor
tenant
action
resource
arguments_hash
auth_time
approval_time
expiration
used_at
如果审批只记录:
user_alice approved invoice.pay
而没有金额、对象和参数摘要,就无法证明用户批准的是哪一次具体操作。
5. 出现“MCP Server 接受了别的服务令牌”
检查:
iss 是否正确?
aud 是否包含当前 MCP Server?
签名算法和密钥是否来自正确 issuer?
令牌是否通过 Authorization Header 传递?
是否存在把客户端令牌直接转发到上游的代码?
MCP 规范特别强调资源受众绑定和禁止令牌透传,因为错误处理会形成 confused deputy:下游服务把本来发给其他资源的令牌当作合法凭据接受。(modelcontextprotocol.io)
十四、规范保证、实现选择与经验建议
需要把三类内容分开。
规范或协议要求
例如:
- OAuth 令牌应经过资源服务器验证;
- MCP HTTP 请求使用 Bearer Authorization Header;
- MCP Server 应验证令牌 audience;
- 无效令牌返回 401,权限不足返回 403;
- MCP 客户端使用 PKCE;
- 令牌不得放在 URL 查询参数中;
- MCP Server 不应透传收到的客户端令牌。(modelcontextprotocol.io)
这些是协议层的要求或规范语义。
常见实现
例如:
- JWT 作为访问令牌;
- OPA、Cedar 或自研策略引擎作为 PDP;
- PostgreSQL RLS 实现租户行隔离;
- Redis 缓存授权决策;
- Worker 执行时重新交换短期令牌;
- 使用审批表保存二次确认状态。
这些是实现方式,不代表所有系统都必须采用。
经验建议
例如:
- 高风险动作不缓存最终允许结果;
- 代理权限取用户权限、委托范围和运行时策略的交集;
- 工具参数中的租户字段不能覆盖认证上下文;
- 审批必须绑定动作、对象和参数摘要;
- 审计记录同时保留 initiator、delegate 和 executor;
- 让 MCP Server 自己成为不可绕过的授权边界。
这些建议来自 Agent 的执行特性和常见故障模式,具体阈值仍需根据业务风险配置。
NIST AI RMF 是用于管理人工智能风险的自愿性框架,目标是将可信性考虑纳入 AI 系统的设计、开发、使用和评估;它不是一套直接定义 Actor、Scope 或租户策略的授权协议。因而,工程团队可以用它组织风险识别、测量、管理和治理,但仍需自行把这些要求落实为身份、策略、日志和执行控制。(nist.gov)
十五、一个可落地的最小授权接口
下面的接口展示授权决策所需的最小输入:
from dataclasses import dataclass
from typing import FrozenSet
@dataclass(frozen=True)
class Actor:
kind: str
id: str
tenant_id: str
delegated_subject_id: str | None
@dataclass(frozen=True)
class Resource:
kind: str
id: str
tenant_id: str
owner_id: str | None
state: str
version: int
@dataclass(frozen=True)
class AuthorizationRequest:
actor: Actor
scopes: FrozenSet[str]
action: str
resource: Resource
step_up_id: str | None
def authorize(req: AuthorizationRequest) -> tuple[bool, str]:
if req.actor.tenant_id != req.resource.tenant_id:
return False, "tenant_mismatch"
if req.action not in req.scopes:
return False, "insufficient_scope"
if req.actor.kind == "agent":
if req.actor.delegated_subject_id is None:
return False, "missing_delegation"
if req.resource.owner_id != req.actor.delegated_subject_id:
return False, "object_permission_denied"
if req.action == "invoice.update" and req.resource.state != "draft":
return False, "invalid_resource_state"
if req.action in {"invoice.pay", "invoice.delete"}:
if req.step_up_id is None:
return False, "step_up_required"
return True, "allow"
这个示例没有实现完整的生产级身份验证、策略版本、数据库事务和撤销机制,但它展示了几个不可省略的事实:
- 租户来自 Actor 和 Resource 的比较;
- Scope 只检查动作是否在令牌能力内;
- Agent 还需要代理主体;
- 对象权限与资源状态独立判断;
- 高风险动作需要二次校验;
- 最终结论不是由模型决定,而是由服务端授权函数决定。
生产环境中,authorize() 的返回值还应与数据库写入绑定,避免“授权成功但写入了另一个版本的对象”:
UPDATE invoices
SET status = 'paid',
version = version + 1
WHERE id = :invoice_id
AND tenant_id = :tenant_id
AND version = :expected_version
AND status = 'approved';
若影响行数为零,必须把操作视为失败,并重新读取对象状态,而不能继续假设授权仍然有效。
Agent 安全授权的核心,不是给模型更多或更少的工具,而是建立一条不可模糊的责任链:
谁发起
→ 哪个 Agent 代理
→ 哪个运行时执行
→ 在哪个租户中
→ 对哪个对象
→ 执行什么动作
→ 持有什么 Scope
→ 满足什么对象和状态条件
→ 是否经过二次校验
→ 最终由哪个服务强制执行
Actor 解决“行动主体”问题,租户解决“隔离边界”问题,对象权限解决“具体资源”问题,Scope 解决“令牌能力上限”问题,二次校验解决“高风险动作的即时意愿”问题。只有这些条件共同参与决策,Agent 才不会因为“代表用户”而获得用户全部权限,也不会因为“拿到了 write Scope”就能修改系统中的任意对象。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 提示注入防护:间接注入、指令隔离、数据标记和检测
- 下一篇:Agent Secret 与网络安全:凭证代理、SSRF、DNS、重定向和出口
- 延伸:MCP 认证与授权:OAuth、客户端身份、Scope、令牌和代理风险
- 延伸:Agent 多租户系统:数据、模型、工具、记忆、配额和密钥隔离
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论