一次性批准令牌:挡住旧授权误发新版本的工程设计

自动化写操作里有一个容易被低估的竞态:人工批准的是 revision 3,真正执行时内容已经变成 revision 4。令牌没过期、用户也有权限,但这次写入仍然不该发生。
我更愿意把人工批准理解成一张"精确能力票据":它不是对某个用户的一段宽泛信任,而是对某个对象、某个版本、某份内容的一次性授权。
工程场景与决策冲突
常见方案只把 token + expires_at 存起来。优点是接入简单,任务失败后可以直接重试;缺点是令牌在有效期内可能跨对象、跨版本复用。它解决了"是否有人批准",却没有解决"批准的究竟是什么"。
更稳妥的记录需要五个字段:
python
@dataclass(frozen=True)
class Approval:
token_sha256: str
object_id: str
revision: int
content_sha256: str
expires_at: datetime
used_at: datetime | None = None
其中 revision 是业务并发身份,content_sha256 是内容字节身份。两者并存并不重复:旁路文件修改可能不递增 revision,而只有 SHA 又难以表达业务版本冲突。
方案拆解与关键权衡

完整链路至少分三层:
- 认证:当前主体是谁,身份保证等级是否足够。
- 授权:主体是否属于显式授权组,设备是否合规,动作与资源是否匹配。
- 单次批准:当前这份内容是否被明确确认,票据是否尚未消费。
"已经登录"只能回答第一层的一部分。高敏感写操作还需要更高保证等级、显式组成员资格和合规设备;人工批准则进一步收窄到本次具体内容。
实现链路与最小示例
创建批准时读取受管正文原始字节并计算 SHA-256,生成高熵随机 token,只持久化 token 摘要。消费接口一次接收全部当前事实:
python
def consume(*, token_sha256, object_id, revision,
content_sha256, consumed_at):
approval = load(token_sha256)
require(approval.used_at is None)
require(consumed_at < approval.expires_at)
require(approval.object_id == object_id)
require(approval.revision == revision)
require(approval.content_sha256 == content_sha256)
mark_used_atomically(token_sha256, consumed_at)
最后一步不能是普通的"先查后改"。以 SQLite 为例,可以先 BEGIN IMMEDIATE,再执行:
sql
UPDATE approval
SET used_at = ?
WHERE token_sha256 = ? AND used_at IS NULL;
只有影响行数为 1 才算消费成功。这样两个并发执行器即使同时通过前置读取,也最多一个能完成条件更新。
一个重要权衡:失败后不要盲目重放

一次性令牌会让网络超时后的恢复更麻烦:执行器不知道外部写入是否已经成功。此时放宽令牌复用看似省事,却重新打开重复写入风险。
更可靠的恢复路径是:先回读目标对象;若已经写入,收敛本地状态;若明确未写入,基于当前 revision 和正文 SHA 重新申请批准。批准消费成功只是授权事实,不等于外部写入成功;外部接口成功也不等于公开结果已经可见。
证据、限制与自动化边界
这套边界已在一个真实 Python 项目中落地:协议要求 token 摘要、对象 ID、revision、正文 SHA 和消费时间;SQLite 实现用立即事务与 used_at IS NULL 条件更新;测试覆盖错对象、错版本、错正文、过期和重复消费。
它仍有明确限制:
- 不能替代身份认证、RBAC 或设备合规策略。
- 不能解决外部系统本身缺少幂等键的问题。
- SHA 必须基于与执行器一致的原始字节与编码规则。
- 明文 token 不能进入日志、数据库或长期任务记录。
- 正文不应复制到批准表,摘要足够承担绑定职责。
可复用检查清单
- 批准是否绑定目标对象?
- 是否同时绑定业务 revision 和正文 SHA-256?
- 是否有明确过期时间?
- 是否只保存 token 摘要?
- 消费是否通过数据库条件更新保证原子性?
- 重复、过期、错对象、错版本、错正文是否分别有测试?
- 超时恢复是否先回读,而不是直接重放?
- 外部写入后是否还有公开结果回读?
收束
如果你的系统只能证明"某人曾经点过批准",却不能证明"他批准的是当前这份字节",那张令牌就仍然太宽。你在类似设计中会优先选择数据库条件更新、幂等键,还是工作流引擎的状态机?
发布前门禁
- 突出方案取舍,未堆砌通用鉴权概念
- 代码和并发边界来自已验证实现
- 未给出未经验证的效果数据
- 未授权平台公开发布