第11章 撤权需要明确生效时刻与一致性
JWT 没过期,不代表权限仍然有效
员工在十点被移出岗位,九点五十九分签发的令牌还剩十五分钟有效。若业务服务只做离线验签,这十五分钟内旧令牌通常仍会通过。这不是 JWT 失效,而是系统选择了离线验证的一致性模型。企业需要明确可以接受的撤权窗口,并为高风险动作增加在线判断。
本书为每个主体维护 epoch。凭据携带签发时版本,身份服务 /state 返回当前版本、租户与 active 状态。撤权操作将版本加一,所有旧版本凭据随即在下一次在线检查时失败。审批也绑定 epoch,因此用户重新登录得到新凭据后,不能复活旧审批。该机制适合说明“权限变化使旧授权失效”的版本模型。
版本递增不等于删除账号。本实验撤权后 Alice 可以重新登录并获得新版本,这是为了分别观察旧凭据失效与旧审批失效。真实离职通常还需要禁用账号、撤销登录会话、刷新令牌和相关服务授权。实验 Person 结构保留 Disabled 字段,但未提供完整账号生命周期管理界面。
即时撤权的准确含义
实验在每次授权边界向身份服务查询状态,因而撤权后的新检查会拒绝旧版本。它不能保证已经通过检查、正在提交的请求被时间倒流取消。身份状态检查与业务事务分属不同服务,中间存在竞态窗口。若要求“撤权提交之后绝无任何旧权利提交”,需要更强的协调机制,例如在业务提交事务中读取同库授权版本,或使用可验证的租约与提交栅栏。
因此本书使用“在检查边界生效”描述撤权,而不承诺跨服务绝对即时。对低风险读取,可以接受有限缓存;对大额退款,应根据业务要求决定是否执行强一致授权提交。架构评审时必须把这个窗口画在时序图上,否则每个团队都可能以为另一个团队已经解决。
失败案例是身份服务故障时,订单服务回退到“只要签名有效就通过”。正常时系统宣传即时撤权,事故时却自动扩大旧令牌有效期。实验选择故障关闭:无法获取 JWKS 或当前状态就拒绝。它牺牲部分可用性来保持声明的边界。生产可以使用短缓存,但必须明确最大陈旧时间与降级权限。
撤销哪些东西
主体权限、服务身份、审批、任务、会话和签名密钥有不同撤销粒度。用户退出一个浏览器会话,不一定需要撤销同一用户所有后台任务;私钥泄漏则可能要求停用一整批凭据。统一使用“注销”描述所有动作,会让实现无法精确响应事故。
建议建立撤销矩阵:事件是离职、调岗、设备丢失、服务泄漏、审批撤回还是密钥泄漏;对象是主体版本、会话、任务授权、单个审批、客户端密钥还是签名键;接收者包括代理、业务和异步执行器;每一格写明传播方式与最大窗口。矩阵应成为运行手册,而不仅是设计文档。
实验与练习
运行 go test -race -run 'TestApprovalExpiryRevocationAndConcurrency|TestKeyRotationAndDependencyOutage' -v ./lab。撤权子测试验证旧代理令牌、旧业务令牌都失败,重新登录得到的新凭据也不能使用旧审批。依赖故障测试关闭身份 HTTP 服务,确认没有放行。
练习:假设你的授权缓存存活三十秒,异步任务每分钟领取一次,业务事务最长五秒。请写出用户撤权后的最坏可执行窗口,并说明哪些阶段可以缩短、哪些需要改变架构。不要把三十秒缓存 TTL 直接当成整个系统的撤权上限。
授权版本如何传播
小系统可以在线查权威状态,大系统往往使用事件传播版本。事件可能重复、乱序或延迟,因此缓存更新应保证版本单调递增,不能让晚到的旧事件把 epoch 降回去。缓存失联时的最大可接受年龄也需要可测量,不能只监控事件队列有没有消息。
对于跨地域系统,撤权提交地与执行地可能暂时看到不同状态。可以让高风险提交回到权威区域,也可以使用带期限授权租约,把可用性与窗口写进契约。若产品要求全球立即一致,成本和延迟应由架构明确承担,而不是在文档里写一个“实时”就结束。
还要防止版本在账号恢复或数据迁移时回退。若数据库备份把 epoch 从十恢复成九,旧令牌可能重新被接受。生产恢复流程应保留撤权水位、使用不易回退的版本或对恢复后主体整体提升版本。安全状态也是需要备份与恢复设计的数据,不只是临时缓存。