第 2 / 21 篇

第1章 从一次退款建立完整的信任边界

一笔看起来正确的调用

客服 Alice 在 Acme 租户处理订单 o-100。她让 Agent 退款十二元。Agent 找到工具 orders.refund,生成租户、订单号、金额和说明。工具返回成功。仅从对话和工具日志看,链路没有错误;如果订单其实属于 Beta,或者 Alice 已被移出退款岗位,这次成功就是事故。

正确的描述必须同时包含主体 Alice、执行程序 agent-demo、租户 Acme、对象 orders/o-100、动作 refund、金额一千二百分,以及执行时刻的授权版本。自然语言可以帮助生成这些字段,但这些字段的权威来源不同:主体来自验证后的身份,租户来自服务端成员关系,对象来自数据库,金额来自明确业务输入,审批来自受控审批记录。把它们全放进一个模型生成的 JSON,再因为 JSON 合法而放行,相当于把身份证和门禁规则都交给访客填写。

本书把链路拆成身份服务、工具代理和业务服务。身份服务签发有时限和受众的凭据,工具代理负责协议适配、输入约束与请求预筛选,业务服务负责对象授权、审批消费和状态修改。三个边界都通过真实 HTTP 连接;测试没有直接跳过网络调用授权函数。读者可以看到凭据真的被签名、传输、解析和验签,也能观察依赖服务不可用时的结果。

第一次运行

在仓库根目录执行:

go version
go test -race -v ./lab
go run ./lab

Go 版本要求为 1.24 或更新版本。第一次执行会下载 golang-jwt/jwt/v5,依赖版本由 go.mod 和 go.sum 约束。手动服务启动后,9101 是身份服务,9102 是工具代理,9103 是业务服务。三者均绑定 127.0.0.1。不要把这些公开开发凭据复制到公网环境。

另开终端获取教学凭据:

curl -s http://127.0.0.1:9101/login \
 -H 'Content-Type: application/json' \
 -d '{"Subject":"alice","Password":"public-alice-pass"}'

返回的是九十秒有效、面向 tool-proxy 的访问凭据。将返回值放在临时 shell 变量中,向 9102 的 /call 提交 {"tenant":"acme","kind":"orders","id":"o-100","action":"read"}。不要将令牌粘贴进问题单或版本库。自动化测试已经完成同样的操作,因此手动观察不是通过验收的唯一方式。

哪些事实必须由服务端确认

工具代理先验证 issuer、audience、签名、有效期、时间约束和主体,再访问身份服务确认当前 epoch。它只接受固定工具结构,不接受任意 URL、任意 HTTP 方法或模型提供的 Authorization。身份服务把代理受众凭据收窄成三十秒的业务受众凭据,保留主体与租户,并标明 actor。业务端独立验证业务受众、在线状态、租户、scope、对象存在性与审批。

失败案例是把 9101 登录得到的原始令牌直接发到 9103。它的签名完全正确,主体也是 Alice,但 audience 是 tool-proxy,因此业务服务拒绝。TestSignedHTTPBoundaries 同时验证代理正常读取成功与该直连失败。这个对比说明,认证成功从来不等于所有服务都应接受同一凭据。

可运行实验与边界

运行 go test -race -run TestSignedHTTPBoundaries -v ./lab。逐个阅读子测试:错误签发方、错误受众、过期、缺失到期时间、未来生效、超长生命周期、损坏签名与无签名算法。它们都从 HTTP 请求进入。测试通过证明当前实现拒绝这些样本,不证明它抵御所有密码学攻击。

实验只有四个业务对象、三个本地主体,没有数据库、消息队列和组织目录。内存互斥锁用于说明审批消费与状态修改的事务关系。后续章节会把这个关系翻译成数据库事务和幂等记录。不要把对象数量少理解成授权可以省略;对象少只是为了让每一次授权决定都能被完整观察。

练习:画出你现有 Agent 调用链,给每条边写下发送者、接收者、凭据受众、撤权来源和最终对象授权位置。如果某条边只写“内网可信”,把它当作待补充的设计事实。

用一次请求追踪责任链

可以在纸上记录一次只读请求的五个时间点:用户凭据签发、代理接收、委托凭据签发、业务授权、业务返回。对每个时间点标出谁掌握权威事实。签发时的租户关系不一定等于执行时,代理收到请求也不等于业务已经提交。把时间点分开,后面讨论撤权和重试时就不会混淆。

如果你在日志中只看到一个 request_id,还应确认它由谁生成、是否允许客户端覆盖、跨服务是否保持关联,以及一个请求重试是否会换 ID。追踪 ID 用于关联,不应被拿来证明权限。业务幂等键与追踪 ID 可以不同:同一业务操作的几次网络尝试应有不同尝试记录,但共享稳定业务引用。

这里还可以做一个简单对照:停止 9103,代理不能再返回伪造的业务成功;停止 9101,代理也不能因为现有令牌尚未到期就忽略当前状态。恢复服务后需要重新登录,且内存业务对象会回到初始状态。这些现象帮助区分网络成功、认证成功、授权成功和业务提交成功,四者不能压缩成一个“工具调用完成”。