Django でログインセッションの有効期限を設定するのが意外と大変だった

# 概要

- [Django 3.0](https://docs.djangoproject.com/ja/3.0/) には最初からユーザーのログイン認証とセッション管理の機能があります
- しかしデフォルト設定のままでは今回のセキュリティ要件を満たせなかったので、設定を変更する必要がありました
- ブラウザを閉じたらセッションを終了する要件については、条件付きでの実現となりました

# それぞれの要件と対応

## 1. 任意の有効期間を設定したい

- デフォルト: 2 週間
- 希望要件: 15分間 〜 2時間の任意の期間に設定したい

### 設定方法

settings.py で定義します。
初期状態の settings.py では定義が省略されているので、例えば 1 時間に設定したい場合は以下の行を追記します。

```Ini:settings.py
SESSION_COOKIE_AGE = 3600
```
参考:
[SESSION_COOKIE_AGE](https://docs.djangoproject.com/ja/3.0/ref/settings/#std:setting-SESSION_COOKIE_AGE)

### 確認方法

データベースに接続して、新しいログインによって `django_session` テーブルに挿入された行の `expire_date` 列の値が、ログイン時の UTC 時刻 + 設定した秒数になっていることで確認できます。

## 2. スマホのスリープのように、最後に操作してから放置した時間で期限切れにしたい

- デフォルト: ユーザーの操作に関係なく、ログイン時から `SESSION_COOKIE_AGE` 秒後に期限が切れる
- 希望要件: 最後にいずれかのページをロードしてから `SESSION_COOKIE_AGE` 秒後に期限切れとしたい

### 設定方法

これも初期状態の settings.py では定義が省略されているので、以下の行を追記します。

```Ini:settings.py
SESSION_SAVE_EVERY_REQUEST = True
```

参考:
[セッションが保存されるタイミング](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/#when-sessions-are-saved)
[SESSION_SAVE_EVERY_REQUEST](https://docs.djangoproject.com/ja/3.0/ref/settings/#std:setting-SESSION_SAVE_EVERY_REQUEST)

### 確認方法

データベースに接続して、画面をリロードするたびに `django_session` テーブルの `expire_date` 列の値が更新されることで確認できます。
デフォルトではログイン認証時に `expire_date` 列の値が決まり、その後の操作で `expire_date` 列の値は更新されません。

## 3. セッションをデータベースで管理したい

- デフォルト: セッションをデータベースで管理する

これはデフォルトのままで希望どおりです。
サーバーのローカルメモリで管理する場合に比べてパフォーマンス面では不利ですが、ロードバランサーなどで前回と違うサーバーにリクエストが届いた場合でも正しく処理されます。

### 設定方法

初期状態の settings.py から変更する必要はありません。以下のとおりになっていればデータベースでセッションが管理されます。

- `INSTALLED_APPS` に `"django.contrib.sessions"` が含まれていること
- `MIDDLEWARE` に `"django.contrib.sessions.middleware.SessionMiddleware"` が含まれていること
- `SESSION_ENGINE` が定義されていないか、または設定値が `django.contrib.sessions.backends.db` であること

参考: [セッションエンジンを設定する](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/#configuring-the-session-engine)

### 確認方法

データベースに接続して、ログインのたびに `django_session` テーブルに新しい行が挿入されることで確認できます。

### 課題

`django_session` テーブルに保存されたセッションの行は、明示的なログアウト処理（内部で [django.contrib.auth.logout()](https://docs.djangoproject.com/ja/3.0/topics/auth/default/#how-to-log-a-user-out) の呼び出し）では削除されるのですが、有効期限が切れただけでは自動的に削除されません。

このため行が増え続けることを防ぐためには、以下のコマンドを時々実行して期限切れのセッションを削除する必要があります。

```Shell:
python manage.py clearsessions
```

## 4. Web ブラウザを閉じたらセッションを終了したい

- デフォルト: ブラウザを閉じても `SESSION_COOKIE_AGE` で設定された期限までセッションが維持される
- 希望要件: 設定された期限に関係なく、ブラウザを閉じたらセッションを終了したい

### 設定方法

これも初期状態の settings.py では省略されているので、以下の行を追記します。

```Ini:settings.py
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
```

参考:
[ブラウザ起動中のみ有効なセッション vs. 永続的なセッション](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/#browser-length-sessions-vs-persistent-sessions)
[SESSION_EXPIRE_AT_BROWSER_CLOSE](https://docs.djangoproject.com/ja/3.0/ref/settings/#std:setting-SESSION_EXPIRE_AT_BROWSER_CLOSE)

### 確認方法

ブラウザを閉じてから起動して、同じページを開こうとするとログインを求められます。
デフォルトでは再ログインすることなく同じページが開きます。

### 課題

この動作はブラウザ自身が終了時に cookie を破棄するかどうかに依存しているため、以下のブラウザと設定では、ブラウザを閉じてもセッションが終了しませんでした。

- Google Chrome 85: 「設定」>「起動時」>「前回開いていたページを開く」が選択されている場合
- Firefox 80: 「設定」>「一般」>「起動」>「前回のセッションを復元する」が有効な場合

# 参考

- [公式ドキュメント: セッションの使いかた](https://docs.djangoproject.com/ja/3.0/topics/http/sessions/)
- [公式ドキュメント: 認証システムを使う](https://docs.djangoproject.com/ja/3.0/topics/auth/default/)
- [Djangoのセッション設定 - 知的好奇心](https://intellectual-curiosity.tokyo/2018/11/10/django%E3%81%AE%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E8%A8%AD%E5%AE%9A/)