TT Lab
开始
学习 学习路径 课程

集成与部署

夜间批处理每次都有几笔死在 401

在 TT Lab 中继续学习

目标

制作一个带着短寿命访问令牌、持续调用别人 API 的客户端。在过期之前提前刷新,留出时钟误差的余量,只接受一次 401 并在刷新后重试,并且即使有多个工作进程,刷新也只发生一次。

为什么重要

“夜间批处理每次都有两三笔死在 401 上”,原因几乎总是相同的。先拿到一次令牌、等来了 401 才重新获取的结构,每到过期的瞬间,一定会失败一次。 令牌短命不是缺陷,而是设计。寿命必须短,一旦泄露,可被利用的时间才会短。所以需要修改的一方不是服务器,而是我们的客户端。 而且在修改时要同时关注三点。时钟不可能完全一致,所以需要余量;401 也可能不是令牌的问题,所以重试次数需要上限;如果有多个工作进程,刷新会在过期的瞬间一起涌来,所以必须把刷新合并为一次。 评分器不会相信你写的句子。评分器会把认证服务器启动在评分器所选的端口上,在服务器一侧统计签发次数和资源调用次数,核对你的客户端实际刷新了几次、重新发起了几次。

步骤

  1. 创建 /root/token/authsrv.py 并在端口 8015 上启动,获取一个令牌并保存到 /root/token/token.json。
  2. 创建 /root/token/decode.py,读取令牌内部的头部和负载,写入 /root/token/claims.json。
  3. 创建 /root/token/should_refresh.py,让它连同时钟误差的余量一起,区分出已过期、尚未生效、即将过期、正常这四种情况。
  4. 创建 /root/token/client.py,让它在过期之前提前刷新,即使多次调用,也一次 401 都不出现。
  5. 让 client.py 收到 401 时,刷新之后只重新发起一次。
  6. 让 client.py 即使多个工作进程同时启动,令牌的签发也只发生一次。
  7. 在 /root/token/token_policy.json 中写出策略表。
  8. 在 /root/token/token_report.md 中分四节进行汇报。

参考

拿一个短命令牌试试

创建 /root/token/authsrv.py 并在端口 8015 上启动,通过 POST /oauth/token 获取一个令牌,并把整个响应保存到 /root/token/token.json。

JWT 是把以 base64url 编码的头部、负载和 HMAC 签名用点连接起来的东西。只用 Python 标准库的 hmac、hashlib、base64 就能制作。如果由服务器统计签发数和资源调用数,之后就能确认客户端实际刷新了几次。

打开令牌看看里面

创建 /root/token/decode.py,使它从通过 --in 接收的令牌响应中取出 JWT,读取头部和负载,并通过 --out 把 {"header": ..., "payload": ..., "ttl_s": exp - iat} 写入 /root/token/claims.json。

JWT 的前两段不是密文,而是以 base64url 编码的明文。base64url 会去掉填充(=),所以解码之前要把长度补成 4 的倍数。请记住,这里做的是读取,不是校验——不能依据这些值来判断权限。

什么时候该更换

创建 /root/token/should_refresh.py,使它通过 --exp·--now·--skew-s·--nbf 进行判定。判定顺序是 nbf、过期、即将过期,reason 为 not_yet_valid·expired·near_expiry·ok。即将过期的令牌仍然可以使用,所以 valid 为 true。

时钟不可能完全一致。nbf 要在我们这一侧留出余量来看,刚拿到的令牌才不会被以“尚未生效”拒绝。即将过期的判定,只需要一行 남은 시간 <= 여유(韩文,意为“剩余时间 <= 余量”)。remaining_s 在过期之后必须是负数。

在过期之前提前更换

创建 /root/token/client.py,使它调用 --calls 次资源,每次都确认令牌是否即将过期,如果即将过期就先刷新。即使调用的时间比令牌寿命更长,unauthorized 也必须是 0。

把上一步的判定器作为模块引入使用,规则就只会存在于一个地方。把令牌保存在文件里,在调用之前问一句“这个还能用吗”。用寿命 3 秒的令牌连续调用超过 3 秒,就可以用服务器的签发数来确认刷新是否真的发生了。

401 只接受一次

让 client.py 收到 401 时刷新,并且只重新发起一次。如果刷新之后仍然是 401,该次调用就必须以失败结束。必须能够通过 --start-expired 带着已过期的令牌开始。

有许多服务器会在权限不足时返回 401。在这样的服务器上,如果重试次数没有上限,刷新和 401 就会无休止地重复,不停地冲击认证服务器。请用服务器的调用数确认,每次调用的资源请求是否恰好只发出了两次。

过期瞬间,二十个一起刷新

让 client.py 即使通过 --workers 同时进行多个调用,令牌的签发也只发生一次。要使用缓存文件和锁,并且在获取锁之后必须再确认一次缓存。

只加锁的话,刷新只是在排队发生,次数并没有变。等待期间可能别人已经更换好了,所以请在获取锁之后重新读取缓存,如果有可用的令牌就用它。在 Python 中使用 fcntl 的文件锁。

用策略表固定下来

在 /root/token/token_policy.json 中写入 token_ttl_s·refresh_skew_s·max_retry_on_401·cache_path·stampede_guard·clock_sync·notes。refresh_skew_s 至少为 1 且必须小于寿命,max_retry_on_401 不超过 1。

请用一行在 notes 中留下这些值的依据。写明余量取了寿命的百分之几,以及为什么把重试限制为 1,就可以了。这张表也是一份文档,在合作方更改了寿命时,它会告诉我们需要同时改什么。

令牌检查报告

在 /root/token/token_report.md 中分为 ## 토큰이 어떻게 생겼나 ## 언제 갈아야 하나 ## 401 을 받으면 무엇을 하나 ## 워커가 여럿일 때 四节来写(韩文,依次意为“令牌长什么样”“什么时候该更换”“收到 401 怎么办”“有多个工作进程时”)。claims.json 和 token_policy.json 中的值必须写进正文。

读者是那个在问夜间批处理为什么会死在 401 上的人。不要以“因为令牌过期了”收尾,请用数字表明:过期是正常的,问题在于我们处理过期的方式。