JWT がステートレスなのにどうユーザーをログアウトさせるか¶
原文¶
Interviewer:
If JWTs are stateless… how do you "log out" a user?
要約¶
JWT はステートレスなため、サーバー側で単純に「削除」できない。ログアウトを実現するには「ブロックリスト(トークン無効化リスト)」「短い有効期限 + リフレッシュトークン」「Opaque Token への切り替え」等のアプローチがある。
回答¶
「JWT はステートレスなため、厳密な意味でサーバーからログアウトさせることはできない。しかし以下の戦略で実質的にログアウトを実現できる:
- ブロックリスト(JTI ベース):無効化した JTI を Redis 等に保存し、リクエスト時に確認
- 短い TTL(15分):アクセストークンの有効期限を短くし、実害を最小化
- リフレッシュトークンの失効:リフレッシュトークンを DB 管理して即時失効させる
- Opaque Token:ステートを持つトークンに切り替えて DB で管理する」
解説¶
なぜ JWT をそのまま無効化できないのか¶
JWT の検証はサーバーの秘密鍵(または公開鍵)だけで完結するため、DB アクセス不要でスケールしやすい。しかしこれは裏を返せば「一度発行したトークンは有効期限まで有効」を意味する。
アプローチ比較¶
| アプローチ | しくみ | メリット | デメリット |
|---|---|---|---|
| ブロックリスト | 失効 JWT の JTI を Redis に保存 | 即時ログアウト可能 | Redis 参照が増える(ステートが生まれる) |
| 短い TTL | access token を 15 分に設定 | シンプル | ログアウトしても最大 15 分は有効 |
| リフレッシュトークン管理 | refresh token を DB で管理、ログアウト時に削除 | refresh token を即時失効できる | access token はまだ有効 |
| Opaque Token | JWT をやめて DB 管理の不透明トークンへ | 即時失効・完全なコントロール | 毎リクエストで DB 参照が必要 |
| トークンローテーション | refresh token 使用のたびに新しいものを発行 | 漏洩を早期検知できる | 実装が複雑 |
実践的な推奨構成¶
access_token: TTL=15分(短命)
refresh_token: TTL=7日(DB管理、ログアウト時に削除)
ログアウト時:
1. refresh_token を DB から削除
2. access_token の JTI を Redis ブロックリストに追加(TTL=残り有効期限)
3. クライアント側でトークンを削除
Go での実装スケッチ¶
// ログアウト
func Logout(ctx context.Context, jti string, exp time.Time) error {
ttl := time.Until(exp)
return redisClient.Set(ctx, "blocklist:"+jti, "1", ttl).Err()
}
// ミドルウェアで確認
func AuthMiddleware(c *gin.Context) {
claims := extractClaims(c)
blocked, _ := redisClient.Exists(ctx, "blocklist:"+claims.JTI).Result()
if blocked > 0 {
c.AbortWithStatus(401)
return
}
c.Next()
}
→ 関連: JWT解説、デバイストークンスコープ設計