Django中級 #5 メッセージ / セッション / クッキー
HTTP は stateless です。あるリクエストと次のリクエストが互いを知らないのが基本。その上に 「同じユーザー」 という概念を乗せるための 3 つのツールがあります。
- メッセージ — 1 つのリクエストから次のリクエストへ少しの間伝達 (flash)
- セッション — 1 人のユーザーの複数リクエストにわたる状態
- クッキー — 上の 2 つを可能にする最も下層の媒体
この記事は上から下へ — メッセージから始まりクッキーセキュリティまで一カ所で見ます。
#3 で見た MessageMiddleware、SessionMiddleware が動作の土台です。
メッセージ — flash パターン #
「保存されました」 のように次のページに遷移して 1 度だけ表示する通知。これを自前で作るとセッションを触るコードを毎回書かなければなりませんが、Django は messages フレームワークで解いてくれます。
セットアップ (普通は自動) #
startproject で作ったプロジェクトはすでに入っています。
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",
],
},
}]ビューでメッセージを追加 #
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.debug | DEBUG | 開発デバッグ |
messages.info | INFO | 単純な案内 |
messages.success | SUCCESS | 成功通知 |
messages.warning | WARNING | 注意 |
messages.error | ERROR | 失敗 |
DEBUG はデフォルトでは表示されません。MESSAGE_LEVEL = messages.DEBUG で有効化できます。
CBV では — SuccessMessageMixin
#
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 パターン)。
テンプレートでレンダ #
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li class="message message--{{ message.tags }}">
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}message.tags—success、errorなど (CSS クラスとして活用)- 1 度レンダされると 自動で消えます — 次のリクエストでは再表示されない
メッセージバックエンド #
デフォルトのバックエンドは FallbackStorage — セッションを試して、ダメならクッキーにフォールバックします。
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 と似ています。
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) または長い値で分岐。
セッションバックエンド — どこに保存するか #
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 バックエンドは期限切れの行が自動で消えません。定期的に整理:
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 responserequest.COOKIES は dict 形式ですべてのクッキー が入っている読み取り専用オブジェクトです。書き込みはレスポンスオブジェクトに。
クッキーセキュリティ — HttpOnly / Secure / SameSite #
クッキーのセキュリティ属性 3 つは 必ず知っておくべき基本 です。
HttpOnly
#
Set-Cookie: sessionid=abc; HttpOnlyJavaScript の document.cookie で読めなくなる ように防ぎます。XSS が起きてもセッションクッキーを盗まれません。
セッションクッキー、認証クッキーは 必ず HttpOnly。Django の SESSION_COOKIE_HTTPONLY と CSRF_COOKIE_HTTPONLY のデフォルト値:
| デフォルト | 意味 | |
|---|---|---|
SESSION_COOKIE_HTTPONLY | True | セッションクッキーは JS アクセス遮断 |
CSRF_COOKIE_HTTPONLY | False | JS がトークンをヘッダに入れて送る必要があるので読めなければならない |
CSRF クッキーだけ例外なのは — fetch リクエストに X-CSRFToken ヘッダでトークンを入れて送らなければならないからです。
Secure
#
Set-Cookie: sessionid=abc; SecureHTTPS 接続でのみ 送信されます。HTTP ではクッキーが行きません。運用では必ず有効化しなければなりません。
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 = TrueSecurityMiddleware (#3) がこの設定を強制します。詳細な運用セキュリティは 上級 #7。
SameSite
#
クッキーが 他ドメイン発のリクエスト に一緒に送られるかを制御します。CSRF の最も強い防衛線。
| 値 | 意味 |
|---|---|
Strict | 同じサイトのリクエストにのみ (外部リンククリック時も送らない) |
Lax | デフォルト推奨。 GET のような安全なトップレベルナビゲーションには送り、POST 等には送らない |
None | すべてのリクエストに (Secure も併せて有効化) |
SESSION_COOKIE_SAMESITE = "Lax" # デフォルト
CSRF_COOKIE_SAMESITE = "Lax"Strict は外部リンクからサイトに来たときにログインが解除されて見える場合があり、UX が不自然です。Lax が普通の答え。
CSRF とクッキーの関係 — 短く #
CSRF (Cross-Site Request Forgery) — 他のサイトがユーザーの認証クッキーに 便乗 してリクエストを偽造する攻撃。
Django の防御:
- フォームに
{% csrf_token %}→ 隠し input でトークンを挿入 - クッキーに同じトークン を保存 (CSRF クッキー)
- POST/PUT/DELETE リクエストで 2 つの値を比較 (
CsrfViewMiddleware) - クッキーとフォームの値が違えば 403 返却
JavaScript fetch なら X-CSRFToken ヘッダでトークンを併せて送ります。
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 トークンは 二重防御 の役割。
小さな実戦 — 未ログインのカート #
セッションを使うありふれたケース — ログインしていないユーザーのカートをセッションに一時保管。
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"]、get、flush、cycle_key、set_expiry - セッションバックエンド:
db(デフォルト)、cache、cached_db、file、signed_cookies - 期限切れセッションの掃除:
python manage.py clearsessions - クッキー —
request.COOKIES、response.set_cookie、response.delete_cookie - セキュリティ属性:
HttpOnly— JS アクセス遮断 (セッションクッキーには必須)Secure— HTTPS でのみ送信 (運用必須)SameSite=Lax— CSRF の強い防衛線
- 運用設定:
SESSION_COOKIE_SECURE、CSRF_COOKIE_SECURE、SECURE_SSL_REDIRECT、HSTS - CSRF トークン + クッキー比較、fetch は
X-CSRFTokenヘッダ - セッションの dict 内部変更は
request.session.modified = True
次回 (#6 Static/Media 運用) では、基礎 #5 で初対面した静的ファイルを、運用視点 で見直します — collectstatic、MEDIA_*、S3 / WhiteNoise / Storage バックエンド まで。