PurePHP のセッションで突然セッションが切れるという不可解な挙動があったので調べてみました。
実験
実験した環境は PHP 8.4.16 です。
用意したのは以下のスクリプトです。現象を再現しやすくするため、GC 保存期間を60秒、毎回 GC 実施するように設定しています。
<?php
header("Content-Type: text/plain");
$options = [
'gc_maxlifetime' => 60,
'gc_probability' => 1,
'gc_divisor' => 1
];
session_start($options);
var_dump($_SESSION);
$timestamp = time();
$_SESSION['timestamp'] = $timestamp;
var_dump($timestamp);
1回目アクセス結果
array(0) {
}
int(1775725914)
$ ls -l sess_*
-rw------- 1 www wheel 23 4月 9 18:11 sess_37d3f3d3dda653793bbc7118eb76a275
$_SESSION は空です。セッションファイルが作成されました。
2回目アクセス結果(1回目から60秒以内)
array(1) {
["timestamp"]=>
int(1775725914)
}
int(1775725964)
$ ls -l sess_*
-rw------- 1 www wheel 23 4月 9 18:12 sess_37d3f3d3dda653793bbc7118eb76a275
1回目アクセス時に格納したデータが $_SESSION に入っています。セッションファイルは1回目と同一のものが存在します。
3回目アクセス結果(2回目から60秒以上)
array(1) {
["timestamp"]=>
int(1775725964)
}
int(1775726086)
$ ls -l sess_*
ls: sess_*: そのようなファイルまたはディレクトリはありません
引き続き $_SESSION にはデータが入っていますが、セッションファイルは GC により削除されました。
4回目アクセス結果(3回目から60秒以内)
array(0) {
}
int(1775726109)
$ ls -l sess_*
-rw------- 1 www wheel 23 4月 9 18:15 sess_37d3f3d3dda653793bbc7118eb76a275
セッションファイルが存在しなかったので $_SESSION は空です。セッションファイルは新たに作成されました(デフォルトのセッション Cookie がブラウザを閉じるまで有効なためセッション ID は1回目からと同一となる)。
不可解な挙動
実験結果から、以下のような挙動が発生することが分かります。
- セッションを使用したシステムを利用する(ログイン等)
- この時点では普通にシステムが利用できる
- セッションを使用しているので当然
- 1 からある程度時間が経過してから再度システムにアクセスする。
- セッションが生きていてログイン状態等が再現される。
- 「お、久しぶりにアクセスしたけどセッション生きてる」
- 実験3回目アクセス相当。セッションは復元されるけど、GC によりセッションファイルは削除される。
- 2 から別のページに遷移するとセッションが切れる(ログアウト等)
- 「え、なんで。さっきログインしてたのにいきなりログアウトされるの?」
- 実験4回目アクセス相当。セッションファイルが削除されているので、セッションは切れている。
1 から 2 が十分に長い時間で 2 から 3 が短い時間だった場合、かなり不自然なユーザ体験となります。これが今回遭遇した不可解な挙動です。
この挙動が発生する原因は、実験3回目アクセスの時の PHP のセッション処理にあります。十分時間が経過して GC 対象となるような古いセッションファイルにも関わらず読み取って $_SESSION を復元するのに、セッションファイル自体は GC の対象として削除してしまうためです。以下のいずれかの処理になっていればこのような問題は発生しなかったでしょう。
- セッションの復元より先に GC を実施する。
-
$_SESSIONが空でなかったら、必ずセッションファイルを書き出す。
もっとも、この指摘は的外れでして、そもそもセッションの GC はセッションタイムアウトに使用するべきではありません。GC はあくまでもゴミ(不要になったセッション)を掃除するだけであって、セッションタイムアウトのための機構ではないからです。また、デフォルトの設定では GC は確率的にしか実行されないため、いつ削除されるかは予測不可能です。
また、1 から 2 の間に GC が行われてないために不可解な挙動が発生しています。もしも GC が実施されていれば 2 の不可解な挙動はありません。1 から 2 の間に該当ユーザ以外も含めて、誰もシステムにアクセスしていなかったために発生しています。他のユーザが頻繁にアクセスするようなシステムでは表面化しなかったでしょう。実際、今回遭遇したのは自分しか利用していない自宅サーバでした。
対応策
このような不可解な挙動を回避するためにはどうしたらいいか。対応策を考えました。
セッションタイムアウトを自分で実装する
これが王道でしょう。以下のような処理を session_start() の直後に追加します。
if(isset($_SESSION['timestamp']) && time() - $_SESSION['timestamp'] > 60) {
$_SESSION = [];
}
王道ではあるのですが、PHP のセッション機構に最初からこのような仕組みが用意されていればいちいち自前で処理を書かなくて済むのになとも思います。
session_regenerate_id() を呼び出す
session_regenerate_id() はセッションIDを再作成することから、必ずセッションファイルを書き出してくれます。上述の
$_SESSIONが空でなかったら、必ずセッションファイルを書き出す。
を実施するのと同等ということですね。
とはいえ、本来の目的とは違う用途で session_regenerate_id() を呼び出すのは少々もやもやします。セキュリティ向上という意味では呼び出していいのですが。
gc_maxlifetime を伸ばす
gc_maxlifetime を十分に大きな値にすれば GC の頻度が下がりますので、不可解な挙動に遭遇することも減るでしょう。デフォルトの値が 1440(24分)ですから 86400(1日) とかにするのは十分ありだとは思います。
根本的な解決にはなっていませんので、他の対応策がとれなくてアドホックに対応する場合に限るでしょうか。