もう迷わない!Djangoのセッション有効期限(set_expiry())プログラミング例

2025-05-27

このメソッドを使うと、現在処理しているセッションの有効期限を個別に制御できます。セッションは通常、ユーザーのブラウザを閉じるまで、またはサーバー全体で設定された有効期限(SESSION_COOKIE_AGEなど)が来るまで存続しますが、set_expiry()を使うことで、より柔軟な期限設定が可能です。

set_expiry()に渡せるvalue引数には、以下の種類があります。

  • None: valueNoneの場合、セッションはグローバルなセッション有効期限ポリシー(通常はsettings.SESSION_COOKIE_AGEによって設定される期間)に戻ります。つまり、set_expiry()で設定されたカスタムの有効期限が解除されます。

  • 0: value0の場合、ユーザーのセッションクッキーは、ユーザーがウェブブラウザを閉じたときに期限切れになります。これは「ブラウザセッション」と呼ばれます。

  • datetime または timedelta オブジェクト: valuedatetimeオブジェクトの場合、セッションはその特定の日時に期限切れになります。timedeltaオブジェクトの場合、現在時刻から指定された期間後に期限切れになります。

  • 整数 (秒数): valueが整数である場合、そのセッションは、最後にセッションが変更されてから指定された秒数後に期限切れになります。例えば、request.session.set_expiry(300)とすると、5分後にセッションが期限切れになるように設定されます。セッションの読み込みは「活動」とはみなされず、有効期限はセッションが「変更」された最後の時点から計算されます。

使用例

Djangoのビュー関数内でセッションの有効期限を設定する一般的な方法です。

from django.shortcuts import render

def my_view(request):
    # セッションにデータを設定
    request.session['my_data'] = 'Hello, Django sessions!'

    # セッションの有効期限を300秒(5分)に設定
    request.session.set_expiry(300)

    # または、ブラウザを閉じたときにセッションを期限切れにする
    # request.session.set_expiry(0)

    # または、グローバルな設定に戻す
    # request.session.set_expiry(None)

    return render(request, 'my_template.html', {'data': request.session.get('my_data')})

重要な点

  • 非同期バージョン: Django 5.1以降では、aset_expiry()という非同期バージョンも提供されています。
  • セッションバックエンド: set_expiry()はDjangoのセッションフレームワークの一部であり、使用しているセッションバックエンド(データベース、ファイル、キャッシュなど)によって、実際の挙動やデータの永続化方法が異なります。
  • セッションの読み込みは活動とみなされない: set_expiry()によって設定された有効期限は、セッションが最後に「変更」された時点から計算されます。単にセッションからデータを読み取るだけでは、有効期限は延長されません。


set_expiry()を設定したのにセッションが意図通りに期限切れにならない

問題の発生

  • Noneに設定したのに、グローバル設定に戻らない。
  • 特定の秒数を設定したのに、その時間よりも早く、または遅く期限切れになる。
  • set_expiry(0)を設定したのにブラウザを閉じてもセッションが維持される。

原因とトラブルシューティング

  • タイムゾーンの問題
    datetimeオブジェクトをset_expiry()に渡す場合、タイムゾーンに注意が必要です。Djangoは通常、USE_TZ = Trueの場合、タイムゾーン対応のdatetimeオブジェクトを期待します。タイムゾーンがずれていると、意図しない有効期限になることがあります。

    from django.utils import timezone
    from datetime import timedelta
    
    # 現在から1時間後のタイムゾーン対応のdatetimeオブジェクト
    future_time = timezone.now() + timedelta(hours=1)
    request.session.set_expiry(future_time)
    
  • デプロイメント環境の問題
    本番環境(Nginx, Apache, Gunicorn, uWSGIなど)でWebサーバーやアプリケーションサーバーのキャッシュ設定、リバースプロキシの設定などがセッションの挙動に影響を与えることがあります。特に、リバースプロキシがセッションクッキーを正しく転送していない場合や、独自のキャッシュを持っている場合に問題が発生することがあります。

    • Webサーバーのログや設定を確認してください。
    • セッションクッキーがクライアントに正しく送信されているか(ブラウザの開発者ツールで確認)。
  • キャッシュによる問題
    Djangoがキャッシュバックエンド(例: Memcached, Redis)を使用してセッションを保存している場合、キャッシュの期限設定とDjangoのセッション有効期限設定が競合する可能性があります。キャッシュバックエンド自体の設定(例: Memcachedのアイテムの最大有効期間)がDjangoのセッション有効期限よりも短い場合、セッションが早期に失われることがあります。

    • キャッシュバックエンドの有効期限設定を確認してください。
    • SESSION_CACHE_ALIAS設定を使用している場合は、正しいキャッシュエイリアスが設定されているか確認してください。
  • SESSION_EXPIRE_AT_BROWSER_CLOSEとの混同
    settings.pySESSION_EXPIRE_AT_BROWSER_CLOSE設定は、セッションクッキーがブラウザを閉じたときに期限切れになるかどうかを制御します。set_expiry(0)はセッション自体をブラウザセッションに設定しますが、SESSION_EXPIRE_AT_BROWSER_CLOSE = Trueも同じ効果を持ちます。これらが意図しない形で影響し合っていないか確認してください。ただし、set_expiry(0)が優先されます。

  • セッションの読み込みは「活動」とみなされない
    set_expiry()は、セッションが最後に変更された時点から有効期限を計算します。単にセッションからデータを読み込むだけでは、有効期限は延長されません。セッションの有効期限を延長したい場合は、セッションデータを書き込むか、明示的にrequest.session.modified = Trueを設定する必要があります。

    # 悪い例 (セッションが更新されないため期限が延長されない可能性がある)
    # request.session['some_data'] # これだけでは期限は延長されない
    
    # 良い例 (セッションが更新され、期限が延長される)
    request.session['some_data'] = request.session.get('some_data', 0) + 1
    # または
    request.session.modified = True
    

SessionMiddlewareが適切に設定されていない

問題の発生

  • request.sessionオブジェクトが存在しない。
  • セッションが全く機能しない。

原因とトラブルシューティング

  • マイグレーションの実行
    INSTALLED_APPS'django.contrib.sessions'を追加した後、python manage.py migrateを実行してセッションデータを保存するためのデータベーステーブル(django_session)を作成したか確認してください。

  • INSTALLED_APPS設定の確認
    データベースセッションを使用する場合(デフォルト)、settings.pyINSTALLED_APPS'django.contrib.sessions'が含まれている必要があります。

セッションバックエンドの設定ミス

問題の発生

  • 予期せぬエラーが発生する。
  • サーバーを再起動するとセッションが消える。
  • セッションが永続化されない(リクエスト間で消える)。

原因とトラブルシューティング

  • SECRET_KEYの漏洩・変更
    SECRET_KEYが変更されると、既存の署名付きクッキーやセッションデータが無効になり、ユーザーがログアウトされたりセッションデータが失われたりします。本番環境でSECRET_KEYを頻繁に変更しないように注意してください。

  • SESSION_ENGINEの設定
    settings.pySESSION_ENGINEが正しく設定されているか確認してください。

    • データベース ('django.contrib.sessions.backends.db') (デフォルト)

      • INSTALLED_APPSdjango.contrib.sessionsがあるか。
      • manage.py migrateを実行してdjango_sessionテーブルが作成されているか。
      • データベースの接続設定が正しいか。
    • ファイル ('django.contrib.sessions.backends.file')

      • SESSION_FILE_PATHで指定されたディレクトリが存在し、Webサーバーがそのディレクトリへの書き込み権限を持っているか。
    • キャッシュ ('django.contrib.sessions.backends.cache'または'django.contrib.sessions.backends.cached_db')

      • CACHES設定が正しく行われているか。
      • MemcachedやRedisなどのキャッシュサーバーが起動しているか。
      • SESSION_CACHE_ALIASを使用している場合は、エイリアス名が正しいか。
      • ローカルメモリキャッシュ('django.core.cache.backends.locmem.LocMemCache')は、マルチプロセス環境や永続化が必要な本番環境には適していません。
  • デバッグログの活用
    セッションに関する問題を特定するために、Djangoのロギング設定を調整して、セッション関連の情報をより詳細に記録するようにすると役立ちます。

    # settings.py
    LOGGING = {
        'version': 1,
        'disable_existing_loggers': False,
        'handlers': {
            'console': {
                'class': 'logging.StreamHandler',
            },
        },
        'loggers': {
            'django.contrib.sessions': {
                'handlers': ['console'],
                'level': 'DEBUG', # DEBUGレベルでログを出力
                'propagate': False,
            },
            'django.security': { # SuspiciousOperationなどのセキュリティ関連エラーも確認
                'handlers': ['console'],
                'level': 'DEBUG',
                'propagate': False,
            },
        },
        'root': {
            'handlers': ['console'],
            'level': 'INFO',
        },
    }
    
  • ブラウザの挙動(特にset_expiry(0)の場合)
    set_expiry(0)は「ブラウザを閉じたときに期限切れ」を意味しますが、最近のブラウザはタブを閉じてもプロセスが完全に終了しない場合があります。これにより、ユーザーがブラウザを閉じてもセッションが維持されるように見えることがあります。これはDjango側の問題ではなく、ブラウザの設計によるものです。完全にセッションを終了させたい場合は、明示的なログアウト機能を提供することを検討してください。



例1: セッションの有効期限を固定の時間(秒数)に設定する

最も一般的な使い方の一つです。ユーザーの活動が一定期間ないと自動的にログアウトさせたい場合などに使います。

# myapp/views.py

from django.shortcuts import render, redirect
from django.urls import reverse

def set_expiry_5_minutes(request):
    """
    セッションの有効期限を5分に設定し、訪問回数をカウントするビュー。
    セッションが変更されるたびに有効期限がリセットされる。
    """
    # セッションに値を設定または更新することで、セッションが「変更」される
    visit_count = request.session.get('visit_count', 0)
    request.session['visit_count'] = visit_count + 1

    # 有効期限を300秒(5分)に設定
    request.session.set_expiry(300)

    message = f"このセッションは5分間活動がないと期限切れになります。訪問回数: {request.session['visit_count']}"
    return render(request, 'session_example.html', {'message': message})

def clear_session(request):
    """
    セッションをクリアするビュー。
    """
    request.session.flush() # セッションデータをすべて削除
    return redirect(reverse('set_expiry_5_minutes'))

# myapp/templates/session_example.html
# (簡単なHTMLテンプレート)
"""
<!DOCTYPE html>
<html>
<head>
    <title>Session Example</title>
</head>
<body>
    <h1>セッション有効期限の例</h1>
    <p>{{ message }}</p>
    <p><a href="{% url 'set_expiry_5_minutes' %}">もう一度訪問してセッションを更新</a></p>
    <p><a href="{% url 'clear_session' %}">セッションをクリアする</a></p>
</body>
</html>
"""

# myproject/urls.py (または myapp/urls.py)
# from django.urls import path
# from myapp import views
#
# urlpatterns = [
#     path('set-expiry-5min/', views.set_expiry_5_minutes, name='set_expiry_5_minutes'),
#     path('clear-session/', views.clear_session, name='clear_session'),
# ]

解説

  • ユーザーがこのページを5分以内に再訪問する限り、セッションは継続されます。5分以上アクセスがないと、セッションは期限切れとなり、visit_countはリセットされます。
  • request.session['visit_count'] = visit_count + 1 の行でセッションデータが変更されます。これにより、set_expiry(300)で設定された5分間の有効期限が、このリクエストが行われた時点からリセットされます。

例2: ブラウザを閉じるとセッションが期限切れになるように設定する

ログインセッションなど、ユーザーがブラウザを閉じたら必ずログアウトさせたい場合に便利です。

# myapp/views.py

from django.shortcuts import render, redirect
from django.urls import reverse

def set_expiry_on_browser_close(request):
    """
    セッションをブラウザを閉じると期限切れになるように設定するビュー。
    """
    if not request.session.get('logged_in'):
        request.session['logged_in'] = True
        request.session['username'] = 'testuser'
        # 有効期限をブラウザを閉じると期限切れになるように設定 (0秒)
        request.session.set_expiry(0)
        message = "ログインしました。ブラウザを閉じるとセッションは期限切れになります。"
    else:
        message = f"ようこそ、{request.session['username']}さん! ブラウザを閉じるまでログイン状態です。"

    return render(request, 'session_example.html', {'message': message})

# myproject/urls.py (または myapp/urls.py)
# urlpatterns = [
#     path('browser-close-expiry/', views.set_expiry_on_browser_close, name='browser_close_expiry'),
# ]

解説

  • logged_inというセッションキーを使って、ユーザーがすでにログインしているかどうかを模擬的に判断しています。
  • request.session.set_expiry(0) がキモです。これにより、セッションクッキーは「セッションクッキー」としてマークされ、ブラウザが閉じられると削除されます。

例3: セッションの有効期限を特定の絶対日時に設定する

定期的なセッションの強制更新や、特定のイベント期間中のみセッションを維持したい場合などに使います。

# myapp/views.py

from django.shortcuts import render, redirect
from django.urls import reverse
from datetime import datetime, timedelta
from django.utils import timezone # タイムゾーン対応の日時を扱うために重要

def set_expiry_absolute_time(request):
    """
    セッションの有効期限を特定の日時に設定するビュー。
    """
    # 現在から1時間後の絶対日時を計算 (タイムゾーン対応)
    expiry_time = timezone.now() + timedelta(hours=1)

    request.session['last_access'] = timezone.now().isoformat()
    # 有効期限を計算した絶対日時に設定
    request.session.set_expiry(expiry_time)

    message = f"このセッションは {expiry_time.strftime('%Y-%m-%d %H:%M:%S %Z')} に期限切れになります。" \
              f"最終アクセス: {request.session['last_access']}"
    return render(request, 'session_example.html', {'message': message})

# myproject/urls.py (または myapp/urls.py)
# urlpatterns = [
#     path('absolute-expiry/', views.set_expiry_absolute_time, name='absolute_expiry'),
# ]

解説

  • django.utils.timezoneを使うことが重要です。USE_TZ = True設定の場合、タイムゾーン対応のdatetimeオブジェクトを扱う必要があります。
  • set_expiry()datetimeオブジェクトを渡すことで、その日時を過ぎるとセッションが期限切れになります。この場合、ユーザーの活動に関わらず、指定された絶対日時でセッションが終了します。
  • timezone.now()timedeltaを組み合わせて、現在から1時間後のdatetimeオブジェクトを作成しています。

set_expiry()でカスタムの有効期限を設定した後、Djangoのグローバルなセッション設定(SESSION_COOKIE_AGE)に戻したい場合に使います。

# myapp/views.py

from django.shortcuts import render, redirect
from django.urls import reverse
from django.conf import settings

def reset_expiry_to_default(request):
    """
    セッションの有効期限をDjangoのデフォルト設定に戻すビュー。
    """
    current_message = request.session.get('custom_expiry_message', 'カスタム有効期限は設定されていません。')

    # セッションの有効期限をデフォルト設定に戻す
    request.session.set_expiry(None)

    # デフォルトの有効期限期間を取得 (settings.pyから)
    default_expiry_seconds = settings.SESSION_COOKIE_AGE
    default_expiry_minutes = default_expiry_seconds // 60

    message = f"セッションの有効期限をデフォルト設定に戻しました({default_expiry_minutes}分)。" \
              f"以前のメッセージ: {current_message}"
    request.session['custom_expiry_message'] = message # セッションを更新して変更を保存

    return render(request, 'session_example.html', {'message': message})

def set_custom_expiry(request):
    """
    カスタムの有効期限を一時的に設定するビュー(比較用)。
    """
    request.session['custom_expiry_message'] = "カスタム有効期限が設定されました(1分)。"
    request.session.set_expiry(60) # 1分後に期限切れ

    return render(request, 'session_example.html', {'message': request.session['custom_expiry_message']})

# myproject/urls.py (または myapp/urls.py)
# urlpatterns = [
#     path('reset-expiry/', views.reset_expiry_to_default, name='reset_expiry_to_default'),
#     path('set-custom-expiry/', views.set_custom_expiry, name='set_custom_expiry'),
# ]
  • この例では、set_custom_expiryで一時的に短い有効期限を設定し、reset_expiry_to_defaultでそれを解除する流れを示しています。
  • request.session.set_expiry(None) は、そのセッションに対するカスタム有効期限設定を解除し、settings.SESSION_COOKIE_AGE(デフォルトでは2週間)などのグローバル設定に戻します。


settings.pyでのグローバル設定

set_expiry()は個別のセッションに対して有効期限を設定するのに対し、settings.pyで定義される設定は、アプリケーション全体のセッション動作に影響を与えます。

  • SESSION_SAVE_EVERY_REQUEST: これをTrueに設定すると、すべてのリクエストでセッションが保存されます。これにより、SESSION_COOKIE_AGEで設定された有効期限が、リクエストのたびに「最後にセッションが変更された時点」ではなく、「最後にセッションがアクセスされた時点」から再計算されるようになります。これは、ユーザーがアクティブである限りセッションを延長し続けたい場合に特に有用です。ただし、パフォーマンスに影響を与える可能性があるため、注意して使用する必要があります。

    # settings.py
    SESSION_SAVE_EVERY_REQUEST = True
    

    注意点: set_expiry()を併用する場合、SESSION_SAVE_EVERY_REQUESTTrueでも、set_expiry()によって設定された具体的な有効期限が優先されます。SESSION_SAVE_EVERY_REQUESTは、あくまでセッションが保存されることで期限が更新されるメカニズムを提供します。

  • SESSION_EXPIRE_AT_BROWSER_CLOSE: これをTrueに設定すると、ユーザーがウェブブラウザを閉じると、セッションクッキーも同時に期限切れになります。これはset_expiry(0)と同じ効果を持ち、一般的に「ブラウザセッション」と呼ばれます。

    # settings.py
    SESSION_EXPIRE_AT_BROWSER_CLOSE = True
    

    注意点: set_expiry(0)Trueに設定されている場合、SESSION_EXPIRE_AT_BROWSER_CLOSEよりも優先されます。

  • SESSION_COOKIE_AGE: これは、セッションクッキーの有効期間を秒単位で設定する最も一般的な方法です。ユーザーがブラウザを閉じても、この期間中はセッションが維持されます。set_expiry(None)を呼び出すと、セッションの有効期限はこの設定に戻ります。

    # settings.py
    SESSION_COOKIE_AGE = 60 * 60 * 24 * 7  # 1週間 (秒数)
    

カスタムミドルウェアの作成

より複雑なセッション管理ロジック(例:特定の条件下でのみセッションを延長する、ユーザーの役割に基づいて異なる有効期限を設定する)が必要な場合、カスタムミドルウェアを作成することが効果的です。

例:非アクティブなユーザーを自動的にログアウトさせるミドルウェア(SESSION_SAVE_EVERY_REQUEST = Falseの場合に特に有効)

# myapp/middleware.py

from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth import logout
from django.utils import timezone

class AutoLogoutMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.timeout_seconds = getattr(settings, 'AUTO_LOGOUT_TIMEOUT', 60 * 30) # 30分

    def __call__(self, request):
        if request.user.is_authenticated:
            last_activity = request.session.get('last_activity')
            now = timezone.now()

            if last_activity:
                last_activity_dt = datetime.fromisoformat(last_activity)
                # タイムゾーン対応の比較
                if (now - last_activity_dt).total_seconds() > self.timeout_seconds:
                    logout(request)
                    request.session['logout_message'] = "セッションがタイムアウトしました。再度ログインしてください。"
                    # 必要であれば、リダイレクト
                    # return redirect(settings.LOGIN_URL)
            
            # セッションを更新し、次回のチェックのために現在時刻を保存
            request.session['last_activity'] = now.isoformat()
            request.session.modified = True # セッションが変更されたことを明示

        response = self.get_response(request)
        return response

# settings.pyに追加
# MIDDLEWARE = [
#     ...
#     'django.contrib.sessions.middleware.SessionMiddleware',
#     'myapp.middleware.AutoLogoutMiddleware', # SessionMiddlewareの後に配置
#     ...
# ]
# AUTO_LOGOUT_TIMEOUT = 60 * 10 # 10分

解説

  • request.session.modified = Trueは、セッションデータが変更されたことをDjangoに明示的に伝えるために重要です。これにより、セッションが実際に保存され、有効期限が更新される(SESSION_SAVE_EVERY_REQUESTFalseの場合)か、新しいlast_activityの値が永続化されます。
  • もし現在の時刻とlast_activityの差がAUTO_LOGOUT_TIMEOUTを超えていれば、ユーザーをログアウトさせます。
  • このミドルウェアは、認証済みユーザーのリクエストごとにlast_activityというカスタムのセッションキーを更新します。

Djangoコミュニティには、セッション管理をより簡単にするためのサードパーティパッケージがいくつかあります。これらは、set_expiry()settings.pyの設定を組み合わせることで、さらに高度な機能を提供します。

  • django-session-security: このパッケージは、単なるタイムアウトだけでなく、セッションが期限切れになる前にユーザーに警告を表示する機能や、特定のURLではセッションの活動時間を更新しないなどのより高度なセキュリティ機能を提供します。

  • django-session-timeout: このパッケージは、ユーザーの非アクティブ期間に基づいてセッションをタイムアウトさせる機能を提供します。SESSION_EXPIRE_SECONDSSESSION_EXPIRE_AFTER_LAST_ACTIVITYなどの設定をsettings.pyに追加するだけで、簡単に導入できます。

    # settings.py
    # pip install django-session-timeout
    
    MIDDLEWARE = [
        # ...
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django_session_timeout.middleware.SessionTimeoutMiddleware',
        # ...
    ]
    
    SESSION_EXPIRE_SECONDS = 3600  # 1時間
    SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True # 最終活動から期限切れにする
    # SESSION_TIMEOUT_REDIRECT = '/login/' # タイムアウト後のリダイレクトURL
    

    これは、set_expiry()を自分で管理する手間を省き、多くのシナリオで望ましい「非アクティブタイムアウト」の挙動を実現します。

sessions.backends.base.SessionBase.set_expiry()は、個々のセッションに対して有効期限をきめ細かく制御したい場合に非常に便利です。しかし、アプリケーション全体で一貫したセッションポリシーを適用したい場合は、settings.pySESSION_COOKIE_AGESESSION_EXPIRE_AT_BROWSER_CLOSE、そしてSESSION_SAVE_EVERY_REQUESTといったグローバル設定が主要な代替手段となります。