面接問題:JWTがランダムトークンではなくペイロードを含む設計理由とトレードオフ
問題¶
As a developer, have you ever thought: Why does JWT include payload data instead of just being a random token?
解答¶
JWT がペイロードを含む理由:ストレージへのラウンドトリップをなくすため(ステートレス性)
ただし、この設計は一貫性問題というトレードオフを生む。
解説¶
JWT vs ランダムトークンの比較¶
ランダムトークン(セッションID方式):
クライアント → トークン送信 → サーバー → DBで検索 → ユーザー情報取得
↑ 毎リクエストにDBアクセスが発生
JWT(Self-Contained):
クライアント → JWT送信 → サーバー → 署名検証だけでユーザー情報を取得
↑ DBへのラウンドトリップが不要 → スケールしやすい
JWT の構造¶
Header.Payload.Signature
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "user123", "role": "admin", "exp": 1720000000 }
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
ペイロードは Base64 エンコードされているだけで暗号化ではない。 機密情報(パスワード・クレカ番号)は絶対に入れてはならない。
一貫性問題(最重要トレードオフ)¶
問題シナリオ:
1. ユーザーに admin 権限を付与 → JWT 発行
2. 不正を検知して admin 権限を剥奪
3. しかし既発行の JWT はまだ有効期限内
→ ユーザーは剥奪された権限で引き続きアクセスできてしまう
解決策:
A. 有効期限を短くする(15分 ~ 1時間)
→ 権限変更が遅れて反映されるが被害範囲は小さい
B. ブラックリストを持つ(Redis に revoked tokens を保存)
→ ラウンドトリップが発生するが即時無効化できる
C. Refresh Token + Short-lived Access Token の組み合わせ
→ 実務で最も採用されているパターン
実装例(Go)¶
// JWT 生成
func GenerateToken(userID string, role string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"role": role,
"exp": time.Now().Add(15 * time.Minute).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
}
// JWT 検証
func ValidateToken(tokenStr string) (jwt.MapClaims, error) {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
return nil, err
}
return token.Claims.(jwt.MapClaims), nil
}
面接でのポイント¶
- 「ステートレスでスケールしやすい」だけ答えると浅い
- 一貫性問題(権限剥奪が即時反映されない) を言及すると深さが出る
- Refresh Token パターンの説明ができると実務経験として評価される
- ペイロードは暗号化でなく Base64 エンコードという点も必ず触れる