Django中級 #5 メッセージ / セッション / クッキー

読了 7分

HTTP は stateless です。あるリクエストと次のリクエストが互いを知らないのが基本。その上に 「同じユーザー」 という概念を乗せるための 3 つのツールがあります。

  • メッセージ — 1 つのリクエストから次のリクエストへ少しの間伝達 (flash)
  • セッション — 1 人のユーザーの複数リクエストにわたる状態
  • クッキー — 上の 2 つを可能にする最も下層の媒体

この記事は上から下へ — メッセージから始まりクッキーセキュリティまで一カ所で見ます。

#3 で見た MessageMiddlewareSessionMiddleware が動作の土台です。

メッセージ — flash パターン #

「保存されました」 のように次のページに遷移して 1 度だけ表示する通知。これを自前で作るとセッションを触るコードを毎回書かなければなりませんが、Django は messages フレームワークで解いてくれます。

セットアップ (普通は自動) #

startproject で作ったプロジェクトはすでに入っています。

settings.py — 確認
INSTALLED_APPS = [
    ...
    "django.contrib.messages",
]

MIDDLEWARE = [
    ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

TEMPLATES = [{
    ...
    "OPTIONS": {
        "context_processors": [
            ...
            "django.contrib.messages.context_processors.messages",
        ],
    },
}]

ビューでメッセージを追加 #

blog/views.py
from django.contrib import messages
from django.shortcuts import redirect

def post_create(request):
    if request.method == "POST":
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save()
            messages.success(request, "記事が保存されました。")
            return redirect("post_detail", pk=post.pk)
        messages.error(request, "入力値を再確認してください。")
    else:
        form = PostForm()
    return render(request, "blog/post_form.html", {"form": form})

レベル別ヘルパー:

関数レベル用途
messages.debugDEBUG開発デバッグ
messages.infoINFO単純な案内
messages.successSUCCESS成功通知
messages.warningWARNING注意
messages.errorERROR失敗

DEBUG はデフォルトでは表示されません。MESSAGE_LEVEL = messages.DEBUG で有効化できます。

CBV では — SuccessMessageMixin #

CBV
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import CreateView

class PostCreateView(SuccessMessageMixin, CreateView):
    model = Post
    form_class = PostForm
    success_message = "%(title)s 記事が保存されました。"

%(field)s 形式でモデルフィールドをメッセージに差し込みます (#1 CBV の Mixin パターン)。

テンプレートでレンダ #

base.html — 一カ所ですべてのメッセージ
{% if messages %}
  <ul class="messages">
    {% for message in messages %}
      <li class="message message--{{ message.tags }}">
        {{ message }}
      </li>
    {% endfor %}
  </ul>
{% endif %}
  • message.tagssuccesserror など (CSS クラスとして活用)
  • 1 度レンダされると 自動で消えます — 次のリクエストでは再表示されない

メッセージバックエンド #

デフォルトのバックエンドは FallbackStorage — セッションを試して、ダメならクッキーにフォールバックします。

settings.py — バックエンド変更可能
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
# または "...cookie.CookieStorage"

クッキーバックエンドはヘッダが大きくなり得て、セッションバックエンドはミドルウェア順序が重要。デフォルトの FallbackStorage が普通の答え です。

Session — 複数リクエストにわたる状態 #

セッションはユーザーごとにサーバ側に保管する dict のような格納庫です。クライアントは セッション ID だけクッキー で持ち、実際のデータはサーバにあります (デフォルトバックエンドの場合)。

基本使用 #

ブログ — 最近見た記事
def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)

    recent = request.session.get("recent_posts", [])
    if pk in recent:
        recent.remove(pk)
    recent.insert(0, pk)
    recent = recent[:10]
    request.session["recent_posts"] = recent

    return render(request, "blog/post_detail.html", {"post": post})

API は通常の dict と似ています。

セッション API
request.session["key"] = "value"        # 保存
request.session.get("key")              # 取得 (なければ None)
request.session.get("key", default)     # デフォルト値
del request.session["key"]              # 削除
"key" in request.session                # 存在検査

request.session.flush()                  # セッション全体を消して新セッションキーを生成
request.session.cycle_key()              # キーだけ新しく (データ維持) — 権限変更後に推奨

セッション期限 — set_expiry #

期限制御
request.session.set_expiry(60 * 60)     # 1 時間 (秒)
request.session.set_expiry(0)           # ブラウザを閉じたら期限切れ
request.session.set_expiry(None)        # デフォルト (settings.SESSION_COOKIE_AGE)

デフォルトの期限は SESSION_COOKIE_AGE = 1209600 (2 週間)。「ログイン維持」チェックボックスのようなケースで set_expiry(0) または長い値で分岐。

セッションバックエンド — どこに保存するか #

settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.db"  # デフォルト
バックエンド保存場所用途
db (デフォルト)DB (django_session テーブル)ほぼすべてのケース — デフォルト値でよく動く
cacheキャッシュ (Redis など)非常に速い。キャッシュをクリアすると消える (注意)
cached_dbキャッシュ → DB フォールバックDB の永続性 + キャッシュの速さ
fileファイルシステム単一サーバ、小さなトラフィック
signed_cookiesクッキー自体に (署名)サーバ stateless。データ量制限、毎リクエスト送信

運用での普通の答え: db または cached_db。Redis がすでにあれば cache または cached_db で加速。

signed_cookies はサーバを完全に stateless にできますが、すべてのリクエストにセッションデータが送信される のでデータを小さく保たなければなりません。秘密情報を入れてはいけません (署名されているだけで暗号化されていません)。

期限切れセッションの掃除 #

db バックエンドは期限切れの行が自動で消えません。定期的に整理:

cron で定期実行
python manage.py clearsessions

クッキー — 最も下層 #

セッション ID、CSRF トークン、ロケールのような小さな値はクッキーで直接扱うこともあります。

クッキーの読み書き #

クッキーを直接扱う
def view(request):
    visited = request.COOKIES.get("visited", "0")
    visited = str(int(visited) + 1)

    response = render(request, "page.html", {"visited": visited})
    response.set_cookie(
        "visited",
        visited,
        max_age=60 * 60 * 24 * 30,   # 30 日
        httponly=True,
        secure=True,
        samesite="Lax",
    )
    return response

def logout_view(request):
    response = redirect("home")
    response.delete_cookie("visited")
    return response

request.COOKIESdict 形式ですべてのクッキー が入っている読み取り専用オブジェクトです。書き込みはレスポンスオブジェクトに。

クッキーセキュリティ — HttpOnly / Secure / SameSite #

クッキーのセキュリティ属性 3 つは 必ず知っておくべき基本 です。

HttpOnly #

Set-Cookie: sessionid=abc; HttpOnly

JavaScript の document.cookie で読めなくなる ように防ぎます。XSS が起きてもセッションクッキーを盗まれません。

セッションクッキー、認証クッキーは 必ず HttpOnly。Django の SESSION_COOKIE_HTTPONLYCSRF_COOKIE_HTTPONLY のデフォルト値:

デフォルト意味
SESSION_COOKIE_HTTPONLYTrueセッションクッキーは JS アクセス遮断
CSRF_COOKIE_HTTPONLYFalseJS がトークンをヘッダに入れて送る必要があるので読めなければならない

CSRF クッキーだけ例外なのは — fetch リクエストに X-CSRFToken ヘッダでトークンを入れて送らなければならないからです。

Secure #

Set-Cookie: sessionid=abc; Secure

HTTPS 接続でのみ 送信されます。HTTP ではクッキーが行きません。運用では必ず有効化しなければなりません。

settings.py — 運用
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True              # HTTP → HTTPS 自動リダイレクト
SECURE_HSTS_SECONDS = 31536000          # 1 年 HSTS
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

SecurityMiddleware (#3) がこの設定を強制します。詳細な運用セキュリティは 上級 #7

SameSite #

クッキーが 他ドメイン発のリクエスト に一緒に送られるかを制御します。CSRF の最も強い防衛線。

意味
Strict同じサイトのリクエストにのみ (外部リンククリック時も送らない)
Laxデフォルト推奨。 GET のような安全なトップレベルナビゲーションには送り、POST 等には送らない
Noneすべてのリクエストに (Secure も併せて有効化)
settings.py
SESSION_COOKIE_SAMESITE = "Lax"     # デフォルト
CSRF_COOKIE_SAMESITE = "Lax"

Strict は外部リンクからサイトに来たときにログインが解除されて見える場合があり、UX が不自然です。Lax が普通の答え。

CSRF とクッキーの関係 — 短く #

CSRF (Cross-Site Request Forgery) — 他のサイトがユーザーの認証クッキーに 便乗 してリクエストを偽造する攻撃。

Django の防御:

  1. フォームに {% csrf_token %} → 隠し input でトークンを挿入
  2. クッキーに同じトークン を保存 (CSRF クッキー)
  3. POST/PUT/DELETE リクエストで 2 つの値を比較 (CsrfViewMiddleware)
  4. クッキーとフォームの値が違えば 403 返却

JavaScript fetch なら X-CSRFToken ヘッダでトークンを併せて送ります。

fetch + CSRF トークン
function getCookie(name) {
  const m = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
  return m ? m[2] : null;
}

await fetch("/api/posts/", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRFToken": getCookie("csrftoken"),
  },
  body: JSON.stringify(data),
});

SameSite=Lax が有効なら単純な POST 偽造はすでに防がれます。CSRF トークンは 二重防御 の役割。

小さな実戦 — 未ログインのカート #

セッションを使うありふれたケース — ログインしていないユーザーのカートをセッションに一時保管。

cart/views.py
def add_to_cart(request, product_id):
    cart = request.session.get("cart", {})
    cart[str(product_id)] = cart.get(str(product_id), 0) + 1
    request.session["cart"] = cart
    request.session.modified = True   # dict 内部の変更は自動検知されない!
    return redirect("cart_detail")

def cart_detail(request):
    cart = request.session.get("cart", {})
    products = Product.objects.filter(pk__in=cart.keys())
    items = [(p, cart[str(p.pk)]) for p in products]
    return render(request, "cart/detail.html", {"items": items})

落とし穴 1 行: dict 内部の値を変更したとき Django が自動で検知できません。 request.session.modified = True で明示しなければ保存されません。または上のコードのように cart 自体を request.session["cart"] = cart で再代入。

まとめ #

今回押さえたもの:

  • メッセージmessages.success/info/warning/error/debug、テンプレート {% for message in messages %}
  • CBV では SuccessMessageMixin
  • メッセージバックエンド — デフォルトの FallbackStorage が普通の答え
  • セッションrequest.session["key"]getflushcycle_keyset_expiry
  • セッションバックエンド: db (デフォルト)、cachecached_dbfilesigned_cookies
  • 期限切れセッションの掃除: python manage.py clearsessions
  • クッキーrequest.COOKIESresponse.set_cookieresponse.delete_cookie
  • セキュリティ属性:
    • HttpOnly — JS アクセス遮断 (セッションクッキーには必須)
    • Secure — HTTPS でのみ送信 (運用必須)
    • SameSite=Lax — CSRF の強い防衛線
  • 運用設定: SESSION_COOKIE_SECURECSRF_COOKIE_SECURESECURE_SSL_REDIRECTHSTS
  • CSRF トークン + クッキー比較、fetch は X-CSRFToken ヘッダ
  • セッションの dict 内部変更は request.session.modified = True

次回 (#6 Static/Media 運用) では、基礎 #5 で初対面した静的ファイルを、運用視点 で見直します — collectstaticMEDIA_*、S3 / WhiteNoise / Storage バックエンド まで。

X