2 方式の構造
JWT(JSON Web Token)
[ログイン成功]
→ サーバーがペイロード(userId, role, exp)を秘密鍵で署名
→ 署名済みトークンをクライアントの Cookie に保存
[その後のリクエスト]
→ Cookie から JWT を取り出す
→ サーバーが署名を検証(DB クエリなし)
→ ペイロードからユーザー情報を取得
JWT はそれ自体にユーザー情報が含まれます。サーバーは状態を持たず(stateless)、検証は署名チェックのみで完結します。
DB Session
[ログイン成功]
→ サーバーがランダムな Session ID を生成して DB に保存
→ Session ID を Cookie に保存
[その後のリクエスト]
→ Cookie から Session ID を取り出す
→ DB で Session ID を検索してユーザー情報を取得
DB Session はトークンの中にユーザー情報を持ちません。Cookie にあるのは DB を参照するための ID のみです。
設計判断軸の比較
| 判断軸 | JWT | DB Session |
|---|---|---|
| 保存場所 | ペイロードをクライアント(Cookie)に保存 | Session ID のみ Cookie、データは DB |
| 検証コスト | 署名チェックのみ(DB クエリなし) | DB クエリが必要 |
| 即時無効化 | 難しい(期限切れまで有効) | 簡単(DB から削除するだけ) |
| 権限変更の反映 | Token 期限が切れるまで遅延する | 次のリクエストから即時反映 |
| スケール | サーバー間でセッション共有不要 | DB / Redis の共有が必要 |
| Cookie サイズ | ペイロード分大きくなる | Session ID のみ(小さい) |
| 秘密鍵の管理 | 必要(ローテーション運用も発生) | 不要 |
無効化と権限変更の扱い
JWT の最大の制約は即時無効化ができないことです。
JWT 期限: 24 時間
15:00 パスワード変更 → 旧 JWT は 翌15:00 まで有効
15:00 不審なアクセスを検知 → 即時ログアウトできない
15:00 admin ロールを剥奪 → 期限まで admin として動作する
対処として「JWT ブラックリスト(DB / Redis)」を導入できますが、その時点でサーバー側に状態が必要になり、stateless の利点は薄れます。
DB Session は DB レコードを削除・更新するだけで即時反映できます。
// 強制ログアウト(DB Session)
await db.session.delete({ where: { id: sessionId } }); // 次のリクエストから無効
// ロール変更(DB Session)
await db.session.update({
where: { userId },
data: { role: "user" }, // 次のリクエストから新ロールが適用される
});
Cookie と組み合わせるときの考え方
どちらの方式でも Cookie を使う場合、セキュリティオプションは同じ設定が必要です。
// JWT を Cookie に保存する場合
response.cookies.set("token", jwt, {
httpOnly: true, // JS から読めないようにする(XSS 対策)
sameSite: "lax", // CSRF 対策
secure: true, // HTTPS のみ
maxAge: 60 * 60 * 24, // 24 時間
});
// DB Session の Session ID を Cookie に保存する場合
response.cookies.set("session_id", sessionId, {
httpOnly: true,
sameSite: "lax",
secure: true,
maxAge: 60 * 60 * 24 * 7, // 7 日間
});
違いはCookie に何を入れるかだけです。JWT はユーザー情報を含む署名済みトークン、DB Session はランダムな ID です。Cookie のセキュリティオプションはどちらも同じルールで設定します。
向いている場面
JWT が向く場面
- 強制ログアウト・権限変更の即時反映が不要なシステム
- 複数サービスにまたがる認証(マイクロサービス)でセッション共有を避けたい
- DB クエリを認証のたびに発行したくない
- 外部サービスとトークンを共有する設計(OAuth 連携など)
DB Session が向く場面
- 強制ログアウトが必要(セキュリティ要件が高いシステム)
- ロール変更・権限剥奪を即時反映したい(管理機能があるアプリ)
- セッションデータが大きく Cookie に収めにくい
- ユーザーのオンライン状態や最終アクセス時刻を管理したい
よくある誤解
「JWT の方が常に軽い」
JWT のペイロードが大きくなると Cookie サイズが増え、毎リクエストのヘッダー転送量が増えます。DB Session の Session ID(数十バイト)より JWT(数百バイト〜 KB 超)の方が大きいケースが多いです。
「DB Session の方が常に安全」
Cookie のセキュリティ(httpOnly / sameSite / secure)はどちらの方式でも同じく必要です。JWT か DB Session かよりも Cookie オプションの設定が正しいかどうかの方が実際のセキュリティに影響します。
「DB Session はスケールしない」
DB Session は Redis などのインメモリキャッシュと組み合わせることで、リクエストごとの DB 負荷を抑えられます。stateful = スケールしないという思い込みは誤りです。
「JWT にしておけばステートレスになる」
即時無効化のためにブラックリストを Redis で管理した時点で、サーバーは状態を持ちます。「ステートレスの維持」が実装上の優先事項かどうかを要件から確認してから選びましょう。
session-management-pattern との接続
session-management-pattern(getSession / requireSession の責務分離)はJWT でも DB Session でも同じ構造で使えます。
// DB Session の場合(DB クエリで取得)
export const getSession = cache(async (): Promise<Session | null> => {
const sessionId = cookieStore.get("session_id")?.value;
if (!sessionId) return null;
return db.session.findUnique({ where: { id: sessionId } });
});
// JWT の場合(署名検証で取得)
export const getSession = cache(async (): Promise<Session | null> => {
const token = cookieStore.get("token")?.value;
if (!token) return null;
try {
return verify(token, SECRET) as Session; // 署名検証
} catch {
return null;
}
});
getSession() の内部実装が変わるだけで、呼び出し側(requireSession / requireRole / ページコンポーネント)は変わりません。
Session 取得 API の責務分離 → 型安全な Session 管理パターン
認証判定をどこに置くか → Next.js 認証方式比較
Cookie オプションの設定 → Cookie セキュリティオプション 3点セット
未認証・権限不足の失敗分岐 → 認証・権限チェックの失敗分岐