JWT(JSON Web Token)徹底解説:構造から実務での活用法まで
こんにちは!Web開発の世界でよく耳にする「JWT」、皆さんはどれくらい理解されていますか?本記事では、JWTの基本的な構造から、なぜ使われるのか、そして実務でどのように活用されているのかを、分かりやすく解説していきます。
1. JWTの基本的な構造
JWTは、大きく分けて以下の3つの部分から構成されます。これらはドット (.
) で区切られています。
<Header>.<Payload>.<Signature>
1.1. Header(ヘッダー)
- 役割: どのようなアルゴリズムでSignatureが生成されたか、トークンのタイプなどを定義します。
-
例:
{ "alg": "HS256", // 使用する署名アルゴリズム (HS256, RS256など) "typ": "JWT" // トークンのタイプ (通常"JWT"で固定) }
-
補足:
alg
には HMAC SHA256 (HS256) や RSA SHA256 (RS256) などが指定されます。
1.2. Payload(ペイロード)
- 役割: トークンに含まれる情報(クレーム)を格納します。これは暗号化されておらず、誰でも内容を確認できますが、改ざんはできません(Signatureによって保証)。
-
例:
{ "sub": "user123", // ユーザーID (subject) "iat": 1716700000, // 発行日時 (issued at) "exp": 1716703600 // 有効期限 (expiration time) }
-
補足:
sub
はユーザー識別子、iat
は発行時刻、exp
は有効期限を表す代表的なクレームです。ここにユーザーのロールや権限などの情報を追加することも可能です。
1.3. Signature(署名)
- 役割: HeaderとPayloadを結合し、Headerで指定されたアルゴリズムとサーバーの秘密鍵(または公開鍵)を使って生成されます。この署名によって、トークンが改ざんされていないことを証明します。
-
生成方法:
Signature = HMACSHA256( base64UrlEncode(Header) + "." + base64UrlEncode(Payload), secret )
- 補足: Payloadの内容が少しでも変更されると、Signatureが無効になります。これがJWTの「誰でも見れるけど、改ざんは不可能」という性質を担保しています。
2. JWTのメリット(なぜ使われるのか?)
JWTが広く利用されるには、いくつかの優れた特徴があります。
2.1. Statelessness(ステートレス性)
- セッションベースの認証と異なり、サーバー側でセッション情報を保持する必要がありません。これにより、データベースへのアクセスが減り、システムのスケーラビリティが向上します。
2.2. Cross-domain and Cross-service(ドメイン間・サービス間の連携)
- 一つのサービスで発行されたJWTを、他のサービスでも検証できます。これはマイクロサービスアーキテクチャとの相性が非常に良いです。
2.3. Compactness(コンパクトさ)
- エンコードされた形式(Base64Urlエンコード)により、HTTPヘッダーやURLパラメータなどで効率的に送信できます。
2.4. Self-contained(自己完結性)
- 必要な情報(ユーザーID、有効期限など)がトークン自体に含まれているため、認証処理に追加のデータベース 조회が不要な場合があります。
3. JWTは無人택배함!?〜ステートレスの例え〜
ここで、JWTのステートレスな性質を分かりやすく例えてみましょう。
【セッションベースの認証: 경비실(警備室)に預ける】
- あなたが荷物を注文し、不在でした。
- 配達員は「警備室に預けてください」と依頼し、警備員は荷物リストにあなたの名前を記録します。
- 後で荷物を受け取りに行く際、「〇〇号室の〇〇です」と伝えると、警備員は記録を確認し荷物を渡してくれます。
- 問題点: 警備員がいない、または荷物が多すぎると、受け取れない(サーバー依存、サーバー負荷)。
【JWT: 無人택배함に預ける】
- あなたが不在でも、配達員は荷物を「無人택배함」に保管します。
- 保管後、あなたは「QRコード」を受け取ります。
- あなたはいつでも無人택배함に行き、QRコードをかざすだけで荷物を受け取れます。
- 利点: 警備員(サーバー)の都合に左右されず、いつでも自分で受け取れます。また、一度渡したQRコード(JWT)は、受け取る側(クライアント)が管理します。
このように、JWTは「受け取った人が自分で管理・提示する情報」という点で、無人택배함に例えることができます。
4. JWTはいつ使うべきか?
どのような状況でJWTが適しているのでしょうか?
- サーバーがステートレスで運用される場合: セッションストアなしで、JWTのみを見て認証処理を行う必要がある時。
- マイクロサービスアーキテクチャ: Aサービスでログインしたユーザーが、BやCサービスでも同じJWTで認証される必要がある場合。
- HTTPベースのAPI通信のみの場合: WebアプリケーションのAPI認証など。
- 複数のドメイン/サービス間で、単一のログインで認証を維持したい場合。
5. io.jsonwebtoken
(jjwt)ライブラリの必要性
JavaやSpring BootでJWTを扱う際、io.jsonwebtoken
(通称 jjwt
)のようなライブラリはなぜ必要なのでしょうか?
- 結論: JWTを直接生成・検証するにはライブラリが必須だからです。
-
理由:
- JWTの生成、署名、検証、クレームの抽出などを簡単に行うためにJWTライブラリが必要になります。
-
代替案:
- Spring Security + OAuth2: Spring Securityが提供するJWT機能を活用することも可能ですが、設定が複雑になる場合があります。
- 直接実装: Base64エンコードや署名処理などを自前で実装することも理論上可能ですが、セキュリティ上の脆弱性を生むリスクが非常に高いため、非推奨です。
-
補足:
jjwt
以外にも、auth0/java-jwt
やNimbus JOSE + JWT
といったライブラリもよく利用されます。
6. コントローラーメソッドの引数名が認識されないエラーとその解決策
Spring MVCで @RequestParam
や @PathVariable
を使う際、稀に引数名が認識されずエラーが発生することがあります。
-
原因: コンパイル時に
-parameters
オプションなしでビルドされた場合、クラスファイルに引数名情報が含まれなくなるためです。 -
例:
public String getToken(String username)
のように引数名を省略した場合、Springがusername
という名前を特定できなくなります。 -
解決策:
-
引数に明示的にアノテーションで名前を指定する:
@GetMapping("/token") public String getToken(@RequestParam("username") String username) { // ... }
-
コンパイル時のオプション設定: ビルドツール(Maven/Gradle)で
-parameters
オプションを有効にする。
-
引数に明示的にアノテーションで名前を指定する:
明示的に指定する方が、コードの意図が明確になり、より安全です。
7. Postmanを使ったJWTのやり取り(イメージ)
PostmanはJWTを使ったAPIテストに非常に便利です。
-
ログインAPI: ID/パスワードを送信し、成功すると
Authorization: Bearer <JWT>
という形式でレスポンスヘッダーにJWTが返ってくることが多いです。また、レスポンスボディにもJSON形式でトークンが含まれる場合もあります。 -
認証が必要なAPI: 前のAPIで取得したJWTを、
Authorization: Bearer <JWT>
の形式でリクエストヘッダーに含めて送信します。
8. HS256アルゴリズムとは?
HS256
はJWTで最も一般的に使用される署名アルゴリズムの一つです。
- HS256: HMAC (Hash-based Message Authentication Code) に SHA-256 ハッシュ関数を組み合わせたものです。
- 仕組み: 秘密鍵(共通鍵)を用いて、ヘッダーとペイロードの結合文字列をハッシュ化し、署名を生成します。サーバーとクライアント(または異なるサービス間)で同じ秘密鍵を共有することで署名の検証が可能になります。
-
例:
Keys.secretKeyFor(SignatureAlgorithm.HS256)
のように、ライブラリで秘密鍵を生成します。この秘密鍵は絶対に漏洩させてはなりません。
9. URL末尾の改行文字 (\n
) による404エラー
PostmanでAPIを呼び出す際、URLの最後に意図せず改行文字 (\n
) が含まれていると、Spring MVCがパスを見つけられずに404エラーを返すことがあります。
-
原因: Springはパスとして
/check\n
というリソースを探しに行きますが、実際には/check
というパスしか存在しないためです。 - 解決策: PostmanのURL入力欄の末尾に余分な改行やスペースが含まれていないか確認し、削除してから再試行してください。
10. JWTはサーバーで「削除」できない理由
JWTはステートレスであるため、サーバー側で直接「削除」することはできません。
-
理由:
- JWTはサーバーに保存されず、クライアント(ブラウザのLocalStorageやCookieなど)に保管されます。
- サーバーは受け取ったJWTの有効性(署名の検証や有効期限)を確認するだけで、そのJWTの存在自体を管理しているわけではありません。
-
【対策】トークンの無効化(ログインアウト):
- 有効期限を短く設定する: 例えば15分〜1時間程度にし、その都度再ログインまたはリフレッシュトークンで更新する。
-
ブラックリスト(失効リスト)の管理: ログアウト時に、そのJWTのID(
jti
クレームなど)をサーバー側のデータベースやRedisに保存し、リクエストごとにブラックリストに含まれていないか確認する。 - リフレッシュトークンとの併用: アクセストークン(短命)とリフレッシュトークン(長命)を使い分け、リフレッシュトークンはサーバー側で管理する。
11. トークンの渡し方:Authorization: Bearer <token>
クライアントからサーバーへJWTを渡す際の標準的な方法は、HTTPヘッダーの Authorization
を使うことです。
-
形式:
Authorization: Bearer <access_token>
-
Bearer
の意味: これはRFC 6750で定義されている認証スキームの一つで、「このトークン(Bearer Token)を持っている人が認証されますよ」という意味を表します。 -
なぜ必要か:
- サーバーが受け取ったトークンが、どのような種類の認証情報なのかを明確にするためです。
Bearer
が付いていることで、サーバーはそれをJWTアクセストークンとして解釈します。 - これがないと、サーバーは認証ヘッダーを適切に処理できず、認証エラーとなる可能性があります。
- サーバーが受け取ったトークンが、どのような種類の認証情報なのかを明確にするためです。
-
他の認証方式の例:
Authorization: Basic <base64encoded_id:password>
など。
12. ResponseEntity
を使ったトークンの返却方法
Spring Bootのレスポンスで、ヘッダーとボディの両方にトークンを含めるのは一般的な方法です。
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token) // 🔹 トークンをレスポンスヘッダーに含める
.body(Map.of("token", token)); // 🔹 同時にレスポンスボディにも含める
メリット:
ヘッダー: クライアント側で response.headers.get("Authorization") のようにして取得でき、標準的な認証ヘッダーとして扱われます。
ボディ: JavaScriptのFetch APIやAxiosなどで response.data.token のようにJSONから直接取り出して扱いやすいです。
この方法は、クライアント側がどちらの方法でトークンを受け取っても対応できるため、汎用性が高いです。
13. JWT認証の典型的なシナリオ
一般的なWebアプリケーションでのJWT認証フローは以下のようになります。
ログイン: ユーザーがID/パスワードをフォームで送信します。
認証: バックエンドは送信された認証情報を検証します。
JWT発行: 認証成功後、バックエンドはJWTを生成し、クライアントに返却します(ヘッダー、ボディなど)。
トークン保存: クライアントは受け取ったJWTを保存します(LocalStorage, SessionStorage, HttpOnly Cookieなど)。
APIリクエスト: 以降の認証が必要なAPIリクエストでは、保存したJWTを Authorization: Bearer <JWT> ヘッダーに含めて送信します。
トークン検証: バックエンドはリクエストヘッダーからJWTを抽出し、署名や有効期限を検証します。
認可: 検証が通れば、そのJWTに含まれる情報(ユーザーIDやロールなど)に基づいて、リクエストされた操作を許可するかどうか(認可)を判断します。
この流れのポイントは、**サーバー側がJWTの状態を管理しない(ステートレス)**点です。
【クライアント側でのトークン保存場所の選択肢】
LocalStorage: JavaScriptからアクセス可能。XSS攻撃に弱い。
SessionStorage: ブラウザタブを閉じると消える。XSS攻撃に弱い。
HttpOnly Cookie: JavaScriptからアクセス不可。XSS攻撃に強く、セキュリティが高い。
14. JWTの内容を暗号化したい場合:JWE(JSON Web Encryption)
標準的なJWT(JWS: JSON Web Signature)は、内容がBase64エンコードされているだけで、誰でもデコードして中身を見ることができます。
JWE: 「JSON Web Encryption」の略で、JWTのペイロード部分を暗号化する仕組みです。
JWS vs JWE:
JWS: データの改ざん防止(署名のみ)。内容は平文。
JWE: データの機密性保護(ペイロードを暗号化)。内容は秘匿。
使い分け:
ユーザーIDや有効期限のような、機密性がそれほど高くない情報であればJWSで十分です。
JWT内に、個人情報や非常に機密性の高い情報をどうしても含めたい場合は、JWEの利用を検討します。
実務では: 通常はJWSで十分ですが、サービス要件に応じてJWEや、秘密鍵を安全に管理した上でJWTを併用するなどの工夫がされます。
15. JWTの限界と実務での組み合わせ
JWT単独で利用するには、いくつかの限界があります。
【JWTの主な限界】
トークン無効化の難しさ: 一度発行されたJWTは、有効期限内であればサーバー側から強制的に無効化できません(削除できない)。
細やかな権限管理の難しさ: JWT内にロール情報を入れても、複雑な条件付きの認可処理はコードが煩雑になりがちです。
トークン漏洩のリスク: クライアント側で管理されるため、XSSなどの攻撃でトークンが漏洩すると、有効期限内は不正利用されます。
リフレッシュ不可: アクセストークンのみの場合、有効期限が切れると再ログインが必要になり、ユーザー体験が悪くなります。
【実務での標準的な組み合わせ】
これらの限界を克服するため、実務では以下のような組み合わせが一般的です。
Access Token (短い有効期限): ユーザー認証情報を含み、各APIリクエストに使用。
Refresh Token (長い有効期限): Access Tokenの再発行に使用。このリフレッシュトークンはサーバー側で(Redisなどのキャッシュに)管理します。
Redisなどのキャッシュストア: リフレッシュトークンの保存、ブラックリスト管理に使用。
Spring Security: 認証・認可処理全体のフレームワークとして機能。
【Token無効化の仕組み】
Access Tokenはステートレスに扱いますが、リフレッシュトークンはサーバー側(Redisなど)で管理します。
ログアウト時には、Redisからリフレッシュトークンを削除したり、Access Tokenをブラックリストに追加したりすることで、事実上のトークン無効化を実現します。
このように、Spring SecurityとJWT、そしてリフレッシュトークンとRedisを組み合わせることで、堅牢でスケーラブルな認証システムを構築するのが一般的です。
まとめ
JWTは、ステートレスな認証を実現し、スケーラビリティやサービス間連携に優れた強力なツールです。しかし、その特性上、トークンの無効化やセキュリティリスクへの対応には、リフレッシュトークンやサーバー側での管理といった他の技術との組み合わせが不可欠です。