为什么不直接阻止 agent 访问敏感操作?
应用中的某些操作比其他操作更重要。修改密码、添加团队成员、导出数据、删除项目、批准订单: 如果这些操作在没有正确权限的情况下发生,造成的损失是真实的。
与此同时,应用中的合法工作越来越多由自动化完成。用户让助手更新设置。合作方集成按计划创建记录。内部 agent 提交支持工单。把所有自动化客户端都拦在敏感路由之外,会破坏这些工作,并把人推回到手工操作,或者推向更难保障安全的变通办法。
答案不是“阻止 agent”或“信任 agent”。而是一个模型,在这个模型里,操作风险、请求背后的权限以及关于 actor 的证据各自发挥不同作用。本指南将构建这个模型。关于其背后的更广泛思路,请参见为人类和 AI agent 设计应用级策略。
第 1 步: 按风险对路由分级
先对操作分类,而不是对客户端分类。一个简单的三级方案对大多数应用都适用:
| 级别 | 示例 | 默认要求 |
|---|---|---|
| 低 | 读取公开数据、搜索、查看自己的仪表盘 | 如有需要,正常身份验证 |
| 中 | 创建或编辑自己的内容、邀请查看者 | 已认证,且对资源有授权 |
| 高 | 更改凭据、更改所有权、批量导出、删除、批准 | 授权加升级检查或复核 |
把级别写入路由配置,这样在代码审查中就能看到:
type Tier = "low" | "medium" | "high";
export const routeTier: Record<string, Tier> = {
"GET /projects": "low",
"POST /projects": "medium",
"POST /projects/:id/members": "medium",
"POST /account/password": "high",
"POST /exports": "high",
"DELETE /projects/:id": "high",
};级别变化缓慢,也容易推理。本指南中的其他内容都建立在它们之上。
第 2 步: 身份、凭据和授权有什么区别?
有三个问题经常被混为一谈:
- 是谁或什么在执行操作? 这是 actor 证据: 一个交互中的人类、一个借助 AI 的人、一个 agent,或者 Unknown,依据可能是 Verified 或 Declared。
- 出示了哪种凭据? 会话 cookie、API key、OAuth access token。
- 这个凭据是否被允许对这个资源执行这个操作? 这就是授权。
一个已验证的 agent 签名(RFC 9421)很好地回答了第一个问题。它并不能说明第三个问题,这一点在已签名的 AI agent 请求证明什么,以及不能证明什么中有详细说明。一个带有正确 scope 的 OAuth token 回答的是第三个问题。它对第一个问题说明很少。对于高级别操作,你的决策始终需要对第三个问题有一个可靠答案,并利用第一个问题来决定还要追加多少额外保证。
interface Context {
actor: { actor: "human" | "human_ai" | "agent" | "unknown"; basis: "verified" | "declared" | "inferred" | "unknown" };
credential: { type: "session" | "oauth" | "api_key"; scopes: string[]; subject: string };
}
function authorized(ctx: Context, action: string, resourceOwner: string): boolean {
return ctx.credential.subject === resourceOwner && ctx.credential.scopes.includes(action);
}不会因为 actor 结果看起来有利,就放松授权。actor 上下文只会增加要求,或者在同样安全的路径之间做选择。
第 3 步: 当 agent 代表人类行动时,使用委托授权
当 agent 代表用户行动时,给它一个自己的、权限受限的凭据,不要让它复用用户的会话。OAuth 2.0 是标准工具: 用户为特定客户端批准特定 scope,token 会记录该客户端。RFC 9700,即 OAuth 2.0 Security Best Current Practice,描述了如何安全地部署它。MCP server 采用同样的模式: MCP authorization specification 将受保护的 MCP server 视为 OAuth 2.1 resource server。我们的文章谁在调用你的 MCP server?把这一模式应用到工具调用中。
这正好带来你在敏感操作中需要的能力:
- token 告诉你哪个客户端在行动,以及它代表谁在行动。
- scope 限制它能做什么。带有
calendar:write的调度 agent 不能修改密码。 - 用户可以撤销授权,而无需更改自己的凭据,且 OAuth 2.0 Token Revocation (RFC 7009) 定义了客户端如何通知 authorization server 该 token 不再有效。
- 审计日志会显示是 agent 而不是用户执行了该操作。
如果你的应用还不支持委托凭据,应优先为 agent 最常需要的操作提供支持。对于自动化访问来说,这是最有效的单项控制。
第 4 步: 什么时候应该要求升级检查?
升级检查意味着在敏感操作发生时要求额外保证。对于受 OAuth 保护的 API,RFC 9470 定义了 resource server 如何通知客户端,当前 token 的认证强度不够,或者时间不够近。合适的升级检查取决于谁在执行操作:
- 交互中的人类 (Human, Human + AI): 重新认证(参见 NIST SP 800-63B 中的重新认证要求)、基于 WebAuthn 的 passkey 提示,或者一次性验证码。
- 带有委托凭据的 agent: 专门用于高级操作的 scope,由用户明确授予,或者发送给用户的带外确认。
- Unknown: 保持路由现有的强校验,不变。
type Requirement = "none" | "reauth" | "explicit_scope" | "user_confirmation" | "review";
function stepUp(tier: Tier, ctx: Context): Requirement {
if (tier !== "high") return "none";
const { actor } = ctx.actor;
if (actor === "human" || actor === "human_ai") return "reauth";
if (actor === "agent" && ctx.credential.type === "oauth") return "explicit_scope";
if (actor === "agent") return "user_confirmation";
return "reauth";
}注意,Human 结果不会跳过升级检查。它只是选择适合交互中人的升级检查。推断可能出错,所以它们应该改变检查的形式,而不是取消检查。
第 5 步: 在需要判断的地方路由到人工复核
有些操作罕见、影响很大,而且很难安全自动化: 批量导出客户数据、凭据丢失后的账户恢复,或者删除组织。对于这些操作,复核队列通常比任何自动规则都更合适。
actor 上下文通过向复核者提供证据,让复核更快也更公平: 类别、依据、使用的凭据、类似请求的历史。复核者做决定。分类提供信息。
function shouldReview(route: string, ctx: Context): boolean {
const reviewRoutes = new Set(["POST /exports", "POST /account/recover"]);
if (!reviewRoutes.has(route)) return false;
return !(ctx.credential.type === "oauth" && ctx.credential.scopes.includes("export:bulk"));
}第 6 步: 按级别选择失败行为
actor 评估可能超时,也可能缺少证据。要提前决定会发生什么。
- 低级别: 失败开放。请求正常继续。
- 中级别: 失败开放,同时保留正常授权。
- 高级别: 失败封闭到现有的强校验。缺失的 actor 证据绝不会削弱保护,也绝不会阻止原本会通过正常检查的请求。
关键属性是对称性。actor 评估中的失败,应让操作保持与添加 actor 上下文之前完全相同的安全性,也完全相同的可用性。
第 7 步: 不要基于推断对人做出有后果的决定
推断出的分类,是关于一次交互的证据,不是关于某个人的事实。不要因为某个会话看起来是自动化或 AI 辅助的,就拒绝某人的求职申请、信贷决定、考试结果、住房,或基本服务的访问权限。在这些流程中,使用 actor 上下文来选择流程,例如人工复核或替代验证路径,并确保当事人可以完成它。
这既是公平原则,也是实用原则。推断会产生误报,而这些流程中的误报代价,会落到一个没有做错任何事的人身上。
第 8 步: 先以观察模式推出
在这些措施影响真实请求之前,先以观察和预览模式运行:
- 记录级别、actor 类别、依据、凭据类型,以及你的策略本应要求的升级检查。
- 按类别抽样复核高级别请求。合法 agent 是在使用委托凭据,还是在复用用户会话?
- 先修复你发现的缺口,通常是提供带范围的凭据,然后再执行。
- 先在单一路由的小范围上执行,衡量完成率,然后再扩大。
第 9 步: 保留能回答“是谁以及代表谁”的审计轨迹
对于每一次高级别操作,都要记录足够的信息,以便日后重建发生了什么:
- 请求发生时的 actor 类别和依据,
- 凭据类型、它被签发给的客户端,以及它所代表的 subject(OAuth 2.0 Token Exchange,RFC 8693,定义了一个
actclaim,用来准确记录这类“代表谁行动”的关系), - 授权该操作的 scope,
- 应用的升级检查或复核要求,以及其结果。
audit.write({
action: "project.export",
actor: ctx.actor,
client: ctx.credential.type === "oauth" ? clientIdFromToken(req) : undefined,
subject: ctx.credential.subject,
scopes: ctx.credential.scopes,
requirement,
outcome: "approved",
at: new Date().toISOString(),
});有了这条轨迹,像“是某个人还是某个助手删除了这个项目,谁批准的”这样的问题就能得到清晰答案。这能让事故响应更快,也让用户相信,应用中的自动化帮助是有责任可追踪的。
一个完整示例
一个项目管理应用看到助手为用户更新任务截止日期,并且偶尔尝试导出整个项目。
- 更新任务属于中级别。助手使用带有
tasks:write的 OAuth token。已授权,无需升级检查。允许。 - 导出项目属于高级别。助手的 token 缺少
export:bulk。应用向用户发送确认请求。用户批准后,应用为该客户端授予该 scope。 - 一个 Unknown 客户端尝试用复用的会话 cookie 执行同样的导出。现有的重新认证提示会按原样生效,和以前一样。
没有人因为是 agent 而被阻止。每个高影响操作都带有明确授权。
Proof of Actor 在这里的作用
Proof of Actor 提供这个模型中的 actor 侧: 谁或什么在行动,以及其依据是什么。你的应用保留身份验证、授权和最终决策。Detect actor,Understand 每一类如何使用你的敏感路由,并为每个请求 Route 到合适的保证级别。
如果你想和我们的团队一起为你的应用设计这套方案,请申请早期访问。关于更详细的 SaaS 场景,请参见 SaaS Apps & Websites。
来源
- RFC 6749: The OAuth 2.0 Authorization Framework: 为客户端提供带范围的委托凭据
- RFC 9700: Best Current Practice for OAuth 2.0 Security: OAuth 部署的当前安全指导
- RFC 7009: OAuth 2.0 Token Revocation: 在不更改用户凭据的情况下撤销授权
- RFC 9470: OAuth 2.0 Step Up Authentication Challenge Protocol: 指明需要更强或更新鲜的认证
- RFC 8693: OAuth 2.0 Token Exchange: 委托所需的
act(actor) claim - MCP specification 2026-07-28: Authorization: 将 MCP server 视为 OAuth 2.1 resource server
- NIST SP 800-63B: Authentication and Authenticator Management: 重新认证指导
- W3C Web Authentication (WebAuthn) Level 3: passkey 提示背后的标准
- RFC 9421: HTTP Message Signatures: 已验证的 agent 签名能够确立什么