トークンは必ず期限が切れる
一言でいうと
アクセストークンは、短く生きて死ぬのが正常なので、クライアントは、期限が切れる前にあらかじめ更新し、時計のずれの分だけ余裕を持たせ、401を受け取ったらたった1回だけ更新して再試行し、ワーカーが複数いても、更新は1回だけ起きるようにしなければなりません。
なぜ必要なのか
「夜間バッチが毎回2、3件ずつ401で失敗します」という報告をたどると、原因は、ほとんどの場合、同じです。トークンを一度受け取っておいて、401が来たらそのとき新しく受け取る構造です。この構造は、期限が切れる瞬間ごとに、必ず1回は失敗します。その失敗がユーザーのリクエストなら、ユーザーの目に触れます。
トークンが短く生きるのは、欠陥ではなく設計です。RFC 6749は、アクセストークンの寿命を短く置き、リフレッシュトークンで再び受け取る構造を定義し、RFC 6750は、そのトークンをAuthorization: Bearerで載せて送る方法と、失敗したときのWWW-Authenticate応答を定めています。寿命が短ければ、トークンが漏れたときに使える期間が短くなります。それが目的です。
どう動くのか
トークンの中を読むことと、信じることは、違います。JWT(RFC 7519)は、ドットで区切られた3つの断片で、前の2つの断片は、暗号ではなく、base64urlでエンコードされた平文です。誰でも開いて見られ、誰でも書き換えられます。そのため、クライアントがexpを読んで「いつ更新するか」を決めるのはかまいませんが、その値を根拠に権限を判断してはいけません。署名の検証は、リソースサーバーの仕事です。
claimのうち、時間に関するものは3つです。iat(発行時刻)、nbf(この時刻より前には使わないこと)、exp(この時刻以後には使わないこと)。RFC 7519は、expとnbfを扱うときに、小さな時計のずれを考慮する余裕(leeway)を置いてもよいと書いています。サーバーとクライアントの時計が数秒ずれるのはよくあることで、余裕がなければ、その数秒のために、受け取ったばかりのトークンが「まだ有効ではない」として拒否されます。
更新は、期限が切れる前に行います。ルールは1行です。남은 시간 <= 여유(韓国語で「残り時間 <= 余裕」を意味する式です)なら、今すぐ更新します。余裕を大きく取ると、更新が頻繁になり、小さく取ると、期限が切れる瞬間に引っかかる危険が大きくなります。寿命の10%から20%の間が、よくある出発点です。
401は、1回だけ受け取ります。あらかじめ更新しても、401は来ることがあります。サーバーがトークンを先に失効させたか、権限が変わったか、時計が大きくずれたときです。このときは、更新して、たった1回だけ、もう一度かけます。ここで回数の制限を置かないと、ループができます。問題がトークンではなく権限である場合(本当は403であるべき状況を、401で答えるサーバーが多いのです)、更新しても401が続き、クライアントは、無限にトークンを受け取りながら、認証サーバーを叩き続けます。
更新の殺到(stampede)を防ぎます。ワーカー20個が同じトークンを共有していると、期限が切れる瞬間に、20個が同時に「更新しなければならない」と判断します。認証サーバーから見れば、普段の20倍のリクエストが一度に来ることであり、そのサーバーが遅くなると、更新がさらに長くかかって、より多くのワーカーが押し寄せます。解決策は、更新を1回だけにすることです。
토큰 필요
│
├─ 캐시를 본다 ── 아직 쓸 만한가? ── 예 ─▶ 그대로 쓴다 (대부분 여기서 끝난다)
│ 아니오
└─ 잠금을 잡는다 ──▶ 캐시를 **다시** 본다 ── 남이 갈아 뒀는가? ── 예 ─▶ 그것을 쓴다
아니오 ─▶ 내가 갱신한다
このコードブロックの韓国語は、トークンが必要、キャッシュを見る、まだ使えるか、はい、そのまま使う(大部分はここで終わる)、いいえ、ロックを取る、キャッシュをもう一度見る、他が更新したか、それを使う、自分が更新する、という分岐のラベルです。
ロックを取ったあとにもう一度確認することが、核心です。待っている間に、ほかのワーカーがすでに更新しているかもしれないからです。この1行を抜くと、ロックは順番に並べるだけで、更新の回数はそのままです。
現場での姿
1つ目、401と403を混ぜて使うサーバーが多いです。権限が足りない状況に401を返すと、クライアントは「トークンの問題だ」と考えて、更新を繰り返します。こちら側では、再試行の回数を1に縛ることが、唯一の防御です。
2つ目、トークンをプロセスのメモリにだけ置いています。ワーカーがプロセス単位で散らばっていると、キャッシュを共有できず、プロセスの数だけ発行が起きます。発行の回数に制限があるパートナーなら、それだけで障害になります。
3つ目、時計を合わせていません。コンテナの時計が30秒ずれていると、余裕をどれだけうまく取っても、ずれた側に押されます。401が特定のノードでだけ起きるなら、そのノードの時計をまず見ます。
4つ目、トークンがログに残っています。アクセストークンは、それ自体が認証情報です。リクエストヘッダーを丸ごと出力するデバッグログが残っていると、そのログを読める人全員が、そのAPIを呼べます。
次のラボですること
短い寿命のトークンを発行する認証サーバーを起動して、トークンを1つ受け取り、その中を開いて、iat・nbf・expを読みます。期限切れ・まだ有効でない・期限間近・正常の4つの分岐を、時計のずれの余裕まで考慮して見分ける判定器を作り、その判定で、期限が切れる前にあらかじめ更新しながら呼び出し続けるクライアントを作ります。そのあと、何を持っていっても401を返すサーバーに対して、再試行が1回で止まるかを確認し、ワーカー6つが同時に始まっても、発行が1回だけ起きるようにします。最後に、これらの値を、ポリシー表として書きます。