# 身份与鉴权 · 让 agent 以自己的身份替人行动 · Agent 实战 · 从零构建完备智能体

**作者** Aklman · **章节** 16 / 19 · **首发** 2026.07 · **语言** 中文为主, 中英双语
**原文** https://library.aklman.com/books/build-agent/identity
**全书 markdown** https://library.aklman.com/books/build-agent/llms.md
**上一章** https://library.aklman.com/books/build-agent/12-choosing/llms.md
**下一章** https://library.aklman.com/books/build-agent/adversarial/llms.md

> 本章来自 Aklman · Library, 完整免费阅读。允许 AI 摘读、引用、问答; 转载请保留作者署名与原文链接 (CC BY-NC-ND 4.0, https://library.aklman.com/license)。

---

> 企业里的第一个问题不是「它能做什么」，是「它以谁的名义在做」—— agent 需要自己的身份、一份受限的委托，而不是你的万能钥匙。

到上一章为止，你的研究助手一直用一把 ANTHROPIC_API_KEY 加一台机器的全部权限在跑 —— 本地开发这没问题。企业篇从这里开始：当它要在一家公司里、在别人的数据上行动，第一个要回答的问题不是「它能做什么」，是「它以谁的名义在做」。这一章给它一个自己的身份、一份受限的委托、和一张工具级的权限表 —— 全部落在工程里，能跑。

先把「企业级」这个词说清。企业级 agent 和「能跑的 agent」的差距，不是更强的模型，而是一整层工程：让它可信地、规模化地、在别人的数据上、留得下审计地行动。这一层有八根支柱 —— 身份鉴权、带权限的检索、对抗性安全、治理审计、常设评测、失败工程、控制平面、多租户。前三根各占一章（本章与接下来两章），其余的已经深化进第 4、10、11 章；最后的 capstone 把八根收成一个可交付的工程。

### I · 一把 API key 走天下，死在企业门口

个人项目里，agent 用你的 key、你的 cookie、你的数据库账号 —— 它就是你。企业里这个「就是你」恰恰是问题本身。

三个死法。**归因死**：审计日志里全是你的名字，没人分得清哪次查询是你点的、哪次是 agent 自己决定的 —— 出了事故无法追责，也无法只关掉 agent 的那部分。**爆炸半径死**：你的凭据能做的一切，被注入劫持的 agent 也能做 —— 一把万能钥匙意味着最坏情况就是全部。**离职死**：agent 挂在某个员工的账号下，那人离职销号，生产里的 agent 一起停摆。OWASP 的 Agentic Top 10 把这类问题单列成一条 ——ASI03「Identity and Privilege Abuse」。[^5]

解法的形状是确定的：**agent 要有自己的 workload identity** —— 像一个服务、而不是像一个人那样存在。这不用发明新轮子：SPIFFE 就是「在动态异构环境里安全标识软件系统」的开源标准，workload 通过 API 拿到短命的身份文件（SVID）去互相认证。[^4]在云上，对应物是 service account / managed identity。要点只有一个：agent 的身份独立于任何员工的身份存在，可以单独授权、单独吊销、单独审计。

---

### II · 委托，不是冒充

agent 有了自己的身份，第二个问题接上：它替用户干活时，是「变成」那个用户，还是「代表」那个用户？这两个词在标准里有精确的分界。

RFC 8693（OAuth 2.0 Token Exchange）的原文把线画得很清楚：**冒充**（impersonation）时，「A 在该上下文里与 B 不可区分」；**委托**（delegation）时，「A 仍保有自己独立于 B 的身份 —— 明确的是 A 在代表 B 行动」。委托靠 token 里的 `act`（actor）声明表达：「委托已经发生，行动方是谁」。[^1]对 agent，答案几乎总是委托 —— 审计里要能同时看到两个名字：替谁干（用户），谁在干（agent）。第一节的三个死法，全靠这两个名字分开来救。

协议层面，这套东西正在标准化成型。OAuth 2.1 把十年的安全最佳实践收编成一份规范（PKCE 全面要求、去掉 implicit flow）—— 注意它截至 2026-03 还是活跃草案，不是 RFC，引用要按草案对待。[^2]而最能说明「agent 世界怎么用它」的一手样本是 MCP：MCP 的授权规范整体是可选项 ——stdio 传输被建议直接从环境取凭据；一旦走 HTTP 并启用授权，措辞立刻变硬：授权服务器 **MUST 实现 OAuth 2.1**，客户端 MUST 用 PKCE、MUST 带 RFC 8707 的 `resource` 参数把 token 绑定到目标服务器；MCP 服务器 MUST 校验 token 是发给自己的，且 **MUST NOT 接受或转传任何其他 token**。[^3]最后这条禁令背后就是下一章的主角 ——confused deputy。

落到工程里，我们给研究助手加一格 authz.py：Principal 记三件事 —— agent 自己是谁（agent_id）、替谁干活（on_behalf_of）、属于哪个租户（tenant_id）。这就是委托语义的最小载体：

_authz.py（工程里可跑的真代码）：Principal 与只降权的 token broker_
```text

```python
@dataclass(frozen=True)
class Principal:
 agent_id: str = "research-agent@local"
 on_behalf_of: str = "dev@local" # 委托:替 TA 干活,但不是 TA(RFC 8693 act 语义)
 tenant_id: str = "local"
 scopes: frozenset[str] = frozenset({"search:read", "web:read", "sandbox:exec"})

@dataclass
class TokenBroker:
 """教学版:真实世界这是一次 OAuth token exchange(RFC 8693)。
 这里只保住两条不变量 —— 短命 + 只降权。"""
 ttl_s: float = 300.0
 def mint(self, principal: Principal, scopes: set[str]) -> dict:
 granted = sorted(scopes & set(principal.scopes)) # 交集 = 只能降权
 return {"token": "tk-" + secrets.token_hex(8),
 "act": principal.agent_id, # 谁在替人行动(可归因)
 "sub": principal.on_behalf_of, # 替谁行动
 "scopes": granted,
 "expires_at": round(time.time() + self.ttl_s, 1)}
```

```

> TokenBroker 是教学骨架：它不接真实的授权服务器，只演示两条在生产里同样成立的不变量 —— 凭据**短命**（以分钟计，不发长期钥匙）、换发**只降权**（granted 是交集，永远不会比持有者更宽）。接真实 IdP 时，把 `mint()` 换成一次 RFC 8693 token exchange 请求，接口形状不变。

---

### III · 每个工具一个 scope，密钥不进上下文

第三章说过「工具的 schema 就是它的权限」—— 那是能力面。企业里还差身份面：同一个工具，这个身份能不能用。两张表相乘，才是完整的权限模型。

工程里这是一张 `TOOL_SCOPES` 表加一个 `check_scope()`：每个工具登记一个 scope，未登记的默认拒绝；循环在执行任何工具前先查它 —— 排在第九章那道白名单闸的前面。研究助手只有三个只读/沙箱 scope（`search:read`、`web:read`、`sandbox:exec`），一个只拿到前两个的 Principal 跑起来，会看到 `run_python` 被当场拒绝、拒绝原因进审计。这就把第九章「最小授权」的口号变成了可以按身份配发的东西：不同用户、不同租户、不同任务，发不同的 scope 集合，而工具代码一行不改。

_loop.py 里的接线：鉴权在白名单之前，拒绝有原因、进审计_
```text

```python
why = check_scope(principal, name) # 身份面:这个 Principal 能用这个工具吗
if why:
 return deny(tid, name, args, why) # 拒绝原因 emit 给用户 + 写进审计链
if not check_allowed(name, cfg): # 能力面:这个部署暴露这个工具吗(ch9)
 return deny(tid, name, args, f"工具 {name} 不在白名单")
```

```

最后是密钥本身。规矩一条：**密钥不进模型的上下文** —— 不进 system、不进消息、不进工具描述。上下文会被 trace 记录、被压缩摘要、被 llms 导出，写进去的密钥等于写进了日志。工程里 `ANTHROPIC_API_KEY` 走环境变量、模型层懒加载，工具拿到的是执行结果而不是凭据；MCP 的 stdio 传输给的建议是同一句话 —— 从环境取凭据。[^3]生产里再加两条：密钥放专门的 secret manager（不进代码库），并让第二节的 broker 只发短命凭据 —— 泄露的钥匙五分钟后是废铁，和泄露一把永久钥匙是两种事故。

动手 · 让鉴权真的拒绝一次：

- **拿一个降权的 Principal 跑一遍**:
 在 `config.py` 里把 `principal_scopes` 改成 `("search:read", "web:read")`，离线跑一遍 —— 看 `run_python` 被拒、任务照样体面收尾（错误回传，ch3 的老朋友）。

- **用 broker 换一次「只降权」的 token**:
 在 REPL 里 `TokenBroker().mint(principal, {"web:read", "admin:write"})` —— 确认拿回来的 scopes 里没有 `admin:write`：交集语义，升权在结构上不可能。

- **搜一遍你自己 agent 的上下文里有没有密钥**:
 把你项目里的 system prompt、工具描述、trace 输出全文搜一遍 `sk-`、`Bearer`、`BEGIN PRIVATE KEY` —— 搜到任何一个，就是这一节要你先修的洞。

> 企业问的第一个问题不是它多能干，
> 是它以谁的名义在干。

## 引用与参考

01 · IETF · RFC 8693「OAuth 2.0 Token Exchange」—— act（actor）声明「表达委托已经发生，并标识被委托了权限的行动方」；委托与冒充的区分原文：冒充时「A 在该上下文里与 B 不可区分」，委托时「A 仍保有自己独立于 B 的身份，明确的是 A 在代表 B 行动」。  (IETF · RFC 8693 (Token Exchange))
02 · IETF OAuth WG · draft-ietf-oauth-v2-1 —— OAuth 2.1 把 OAuth 2.0 的安全最佳实践收编成一份规范（PKCE 全面要求、去掉 implicit 等）。注意它截至 2026-03 仍是活跃草案（-15，2026-03-02），尚未成为 RFC —— 引用时按草案对待。  (IETF · OAuth 2.1 (draft))
03 · MCP 规范（2025-11-25）· Authorization —— 授权对 MCP 是可选项；HTTP 传输「SHOULD 遵循本规范」、stdio 传输「SHOULD NOT，改从环境取凭据」。采用时：授权服务器「MUST 实现 OAuth 2.1」（基于 draft-ietf-oauth-v2-1-13），客户端 MUST 实现 PKCE、MUST 带 RFC 8707 resource 参数；MCP 服务器「MUST 校验 token 是专门发给自己的」且「MUST NOT 接受或转传任何其他 token」。截至 2026-07。  (MCP · Authorization (2025-11-25))
04 · SPIFFE —— 「Secure Production Identity Framework for Everyone，一组在动态异构环境里安全标识软件系统的开源标准」。workload identity 的既有标准：workload 通过 API 拿到短命的 SVID 身份文件去互相认证 —— agent 的「自己的身份」不必发明新轮子。  (SPIFFE · Overview)
05 · OWASP · Top 10 for Agentic Applications 2026（2025-12-09 发布）—— ASI03 即「Identity and Privilege Abuse」：agent 的身份与权限被滥用，是官方清单里单列的一条。以官方 PDF 为准。  (OWASP · Agentic Top 10 (2026))

---

*身份与鉴权 · 让 agent 以自己的身份替人行动 · Agent 实战 · 从零构建完备智能体 · Aklman 著 · CC BY-NC-ND 4.0 · https://library.aklman.com/books/build-agent/identity*
