AI Agent 身份实测:同一把 API Key 跑了三个月,换成 398 字节签名凭证后能查到谁干的

📘 教程 2026-11-04 约 17 分钟阅读

五个 agent 共用一把 API key 跑了三个月,一笔异常退款回查时,日志里只有同一串 key,换 key 又把这五个一起停掉。

💡 你将学到

五个 agent 共用一把 API key 跑了三个月,一笔异常退款回查时,日志里只有同一串 key,换 key 又把这五个一起停掉。

📜 目录

五个 agent 共用 .env 里的一把 API key。三个月后回查一笔越权退款,日志里只有同一串 key,分不清哪个 agent 发的、替谁发的;换 key 又把五个一起停掉。缺的是身份,权限本身没问题。

环境:Python 3.11.6、PyJWT 2.13.0、cryptography 49.0.0。签发跑 2000 次、校验 10000 次取均值,HTTP 走本机回环 200 次取中位。

一、三种凭证的开销

同一份权限(读订单加退款,代表用户 10492,5 分钟有效)用三种方式承载:

形式长度签发校验即时吊销定位到谁
静态 API key46 字符预生成0.1 µs能只到 key
HS256 JWT355 字符17.3 µs27.7 µs不能到 sub
Ed25519 JWT398 字符56.9 µs156.4 µs不能到 sub 与 act

单核换算:HS256 校验约每秒 36000 次,Ed25519 约 6400 次,差 5.6 倍。真实往返又是另一回事,API key 中位 0.67 ms、p95 0.82 ms,Ed25519 中位 1.02 ms、p95 1.49 ms,只差 0.35 ms;请求头从 57 字节变 408 字节,在 1.5 KB 的请求里看不出来。

50 rps 的网关,Ed25519 只用掉单核 0.8%。换来的好处是服务端只存公钥,泄露范围从所有下游缩到一个签名方,这对容易被抄进日志和 prompt 的 agent 凭证划算。

二、token 里写什么

claim实测值作用
isshttps://id.example.com只信一个签发方
subagent:refund-bot哪个 agent,不写人名
audbilling-api只给一个服务用
scopeorders:read orders:refund能做哪几件事
act{"sub":"user:10492"}替谁做的
jti16 字节随机串撤销与防重放
exp 与 iat相差 300 秒有效窗口

Ed25519 签名段固定 86 个 base64url 字符。单钥 JWKS 实测 146 字节,取回加解析 0.7 ms,缓存即可,不必每请求回源。

三、六种失败的原文

场景异常报错原文
过期ExpiredSignatureErrorSignature has expired
nbf 在未来 30 秒ImmatureSignatureErrorThe token is not yet valid (nbf)
aud 不符InvalidAudienceErrorAudience doesn't match
alg=noneInvalidAlgorithmErrorThe specified alg value is not allowed
公钥 PEM 当 HMAC 密钥InvalidKeyErrorThe specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
改 payload 的 scopeInvalidSignatureErrorSignature verification failed

后两条是经典攻击面,PyJWT 的算法白名单自己挡住了。生产事故多出在自己写验证时放开 algorithms,或调 decode 不传 audience。

HTTP 层同样是测出来的:缺凭证、key 错、过期、aud 不符都是 401,jti 已撤销是 403,只读凭证打退款接口返回 403 并给出 {"error":"scope_missing","need":"orders:refund","held":"orders:read","amount":180},换成带退款权限的凭证返回 {"ok":true,"refunded":180}。这条判断在代码里,agent 改不了。

四、撤销只有一个旋钮

签名发出就收不回,只能每请求查一次撤销表:内存集合 0.07 µs,SQLite 主键 1.7 µs,相对 156 µs 的验签等于免费。真正的代价是窗口,300 秒 TTL 在 50 rps 下能签出 15000 次请求,吊销最坏等 300 秒。压到 60 秒,窗口缩五分之一,刷新频率涨五倍。60 到 300 秒配撤销表是可用区间。

五、委托链每层 28 字符

根凭证 379 字符,act 为 {"sub":"user:10492"};子凭证 422 字符,act 为 {"sub":"agent:planner","chain":["user:10492"]}。叠 1、2、4 层分别是 443、471、527 字符,每层约 28 字符,成本可忽略。

要紧的是签发方裁剪子凭证的 scope。子 agent 申请 orders:read admin:*、父只有 orders:read orders:refund 时,实测抛 PermissionError: scope escalation: ['admin:*'] not held by parent。这条检查交给子 agent 自觉遵守,等于没有。

六、DPoP 加一层 253 µs

Bearer token 谁拿到谁用。DPoP 每请求附一张私钥签的证明,把 token 绑到密钥上。实测 JWK 指纹 43 字符,证明 417 字符,整条链路校验 252.7 µs/请求,3000 次循环。

防重放实测有效:同一张证明第一次被接受,第二次抛 ValueError: replay: jti seen;改 GET 抛 htm mismatch: GET;换域名抛 htu mismatch。代价是每请求多一次签名加验签,jti 缓存每请求一条,3000 请求就是 3000 条,必须设过期清理。

七、自己踩的两个坑

第一版把一条已撤销的 token 判成了 200。撤销表和活跃会话共用一个字典,jti 在里面就被当成有效。撤销集合必须独立且 deny 优先,否则等价于黑名单和用户表混用。

另一条过期 token 少了 kid 头,服务端回的是 {"error":"unknown_kid","kid":null} 而不是 token_expired,因为 kid 检查排在验签前面,过期判断没跑到。正确顺序是验签、判 exp 与 aud、最后查撤销,报错才指向真问题。

八、落地清单

每个 agent 一条实测建议
agent_idagent:<名字>,与人类账号分表
公钥与 kidEd25519,PEM 113 字节,可多把轮换
scope按服务拆,一个 agent 不超过 3 个
act用户 ID,逐跳传递
TTL60 到 300 秒
jti每请求唯一,进撤销表与重放缓存

标准不用自己发明:JWT 访问令牌是 RFC 9068,换发更窄权限的令牌走 RFC 8693 的 token exchange(act 声明出自这里),资源限定是 RFC 8707,发送方绑定证明是 RFC 9449。MCP 2025-06-18 的授权规范把这四件拼在一起:服务端按 OAuth 资源服务器实现,必须校验 aud,失效 401、权限不足 403,禁止把收到的 token 原样转给下游。

回到那台工单 agent。改完是每个 agent 一把 398 字节凭证,sub 写明自己、act 写明替谁、scope 只到退款、60 秒过期、撤销表独立。出事时日志里能读出三行:哪个 agent、替哪个用户、用了哪条权限。

延伸阅读:MCP 授权章节怎么读、agent 权限与人工确认清单、给 agent 加追踪、12 个 MCP 服务实测。

相关文章
2026-08-01
Embedding API价格对比:OpenAI vs Cohere vs Gemini vs Mistral
2026-08-01
LangGraph构建RAG智能体
2026-07-19
AI Agent市场大盘点:Coze/GPTs/AgentGPT等平台怎么选
2026-08-01
多智能体系统实例:8个真实部署
2026-07-26
自托管AI + n8n工作流自动化
2026-07-16
AI Agent知识库搭建:让Agent学会用你的文档回答问题

本站文章由编辑人工撰写,收录的工具均经过实测或公开资料核验。文中链接指向工具官网或 GitHub 仓库,仅作信息参考,不构成付费推广。

💬 评论 (0)

暂无评论,来说两句吧~

登录后评论