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

集成与部署

令牌本来就会过期

在 TT Lab 中继续学习

一句话总结

访问令牌短命才是正常的,所以客户端必须在过期之前提前更换,按时钟误差留出余量,收到 401 时只刷新一次并重试,而且即使有多个工作进程,刷新也要只发生一次。

为什么需要它

顺着“夜间批处理每次都有两三笔死在 401 上”的投诉追下去,原因几乎总是相同的。结构是先拿到一次令牌,等来了 401 才重新获取。这种结构每到过期的瞬间,一定会失败一次。如果那次失败是用户请求,用户就会看到。

令牌短命不是缺陷,而是设计。RFC 6749 定义了把访问令牌的寿命设短、再用刷新令牌重新获取的结构,RFC 6750 则规定了以 Authorization: Bearer 携带该令牌的方法,以及失败时的 WWW-Authenticate 响应。寿命必须短,令牌一旦泄露,可被利用的时间才会变短——这就是目的。

工作原理

读取令牌内部,与信任它,是两回事。JWT(RFC 7519)是用点分隔的三段,前两段不是密文,而是以 base64url 编码的明文。任何人都可以打开看,任何人都可以修改。所以客户端读取 exp 来决定“什么时候更换”没有问题,但不能依据这个值来判断权限。签名校验是资源服务器的事。

在 claim 中,与时间有关的有三个。iat(签发时刻)、nbf(在此时刻之前不得使用)、exp(在此时刻之后不得使用)。RFC 7519 写道,处理 exp 和 nbf 时可以留出容纳小幅时钟误差的余量(leeway)。服务器与客户端的时钟相差几秒很常见,如果没有余量,就会因为这几秒,让刚拿到的令牌被以“尚未生效”拒绝。

在过期之前就更换。规则只有一行。남은 시간 <= 여유(韩文,意为“剩余时间 <= 余量”)成立时,现在就去更换。余量设得大,更换就会频繁;设得小,则在过期瞬间被卡住的风险会变大。寿命的 10% 到 20% 是常见的起点。

401 只接受一次。即使提前更换,也仍可能收到 401——比如服务器提前作废了令牌、权限变了,或者时钟相差很大。这时更换之后,只重新发起一次。如果在这里不限制次数,就会形成循环。如果问题不在令牌而在权限(许多服务器会在本该返回 403 的情况下返回 401),更换之后仍然是 401,客户端就会无限地获取令牌,不停地冲击认证服务器。

防止刷新风暴(stampede)。如果 20 个工作进程共享同一个令牌,在过期的瞬间,这 20 个会同时判断“需要更换”。对认证服务器来说,就是平时 20 倍的请求同时涌来,而该服务器一旦变慢,更换就会耗时更长,从而有更多工作进程涌入。解决办法是让刷新只发生一次。

토큰 필요
  │
  ├─ 캐시를 본다 ── 아직 쓸 만한가? ── 예 ─▶ 그대로 쓴다  (대부분 여기서 끝난다)
  │                                   아니오
  └─ 잠금을 잡는다 ──▶ 캐시를 **다시** 본다 ── 남이 갈아 뒀는가? ── 예 ─▶ 그것을 쓴다
                                                              아니오 ─▶ 내가 갱신한다

关键在于获取锁之后再确认一次。因为在等待期间,其他工作进程可能已经更换好了。漏掉这一行,锁就只是让大家排队,刷新的次数并没有减少。

在现场相遇的样子

第一,许多服务器会混用 401 和 403。权限不足时却返回 401,客户端就会以为“是令牌有问题”而反复更换。在我们这边,把重试次数限制为 1,是唯一的防御。

第二,把令牌只放在进程内存中。如果工作进程是按进程分散的,就无法共享缓存,有多少个进程就会签发多少次。如果合作方对签发次数有限制,这本身就会成为故障。

第三,没有校准时钟。如果容器的时钟相差 30 秒,余量无论设得多么合理,也会被推向偏差的那一侧。如果 401 只在特定节点上出现,就先检查那个节点的时钟。

第四,令牌会留在日志里。访问令牌本身就是凭据。如果遗留着把整个请求头都打印出来的调试日志,那么所有能读到这份日志的人,都可以调用那个 API。

下一项实验要做什么

启动一台签发短命令牌的认证服务器,获取一个令牌,并打开它读取 iat·nbf·exp。制作一个判定器,把已过期、尚未生效、即将过期、正常这四种情况,连同时钟误差的余量一起区分出来,再用这个判定制作一个在过期之前提前更换并持续调用的客户端。然后对着不论拿什么都返回 401 的服务器,确认重试是否在一次之后停下,并让六个工作进程即使同时启动,签发也只发生一次。最后把这些值写成策略表。