Djangoセッションの有効期限を徹底解説: get_expiry_age()の基本と活用法

2025-05-27

Djangoにおける sessions.backends.base.SessionBase.get_expiry_age() は、セッションがどれくらいの期間で期限切れになるかを秒単位で返すメソッドです。これは、すべてのセッションバックエンドの基底クラスである SessionBase で定義されています。

役割と機能

このメソッドの主な役割は、現在のセッションの有効期限までの残りの時間を秒数で計算して提供することです。具体的には、以下のいずれかの情報に基づいて計算を行います。

  • ブラウザを閉じると期限切れになるように設定されている場合
    set_expiry(0) が呼び出された場合など、ブラウザを閉じるとセッションが期限切れになるように設定されている場合でも、SESSION_COOKIE_AGE の値が返されます。これは、ブラウザを閉じると期限切れになるという挙動は、サーバー側で管理される「残り時間」としては特定できないため、実質的にデフォルトの最大有効期間が返されると理解できます。
  • カスタムの有効期限が設定されていない場合
    セッションに明示的な有効期限が設定されていない場合、Djangoの設定ファイル settings.py で定義されている SESSION_COOKIE_AGE の値(デフォルトは2週間)が返されます。
  • カスタムの有効期限が設定されている場合
    request.session.set_expiry() メソッドなどを使って明示的にセッションの有効期限(特定の datetime オブジェクト、timedelta オブジェクト、または秒数)が設定されている場合、その設定に基づいて残り時間が計算されます。

引数

get_expiry_age() は、以下のオプションのキーワード引数を受け入れます。

  • expiry: セッションの有効期限情報を示す datetime オブジェクト、整数(秒単位)、または None。通常は set_expiry() で設定された値がデフォルトで使われますが、これを明示的に渡すことも可能です。
  • modification: セッションが最後に変更された日時を示す datetime オブジェクト。指定されない場合、現在の時刻が使用されます。

使用例

通常、request.session.get_expiry_age() のように、HttpRequest オブジェクトの session 属性を通じてアクセスします。

from django.shortcuts import render
from datetime import timedelta
from django.utils import timezone

def my_view(request):
    # セッションに何かデータを保存
    request.session['last_access'] = str(timezone.now())

    # セッションの残り有効期間を秒単位で取得
    # カスタムの有効期限が設定されていなければ、settings.SESSION_COOKIE_AGE の値が返される
    remaining_age = request.session.get_expiry_age()
    print(f"セッションの残り有効期間 (秒): {remaining_age}")

    # 例: セッションを5分後に期限切れにする
    request.session.set_expiry(300) # 300秒 = 5分
    remaining_age_after_setting = request.session.get_expiry_age()
    print(f"カスタム設定後のセッションの残り有効期間 (秒): {remaining_age_after_setting}")

    # 明示的な日時でセッションを期限切れにする
    future_time = timezone.now() + timedelta(hours=1)
    request.session.set_expiry(future_time)
    remaining_age_datetime = request.session.get_expiry_age()
    print(f"日時設定後のセッションの残り有効期間 (秒): {remaining_age_datetime}")

    return render(request, 'my_template.html', {})
  • このメソッドは、主にセッションクッキーの Max-Age ヘッダーを設定するために内部的に使用されますが、開発者がセッションの残り時間を把握したい場合にも役立ちます。
  • set_expiry(0) を使用してブラウザを閉じると期限切れになるように設定した場合、get_expiry_age()settings.SESSION_COOKIE_AGE の値を返します。これは、ブラウザ側の挙動(ブラウザが閉じられるとクッキーが削除される)がサーバー側で具体的な残り秒数として把握できないためです。
  • get_expiry_age() は、セッションが最後に変更された時点からの残り時間を計算します。Djangoのセッションは、データが変更されるたびに有効期限が更新されるのが一般的な挙動です(SESSION_SAVE_EVERY_REQUEST 設定によりますが、通常はデータを変更しないと保存されません)。


set_expiry(0) または set_expiry(None) を設定したのに get_expiry_age() が SESSION_COOKIE_AGE を返す

問題の現象
セッションを「ブラウザを閉じると期限切れにする」ために request.session.set_expiry(0) を呼び出したり、グローバル設定に戻すために request.session.set_expiry(None) を呼び出したりしても、get_expiry_age()settings.SESSION_COOKIE_AGE の値(通常は1209600秒 = 2週間)を返してしまう。これにより、セッションが期待通りに期限切れにならないと誤解することがあります。

原因
これは Django の仕様であり、バグではありません。get_expiry_age() メソッドは、以下のロジックに基づいています。

  • このため、このようなケースでは、get_expiry_age()settings.SESSION_COOKIE_AGE の値を返します。これは、実質的に「デフォルトの最大有効期間」を表していると解釈できます。
  • set_expiry(0) (ブラウザを閉じると期限切れ) や set_expiry(None) (グローバル設定に従う) の場合、サーバー側で具体的な残り時間を計算することはできません。特に 0 の場合は、ブラウザが閉じたときにクッキーが破棄されるというクライアント側の挙動に依存するため、サーバーは具体的な秒数を持ちません。
  • セッションにカスタムの有効期限が datetime オブジェクトとして明示的に設定されている場合のみ、現在時刻との差分を計算して残り秒数を返します。


基本的な使用例と有効期限の確認

この例では、セッションにデータを保存し、そのセッションの残り有効期間を get_expiry_age() で確認します。

# myapp/views.py
from django.shortcuts import render
from django.http import HttpResponse
from datetime import timedelta
from django.utils import timezone # タイムゾーン対応の日時を扱うために必要

def session_expiry_example(request):
    # セッションにデータを保存または更新すると、有効期限が更新される(SESSION_SAVE_EVERY_REQUEST が True の場合、または modified = True の場合)
    if 'visit_count' not in request.session:
        request.session['visit_count'] = 1
    else:
        request.session['visit_count'] += 1

    # セッションの残り有効期間を秒単位で取得
    # 明示的な有効期限が設定されていない場合、settings.SESSION_COOKIE_AGE (デフォルト2週間) が返される
    remaining_age = request.session.get_expiry_age()

    # セッションの有効期限日時を取得
    expiry_date = request.session.get_expiry_date()

    return HttpResponse(
        f"<h1>セッションの有効期限の例</h1>"
        f"<p>このページへの訪問回数: {request.session['visit_count']}</p>"
        f"<p>セッションの残り有効期間 (秒): {remaining_age}</p>"
        f"<p>セッションの有効期限日時: {expiry_date}</p>"
    )

  • SESSION_SAVE_EVERY_REQUEST = True に設定すると、すべてのリクエストでセッションが保存され、有効期限が自動的に延長されます(パフォーマンスへの影響を考慮)。
  • settings.pySESSION_COOKIE_AGE の値を変更して、デフォルトのセッション有効期限を調整できます。

カスタムの有効期限を設定した場合

set_expiry() メソッドを使用してセッションの有効期限を明示的に設定し、その後の get_expiry_age() の挙動を確認します。

# myapp/views.py
from django.shortcuts import render
from django.http import HttpResponse
from datetime import timedelta
from django.utils import timezone

def set_custom_expiry_example(request):
    message = []

    if request.method == 'POST':
        # フォームから受け取った秒数で有効期限を設定
        try:
            expiry_seconds = int(request.POST.get('expiry_seconds', 0))
            if expiry_seconds == 0:
                # ブラウザを閉じると期限切れ
                request.session.set_expiry(0)
                message.append("セッションはブラウザを閉じると期限切れになるように設定されました。")
            elif expiry_seconds > 0:
                # 指定した秒数で期限切れ
                request.session.set_expiry(expiry_seconds)
                message.append(f"セッションは {expiry_seconds} 秒後に期限切れになるように設定されました。")
            else:
                # 無効な入力
                message.append("無効な秒数が指定されました。")
        except ValueError:
            message.append("秒数は数値で入力してください。")

        # セッションのデータを更新して、保存をトリガー
        request.session['last_set_time'] = str(timezone.now())
        request.session.modified = True # 明示的にセッションを保存対象にする

    # 現在のセッションの残り有効期間と期限日時を取得
    remaining_age = request.session.get_expiry_age()
    expiry_date = request.session.get_expiry_date()

    return HttpResponse(
        f"<h1>カスタム有効期限の設定例</h1>"
        f"<p>{'<br>'.join(message)}</p>"
        f"<p>現在のセッション残り有効期間 (秒): {remaining_age}</p>"
        f"<p>現在のセッション有効期限日時: {expiry_date}</p>"
        f"<form method='post'>"
        f"  <label for='expiry_seconds'>セッションを何秒後に期限切れにしますか? (0 でブラウザを閉じると期限切れ):</label>"
        f"  <input type='number' id='expiry_seconds' name='expiry_seconds' value='{remaining_age if remaining_age > 0 else 0}'>"
        f"  <button type='submit'>設定</button>"
        f"</form>"
    )

urls.py の設定例

# myproject/urls.py
from django.contrib import admin
from django.urls import path
from myapp import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('session_expiry/', views.session_expiry_example, name='session_expiry_example'),
    path('set_custom_expiry/', views.set_custom_expiry_example, name='set_custom_expiry_example'),
]

get_expiry_age() の挙動のポイント

  • set_expiry(datetime_object) の場合
    timedelta ではなく、特定の datetime オブジェクトで設定した場合、get_expiry_age() は現在時刻と指定された datetime の差分を計算して返します。
  • set_expiry(None) の場合
    グローバルな SESSION_COOKIE_AGE 設定に戻したい場合は request.session.set_expiry(None) を呼び出します。この場合も get_expiry_age()settings.SESSION_COOKIE_AGE を返します。
  • set_expiry(0) の場合
    この例で 0 を入力して設定すると、get_expiry_age()settings.SESSION_COOKIE_AGE (デフォルト2週間) を返します。これは、ブラウザを閉じるとセッションが期限切れになるという挙動は、サーバー側で具体的な残り時間を計算できないためです。実際のセッションクッキーには expires 属性が設定されなくなり、ブラウザが閉じられると破棄されます。

ユーザーの活動がない場合にのみセッションを期限切れにしたい、という場合に、ミドルウェアでセッションの有効期限を管理する例です。これは get_expiry_age() を直接利用するものではありませんが、セッションの有効期限管理の文脈で関連します。

多くのケースでは SESSION_SAVE_EVERY_REQUEST = True が手軽ですが、より細かく制御したい場合にミドルウェアを検討します。

# myapp/middleware.py
from django.conf import settings
from datetime import timedelta
from django.utils import timezone

class SessionIdleTimeoutMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.idle_timeout = getattr(settings, 'SESSION_IDLE_TIMEOUT', None) # settings.py で設定

    def __call__(self, request):
        if not request.session.session_key:
            # 新しいセッション
            return self.get_response(request)

        # 最後の活動日時をセッションから取得
        last_activity_str = request.session.get('last_activity')
        if last_activity_str:
            last_activity = timezone.datetime.fromisoformat(last_activity_str)
        else:
            last_activity = None

        # 現在時刻
        now = timezone.now()

        if self.idle_timeout and last_activity and (now - last_activity > timedelta(seconds=self.idle_timeout)):
            # アイドルタイムアウトを超えた場合、セッションをクリア(ログアウト相当)
            request.session.flush()
            request.session['logged_out_by_timeout'] = True # フラグを設定してビューでメッセージ表示など

        # 最後に活動した日時を更新
        request.session['last_activity'] = now.isoformat()
        request.session.modified = True # セッションが変更されたことをマークして保存を強制

        response = self.get_response(request)
        return response

settings.py での設定

# settings.py
# ...
MIDDLEWARE = [
    # ...
    'myapp.middleware.SessionIdleTimeoutMiddleware', # 他のミドルウェアの後に
    'django.contrib.sessions.middleware.SessionMiddleware', # SessionMiddleware は必ずこのミドルウェアの後に
    # ...
]

# ユーザーの活動がない場合にセッションを期限切れにする秒数 (例: 30分)
SESSION_IDLE_TIMEOUT = 30 * 60 # 30分

このミドルウェアは、get_expiry_age() を直接呼び出すわけではありませんが、セッションの有効期限管理の文脈で、ユーザーの活動に基づいてセッションを管理する一般的なアプローチを示しています。get_expiry_age() は、このミドルウェアがセッションの現在の有効期限を把握したい場合に利用できます。



以下に、get_expiry_age() の代替となるプログラミング方法や、関連するセッション管理のアプローチについて説明します。

request.session.get_expiry_date() の使用

get_expiry_age() が残り秒数を返すのに対し、get_expiry_date() はセッションが正確にいつ期限切れになるかを datetime オブジェクトで返します。

用途

  • セッションがいつまで有効かを正確に把握し、その情報に基づいて別の処理(例: 「あとX時間でログアウトします」といった警告表示)を行いたい場合。
  • セッションの有効期限を具体的な日付と時刻として表示したい場合。

利点

  • set_expiry(0) の場合も、settings.SESSION_COOKIE_AGE に基づいた具体的な日時を返すため、get_expiry_age() のように settings.SESSION_COOKIE_AGE の値がそのまま返ってくることによる混乱を避けられる。
  • 時間計算を自分で行う必要がない。


from django.shortcuts import render
from django.http import HttpResponse

def show_expiry_date(request):
    expiry_date = request.session.get_expiry_date()
    # expiry_date は datetime オブジェクト

    return HttpResponse(
        f"<h1>セッションの有効期限</h1>"
        f"<p>このセッションは <strong>{expiry_date.strftime('%Y年%m月%d日 %H時%M分%S秒')}</strong> に期限切れになります。</p>"
    )

セッションにカスタムのタイムスタンプを保存する

Django の組み込みセッション機能に頼らず、セッション辞書の中に独自の最終活動タイムスタンプを保存し、それに基づいて有効期限を管理する方法です。

用途

  • より複雑なセッション管理ロジックを実装したい場合。
  • SESSION_SAVE_EVERY_REQUESTTrue にしたくないが、特定のアクションがあった場合にのみ有効期限を延長したい場合。
  • ユーザーのアイドル時間に基づいてセッションを管理したい場合(例: 30分間操作がなければログアウト)。

利点

  • 特定のイベント(例: ユーザーがフォームを送信した時)に基づいてのみ有効期限を更新できる。
  • 有効期限のロジックを完全に制御できる。

欠点

  • セッションの永続化とクッキーの管理は Django のセッションバックエンドに依存するため、完全に代替するわけではない。
  • 手動で管理する手間が増える。


from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from datetime import timedelta
from django.utils import timezone

def login_view(request):
    # ログイン成功時
    if request.method == 'POST':
        # ... 認証処理 ...
        request.session['user_id'] = 123
        request.session['last_activity'] = timezone.now().isoformat() # ISOフォーマットで保存
        request.session.set_expiry(request.session.get_expiry_age()) # Djangoの有効期限も更新
        return HttpResponseRedirect(reverse('dashboard'))
    return render(request, 'login.html')

def dashboard_view(request):
    if 'user_id' not in request.session:
        return HttpResponseRedirect(reverse('login'))

    last_activity_str = request.session.get('last_activity')
    if last_activity_str:
        last_activity = timezone.datetime.fromisoformat(last_activity_str)
        idle_timeout_seconds = 30 * 60 # 30分
        if timezone.now() - last_activity > timedelta(seconds=idle_timeout_seconds):
            # アイドルタイムアウトを超えた場合
            request.session.flush() # セッションをクリア
            return HttpResponse("セッションがタイムアウトしました。再度ログインしてください。", status=401)
    
    # ユーザーが活動した場合、タイムスタンプを更新
    request.session['last_activity'] = timezone.now().isoformat()
    request.session.modified = True # セッションが変更されたことをマークし、保存を強制

    remaining_session_age = request.session.get_expiry_age() # Djangoのセッション残り時間

    return HttpResponse(
        f"<h1>ダッシュボード</h1>"
        f"<p>ログインユーザー: {request.session['user_id']}</p>"
        f"<p>カスタム最終活動時刻: {last_activity_str}</p>"
        f"<p>Djangoセッション残り有効期間 (秒): {remaining_session_age}</p>"
    )

SESSION_COOKIE_AGE および SESSION_EXPIRE_AT_BROWSER_CLOSE の活用

get_expiry_age() は現在のセッションの残り時間を返しますが、システム全体のセッション有効期限のデフォルト挙動を制御したい場合は、settings.py のこれらの設定を直接調整します。

用途

  • ブラウザを閉じたらセッションを自動的に終了させたい場合。
  • アプリケーション全体のセッションのデフォルト有効期間を設定したい場合。

利点

  • カスタムロジックを書く必要がない。
  • 簡単な設定変更で全体に適用できる。

例 (settings.py)

# settings.py

# デフォルトのセッション有効期限を1日に設定 (秒単位)
SESSION_COOKIE_AGE = 60 * 60 * 24 # 1日

# ブラウザを閉じるとセッションを期限切れにする
# 個別のセッションで request.session.set_expiry(0) と同じ効果
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

# すべてのリクエストでセッションを保存し、有効期限を延長する
# これを True にすると、ユーザーがアクティブな限りセッションは期限切れにならない傾向にある
# ただし、パフォーマンスへの影響を考慮する必要がある
# SESSION_SAVE_EVERY_REQUEST = False # デフォルトは False

非常に特殊なセッション管理要件がある場合、Django の SessionBase を継承して独自のセッションバックエンドを作成することができます。これにより、get_expiry_age() などのメソッドの挙動をオーバーライドしたり、セッションデータの保存・取得ロジックを完全にカスタマイズしたりできます。

用途

  • 複数のシステムでセッションを共有するなど、複雑な連携が必要な場合。
  • Django の既存のセッションバックエンドでは実現できない、独自の永続化ロジックや有効期限管理ロジックを必要とする場合。

利点

  • アプリケーションの特定のニーズに完全に合致するセッション管理を実現できる。
  • 究極の柔軟性。

欠点

  • Django のセッションシステムの内部動作に関する深い知識が必要。
  • 実装の複雑性が大幅に増す。

例 (概念)

# myapp/custom_session_backend.py
from django.contrib.sessions.backends.db import SessionStore as DBStore
from datetime import timedelta
from django.utils import timezone

class MyCustomSessionStore(DBStore):
    # DBStore のメソッドをオーバーライドしたり、新しいメソッドを追加したりする
    
    def get_expiry_age(self, modification=None, expiry=None):
        # 親クラスのロジックを呼び出すか、独自のロジックを実装する
        # 例: アイドルタイムアウトを考慮した残り時間を計算する
        if 'last_activity' in self._session:
            last_activity = timezone.datetime.fromisoformat(self._session['last_activity'])
            idle_timeout = timedelta(seconds=getattr(settings, 'MY_CUSTOM_IDLE_TIMEOUT', 3600)) # 例: 1時間
            
            time_since_last_activity = timezone.now() - last_activity
            
            if time_since_last_activity > idle_timeout:
                return 0 # 既にタイムアウト済み
            else:
                # 残りのアイドル時間を返すか、または元の有効期限も考慮に入れる
                # ここでは単純にアイドルタイムアウトの残り時間を返す
                return max(0, int((idle_timeout - time_since_last_activity).total_seconds()))
        
        # デフォルトの挙動に戻す
        return super().get_expiry_age(modification, expiry)

# settings.py でこのカスタムバックエンドを使用するように設定
# SESSION_ENGINE = 'myapp.custom_session_backend'

get_expiry_age() は特定の状況で便利ですが、セッションの有効期限を扱う方法はいくつかあります。