こんにちは!Web APIの世界へようこそ。インフラアーキテクトの視点から、日々のネットワークを行き交うデータたちのドラマをわかりやすくお伝えしている筆者です。
APIの設計をしていると、ユーザーの認証やデータのやり取りで「JWT(JSON Web Token)」という言葉に必ずと言っていいほど出会いますよね。「なんだか暗号化された難しそうな文字列だな…」と感じていませんか?
今回は、そのJWTの中身である「ペイロード」に格納される、トークンの安全性を守るための重要人物たち――標準クレームである sub、iat、nbf、jti にスポットを当ててみましょう。特に、悪意ある攻撃からシステムを守る jti(JWT ID)の役割に迫ります。
難しい専門用語はちょっと置いておいて、まずは身近な世界に置き換えて一歩ずつ理解していきましょう!
—
1. JWTとペイロードってなに?(現実世界の例え)
突然ですが、みなさんはテーマパークの「1日パスポート」や、ホテルの「電子ルームキー」を使ったことはありますか?
あれは、チケット売り場(認証サーバー)で身分を証明して、「この人は1日中アトラクションに乗っていいですよ」という証明書をもらう仕組みですよね。一度それを受け取れば、いちいちチケット売り場に並ばなくても、各アトラクションの入り口(APIサーバー)でそのパスポートを見せるだけでスイスイ入れます。
Webの世界におけるこの「パスポート」が JWT です。
そして、JWTの中身は大きく3つに分かれていますが、その中でも「誰に、いつまで、どんな権限で発行されたか」という重要情報が書かれている身分証明書のメインスペースを ペイロード(Payload) と呼びます。
このペイロードの中に書かれる項目(クレーム)には、お行儀よく動くための「お約束のルール(標準クレーム)」が存在します。それが今回主役となる sub、iat、nbf、jti たちです。
—
2. 標準クレームの役割を紐解く
それでは、パスポートに記載されている4つの大切なスタンプ(標準クレーム)について、ひとつずつ優しく見ていきましょう。
sub(Subject / サブジェクト): 「誰の証明書?」
これは、このトークンが「誰のものか」を表すIDです。
例えば、オンラインショップの会員ID(例:user_12345)などがここに書かれます。APIサーバーはこの sub を見ることで、「あ、さっきログインしてくれた山田さんだな」と認識できます。
iat(Issued At / 発行日時): 「いつ作られた?」
これは、パスポートが「いつ発行されたか」というタイムスタンプ(Unixエポック秒)です。
「この証明書は、2023年10月1日 12:00:00 に作られましたよ」という記録ですね。これが古すぎないかをチェックすることで、トークンの鮮度を保ちます。
nbf(Not Before / 有効開始日時): 「いつから使える?」
英語をそのまま訳すと「〜より前ではない」となります。つまり、「この日時が来るまでは使っちゃダメですよ」という解禁日時です。
例えば、未来のチケットや、特定の時間から始まるセール用のトークンを発行するときに、「今日の15:00から有効にしてね」と指定するために使われます。
jti(JWT ID / JWTの固有ID): 「世界にひとつだけの番号」
そして今回のハイライトがこの jti です。これは、発行されたトークン一枚一枚に割り振られる「シリアルナンバー(固有のID)」です。
人間でいう「マイナンバー」や、映画のチケットに必ず書いてある「座席・バーコード番号」のようなものですね。同じ内容のトークンであっても、発行されるたびにこの jti の値はランダムかつユニーク(一意)なものに変化します。
—
3. なぜ jti が必要なのか?(リプレイ攻撃の脅威)
「ただのIDなら、他のクレームだけで十分じゃないの?」と思われるかもしれませんが、ここでネットワークの裏側で待ち受ける恐ろしい脅威のお話をさせてください。
それが 「リプレイ攻撃(再送攻撃)」 です。
郵便配達の例えで考えてみる
想像してみてください。あなたが銀行にお金を振り込むための「お墨付きの指示書」を、信頼できるメッセンジャー(通信)に託して送ったとします。
もし、そのメッセンジャーの道すがら、悪意ある盗聴者がその指示書をそっくりそのまま写真に収めてコピーしてしまったらどうなるでしょう?
盗聴者は、あなたが送ったあとに、その「コピーされた全く同じ指示書」を何度も何度も銀行に持ち込んで、「お金をこっちの口座に振り込んで!」と不正に要求できてしまいますよね。これがリプレイ攻撃の仕組みです。
Webの通信も同じです。悪意ある第三者が、正規のユーザーが送ったAPIリクエスト(JWTを含む)を途中で盗み見し、後から全く同じリクエストを何度もサーバーに送りつけて不正な操作を行おうとします。
jti はどうやってこれを防ぐのか?
ここで jti の出番です!
APIサーバー側で、受け取ったJWTに含まれる jti の値を、一度使ったかどうか記録(キャッシュやデータベースで管理)しておく仕組みを作ります。
1. ユーザーがリクエストを送信する(jti: "abc-xyz-789")。
2. APIサーバーは「お、この jti は初めて見るな。処理を実行しよう」と許可し、jti: "abc-xyz-789" を使用済みリストに登録する。
3. 悪意ある攻撃者が、同じリクエスト(jti: "abc-xyz-789")をもう一度送ってくる。
4. APIサーバーは使用済みリストを確認し、「あれ?この番号のチケット、さっきもう使われたぞ!」と気づき、即座にリクエストを拒否(ブロック)する!
このように、jti は「一度使われたチケットの使い回し(二重使用・リプレイ攻撃)」を完璧に見破るための、心強い番人なのです。
—
4. 実装のイメージを見てみよう
「理屈は分かったけれど、実際にどうやってコードに書くの?」という方のために、Python(PyJWTライブラリ)を使った非常にシンプルな発行と検証のイメージを見てみましょう。実務でもそのまま参考になるようにコメントを添えています。
import time
import jwt
import uuid
# 秘密鍵(実際には環境変数などから安全に読み込みます)
SECRET_KEY = "super-secret-key-donot-share"
def create_user_token(user_id: str) -> str:
"""
ユーザーのログイン成功時にJWT(パスポート)を発行する関数
"""
current_time = int(time.time())
payload = {
"sub": user_id, # 誰のトークンか(ユーザーID)
"iat": current_time, # 発行日時(今)
"nbf": current_time, # 有効開始日時(今すぐ)
"exp": current_time + 3600, # 有効期限(1時間後)
"jti": str(uuid.uuid4()) # ★世界にひとつだけの固有ID(jti)を付与!
}
# トークンを署名して生成
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return token
# --- 使用例 ---
my_token = create_user_token("user_999888")
print(f"生成されたJWT:\n{my_token}")
サーバー側でこのトークンを受け取ったときは、次のような処理を行います。
def verify_and_check_replay(token: str, used_jti_cache: set) -> bool:
"""
APIリクエストを受け取った際に、JWTの検証とjtiの重複チェックを行う関数
"""
try:
# 1. 署名の検証と期限(exp, nbf, iat)の自動チェック
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# 2. jtiの抽出
token_jti = payload.get("jti")
if not token_jti:
print("エラー: jtiが含まれていません。不正なトークンです。")
return False
# 3. リプレイ攻撃の防止(jtiがすでに使われていないかチェック)
if token_jti in used_jti_cache:
print(f"警告: リプレイ攻撃を検知しました!すでに使用されたjtiです: {token_jti}")
return False
# 4. 問題なければ、このjtiを使用済みとしてキャッシュに保存(※有効期限に合わせてキャッシュから自動削除する設計にします)
used_jti_cache.add(token_jti)
print(f"認証成功!ようこそ ユーザー: {payload['sub']} さん")
return True
except jwt.ExpiredSignatureError:
print("エラー: トークンの有効期限が切れています。")
return False
except jwt.InvalidTokenError:
print("エラー: 無効なトークンです。")
return False
# 簡易的な使用済みjtiのキャッシュ置き場(実際にはRedis等のインメモリDBを使用します)
cache_db = set()
# テスト実行
# verify_and_check_replay(my_token, cache_db) # 1回目は成功
# verify_and_check_replay(my_token, cache_db) # 2回目は「リプレイ攻撃検知」で弾かれる!
—
5. まとめ
今回は、JWTのペイロードを彩る標準クレーム(sub、iat、nbf、jti)について、現実世界の仕組みや郵便配達の例えを交えながら解説しました。
subで「誰の証明書か」を示し、iatやnbfで「時間のルール」を定めて、jtiによって「世界にひとつの番号」を持たせ、リプレイ攻撃からシステムを鉄壁に守る。
一見するとただの文字列の羅列に思えるJWTも、こうしてそれぞれの役割を紐解いていくと、ネットワークの安全性を保つための美しい工夫が詰まっていることが分かりますよね。
インフラやセキュリティの世界は、こうした小さなルールの積み重ねで私たちの安全なデジタル生活を支えています。ぜひ、今日からご自身の開発するAPIでも jti を意識した設計を取り入れてみてくださいね。それでは、また次回の技術解説でお会いしましょう!
コメント