🕌

# 4.6 JWT とセッション管理の実践

に公開

これまで Django × Vue での認証・権限管理について書いてきた。
今回は SPA(Single Page Application)構成における認証方式 にフォーカスして、
セッション認証と JWT 認証をどう使い分けるか、実際の運用も含めて整理する。


1. SPA と認証の関係

Django 単体でテンプレートを使っていた頃は、
「ログインしたユーザーの情報をサーバーセッションで管理し、画面描画に直接反映する」構成が自然だった。

しかし Vue などの SPA では、

  • バックエンドは API 提供に徹する
  • フロントは UI と状態管理に徹する
    という役割分担が明確になる。

このとき、セッション管理をどう持つか が重要になる。


2. セッション認証(サーバーサイド管理)

Django 標準の方式。
ユーザーがログインすると「セッションID」が Cookie に保存され、
サーバー側にユーザー情報を保持する。

  • メリット

    • Django 標準機能でそのまま使える
    • サーバー管理なのでセキュリティが高い
    • 業務システムなど「社内利用」には十分
  • デメリット

    • SPA との相性がやや悪い(CSRF 対策が必須)
    • サーバーをスケールアウトする場合、セッション共有が必要

Vue 側での対応(axios の例):

import axios from "axios";

const api = axios.create({
  baseURL: "/api",
  withCredentials: true // Cookie を送信する
});

api.interceptors.request.use(config => {
  const csrfToken = document.cookie.match(/csrftoken=([^;]+)/)?.[1];
  if (csrfToken) {
    config.headers["X-CSRFToken"] = csrfToken;
  }
  return config;
});

3. JWT 認証(トークンベース)

「セッションをサーバーに持たず、署名付きトークンをクライアントに保持する」方式。
ユーザーがログインすると JWT が発行され、以降は API リクエストに付与して認証する。

  • メリット

    • サーバーに状態を持たないためスケールしやすい
    • モバイルアプリや外部サービス連携に向く
    • API ドリブンなアーキテクチャに馴染む
  • デメリット

    • トークン流出リスクが高い(localStorage などに保存するため)
    • 有効期限管理やリフレッシュ制御が複雑
    • 並列リクエストでリフレッシュ競合が起きやすい

Vue 側での対応(axios の例):

import axios from "axios";

const api = axios.create({ baseURL: "/api" });

api.interceptors.request.use(config => {
  const token = localStorage.getItem("access_token");
  if (token) {
    config.headers["Authorization"] = `Bearer ${token}`;
  }
  return config;
});

4. 運用でハマったポイント

セッションの場合

  • CSRF トークンを正しく付与しないと 403 が返る
  • 特に SPA だと「初回ロード時に CSRF が未取得」でエラーになるケースが多い

JWT の場合

  • 画面初期化で複数 API を同時に叩くと、アクセストークンが期限切れで 同時にリフレッシュ API を叩き、競合で失敗する
  • 対策として「リフレッシュ処理を直列化する仕組み」を入れた
let isRefreshing = false;
let refreshSubscribers = [];

function onRefreshed(token) {
  refreshSubscribers.forEach(cb => cb(token));
  refreshSubscribers = [];
}

api.interceptors.response.use(
  res => res,
  async error => {
    const { config, response } = error;
    if (response?.status === 401 && !config._retry) {
      if (!isRefreshing) {
        isRefreshing = true;
        try {
          const refresh = localStorage.getItem("refresh_token");
          const res = await axios.post("/api/token/refresh/", { refresh });
          const newToken = res.data.access;
          localStorage.setItem("access_token", newToken);
          isRefreshing = false;
          onRefreshed(newToken);
        } catch (e) {
          isRefreshing = false;
          return Promise.reject(e);
        }
      }

      return new Promise(resolve => {
        refreshSubscribers.push(token => {
          config._retry = true;
          config.headers.Authorization = `Bearer ${token}`;
          resolve(api(config));
        });
      });
    }
    return Promise.reject(error);
  }
);

5. AI活用のポイント

AI は認証の雛形コード(axios 設定や Django 側設定)を出すのは得意だが、以下の点で注意が必要。

① リフレッシュ処理は AI が苦手

  • 並列リクエスト時の挙動や Vue のリアクティブのイベント順序は、AIが想定する動きと実際の動きがズレることが多い

  • 例えば:

    • 「mounted で状態を更新 → それに依存した watch が走る」
    • AI のコードでは watch が必ず後に動く前提になっているが、実際には mounted より早く走ってしまうケースがある
  • その結果、AIが提示したリフレッシュ処理の順序通りには動かないことがざらにある

👉 実際には「あるイベントが必ず先に発火する」と AI が前提している部分が、開発端末では逆順になってバグ化することが多い。
こればかりは実機テストで確認し、イベント順序をログで追って調整するしかない。

② 状態管理の差分を伝えにくい

  • 「開発端末でこういう順序で発火している」といった情報を AI に正確に伝えるのは難しい
  • そのため リフレッシュの不具合は一度AIの雛形を参考にしたあと、人間がログを見て地道に調整する作業が必須

③ AIの使いどころ

  • 雛形生成:axios の interceptor や Django 側の設定は一発で書ける
  • 調整は人間:発火順序やリフレッシュ競合は現場で確認しないと解決しない

6. 実際の選択

  • 社内業務システム(SPA 構成)
    → セッション認証を基本とする
    → セキュリティが高く、Django 標準でカバー可能

  • 外部公開 API / EC サイト / モバイルアプリ
    → JWT を採用
    → スケールや外部連携を考えると必須になる

実際のプロジェクトでは「業務システムはセッション」「EC 系は JWT」と使い分けている。


まとめ

  • SPA ではバックエンドとフロントの責務が明確になるため、認証方式の選択が重要
  • セッション認証: 内部システム向け。安全でシンプル
  • JWT 認証: 外部 API やモバイル連携向け。柔軟だが運用は難しい
  • 実際には「用途に応じて両方を使い分ける」のが現実的

次回は、権限管理の延長として 「API 側の permission 実装パターン」 を紹介する予定。

Discussion