PHPのセッションの有効期限について

採用はこちら

PHPのセッションには、単一の「有効期限」が設定されているわけではありません。

実際には、次の3つを分けて考える必要があります。

項目管理場所主な設定・実装役割
セッションID Cookieの有効期間ブラウザsession.cookie_lifetimeブラウザがセッションIDを保持する期間
セッションデータの保存期間サーバーsession.gc_maxlifetime古いセッションデータを削除対象とみなす基準
ログイン状態の有効期間アプリケーションlast_activityや有効期限時刻実際にアクセスを許可する期間

この3つは別々に動作します。

そのため、php.iniで有効期限らしい値を1つ設定しただけでは、ログイン状態を正確に制御できない場合があります。

目次

PHPセッションの基本的な仕組み

PHPの標準的なセッションは、次の流れで動作します。

  1. PHPでsession_start()を実行する
  2. セッションIDが発行される
  3. ブラウザにセッションID Cookieが保存される
  4. セッションの実データはサーバー側に保存される
  5. 次回アクセス時にブラウザがセッションIDを送信する
  6. PHPが対応するセッションデータを読み込む

通常、ブラウザに保存されるのはログイン情報そのものではなく、セッションデータを識別するためのIDです。

標準的なCookie名は次のとおりです。

PHPSESSID

基本的な利用例は以下です。

<?php

session_start();

$_SESSION['user_id'] = 123;
$_SESSION['user_name'] = 'Yukino';

session.cookie_lifetimeとは

session.cookie_lifetimeは、ブラウザがセッションID Cookieを保持する期間を指定する設定です。

0を指定した場合

初期設定は通常、次のようになっています。

session.cookie_lifetime = 0

0は、セッションCookieとして発行することを意味します。

セッションCookieは、基本的にはブラウザのセッションが終了するまで保持されます。

ただし、ブラウザによってはセッション復元機能が有効になっている場合があります。

その場合、ブラウザを閉じても、次回起動時にセッションCookieが復元されることがあります。

そのため、次のように考える必要があります。

cookie_lifetime = 0

だからといって、

ブラウザを閉じれば必ずログアウトする

とは限りません。

ログイン状態の期限をCookieだけに依存させる設計は避けるべきです。

秒数を指定した場合

1時間保持する場合は、次のように設定します。

session.cookie_lifetime = 3600

PHPコードから指定する場合は、session_start()より前に設定します。

<?php

session_set_cookie_params([
    'lifetime' => 3600,
    'path' => '/',
    'domain' => '',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

session_start();

session_start()後に指定しても、すでにCookieが発行されているため、期待どおりに反映されない可能性があります。

session.gc_maxlifetimeとは

session.gc_maxlifetimeは、サーバー側のセッションデータを古いデータとして扱うまでの秒数です。

標準的な設定値は次のとおりです。

session.gc_maxlifetime = 1440

1440秒は24分です。

30分に設定する場合は次のようにします。

session.gc_maxlifetime = 1800

2時間の場合は以下です。

session.gc_maxlifetime = 7200

指定時間を超えても即座には削除されない

session.gc_maxlifetimeは、厳密な削除時刻ではありません。

たとえば次の設定を行ったとします。

session.gc_maxlifetime = 1800

この場合、30分を経過した瞬間にセッションデータが必ず削除されるわけではありません。

正確には、一定時間更新されていないセッションデータが、GCによる削除対象として扱われるようになります。

つまり、次のような流れです。

  1. セッションデータが一定時間更新されていない
  2. gc_maxlifetimeを超える
  3. GCが実行される
  4. 対象データが削除される

GCが実行されなければ、期限を超えたセッションデータがサーバー上に残る場合があります。

PHPのGCは確率的に実行される

PHPの標準的なセッション管理では、古いセッションデータの削除にガベージコレクションを使用します。

GCの実行確率は、主に次の2つで決まります。

session.gc_probability = 1
session.gc_divisor = 100

計算式は次のとおりです。

GC実行確率
=
session.gc_probability
÷
session.gc_divisor

上記の設定では、GCが実行される確率は1%です。

ただし、実際のサーバーでは、OS、ホスティングサービス、Dockerイメージ、PHP-FPMなどの構成によって値が変更されている場合があります。

実環境の設定は、次のように確認できます。

<?php

var_dump(ini_get('session.gc_probability'));
var_dump(ini_get('session.gc_divisor'));
var_dump(ini_get('session.gc_maxlifetime'));
var_dump(session_save_path());

CLIから確認する場合は、次のようなコマンドを使用します。

php -i | grep session.gc

ただし、CLI版PHPとWebサーバー版PHPでは、異なるphp.iniを読み込んでいる場合があります。

Webアプリケーションの実際の設定を確認する場合は、Webサーバー経由で実行されるPHPから確認するほうが確実です。

Cookieとサーバー側データは別々に期限切れになる

セッションID Cookieとサーバー側セッションデータは、それぞれ独立して管理されます。

Cookieのほうが長い場合

次の設定を考えます。

session.cookie_lifetime = 86400
session.gc_maxlifetime = 1800

この場合、ブラウザはセッションIDを24時間保持します。

一方、サーバー側データは30分を超えるとGCの削除対象になります。

1時間後にアクセスした場合、ブラウザは同じセッションIDを送る可能性があります。

しかし、サーバー側のデータがすでに削除されていれば、ログイン情報は復元できません。

サーバー側データのほうが長い場合

次の設定では、ブラウザ側のCookieが先に期限切れになります。

session.cookie_lifetime = 1800
session.gc_maxlifetime = 86400

ブラウザがセッションIDを失えば、サーバーにデータが残っていても、通常はそのセッションを参照できません。

数値を機械的にそろえる必要はない

session.cookie_lifetimesession.gc_maxlifetimeを、必ず同じ値にする必要はありません。

また、必ず次の関係にしなければならないわけでもありません。

session.gc_maxlifetime
>=
session.cookie_lifetime

重要なのは、それぞれの役割を理解したうえで、認証方針に合った設定にすることです。

たとえば次のような構成も成立します。

アイドルタイムアウト:30分
サーバー側データ保持:2時間
Cookie:ブラウザセッションCookie

実際のアクセス許可はアプリケーション側で30分に制限し、サーバー側データは削除処理の都合で少し長く保持する設計です。

正確な有効期限はアプリケーション側で判定する

session.gc_maxlifetimeは、セッションデータの整理を目的とする設定です。

そのため、正確なログアウト時刻を制御する用途には適していません。

ログイン状態を確実に期限切れにするには、アプリケーション側で時刻を保存し、リクエストごとに判定します。

代表的な期限は次の2種類です。

  • アイドルタイムアウト
  • 絶対有効期限

アイドルタイムアウトを実装する

アイドルタイムアウトは、最後の操作から一定時間アクセスがなければログアウトさせる仕組みです。

30分でタイムアウトさせる例

<?php

declare(strict_types=1);

const SESSION_IDLE_TIMEOUT = 1800;

session_start();

$now = time();
$lastActivity = $_SESSION['last_activity'] ?? null;

if (
    is_int($lastActivity) &&
    ($now - $lastActivity) >= SESSION_IDLE_TIMEOUT
) {
    destroyCurrentSession();

    header('Location: /login.php?reason=timeout');
    exit;
}

if (isset($_SESSION['user_id'])) {
    $_SESSION['last_activity'] = $now;
}

function destroyCurrentSession(): void
{
    $_SESSION = [];

    if (filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOL)) {
        $params = session_get_cookie_params();

        setcookie(session_name(), '', [
            'expires' => time() - 3600,
            'path' => $params['path'],
            'domain' => $params['domain'],
            'secure' => $params['secure'],
            'httponly' => $params['httponly'],
            'samesite' => $params['samesite'] ?: 'Lax',
        ]);
    }

    if (session_status() === PHP_SESSION_ACTIVE) {
        session_destroy();
    }
}

このコードでは、最後のアクセスから30分以上経過している場合にセッションを破棄します。

バックグラウンド通信に注意する

すべてのリクエストでlast_activityを更新すると、ユーザーが操作していなくてもセッションが延長され続ける場合があります。

たとえば、次のような通信です。

  • 定期的なAPIポーリング
  • 通知の自動取得
  • アクセス解析
  • 入力内容の自動保存
  • ステータス確認
  • バックグラウンド同期

厳密に「ユーザーの操作」を基準にする場合は、セッション確認とアクティビティ更新を分けます。

<?php

function checkSessionTimeout(): void
{
    $lastActivity = $_SESSION['last_activity'] ?? null;

    if (
        is_int($lastActivity) &&
        (time() - $lastActivity) >= 1800
    ) {
        destroyCurrentSession();

        header('Location: /login.php?reason=timeout');
        exit;
    }
}

function markUserActivity(): void
{
    $_SESSION['last_activity'] = time();
}

ユーザーの明示的な画面操作や主要APIだけでmarkUserActivity()を実行すれば、バックグラウンド通信による自動延長を避けやすくなります。

絶対有効期限を設定する

アイドルタイムアウトだけでは、ユーザーが操作し続ける限り、セッションが長期間継続する可能性があります。

そのため、ログイン後の最大継続時間も設定します。

たとえば、次のような構成です。

最終操作から30分で失効
ログインから最大8時間で失効

ログイン時に期限を保存する

ログイン成功時に、絶対有効期限を保存します。

<?php

$now = time();

$_SESSION['user_id'] = $userId;
$_SESSION['last_activity'] = $now;
$_SESSION['absolute_expires_at'] = $now + 28800;

28800秒は8時間です。

リクエスト時に判定する

<?php

$now = time();

$lastActivity = $_SESSION['last_activity'] ?? null;
$absoluteExpiresAt = $_SESSION['absolute_expires_at'] ?? null;

$idleExpired =
    is_int($lastActivity) &&
    ($now - $lastActivity) >= 1800;

$absoluteExpired =
    is_int($absoluteExpiresAt) &&
    $now >= $absoluteExpiresAt;

if ($idleExpired || $absoluteExpired) {
    destroyCurrentSession();

    header('Location: /login.php?reason=session_expired');
    exit;
}

期限時刻を直接保存しておくと、ログイン時刻からの経過秒数を毎回計算する必要がありません。

ログイン時にはセッションIDを再生成する

ログイン成功時には、セッション固定攻撃への対策として、セッションIDを再生成します。

session_regenerate_id(false);

ただし、session_regenerate_id()の第1引数には注意が必要です。

trueを指定する場合の注意点

次のようにtrueを指定すると、古いセッションデータが即時削除されます。

session_regenerate_id(true);

単純な同期型サイトではよく使われるコードですが、次のような環境では競合が発生する可能性があります。

  • Ajax通信が並行して実行される
  • SPAで複数APIを同時に呼び出す
  • 複数タブを利用している
  • ログイン直後に複数リクエストが発生する
  • モバイル回線などで通信順序が前後する
  • 不安定なネットワークを利用している

古いセッションを即時削除すると、別の同時リクエストが古いセッションIDを使い、セッション情報を失う場合があります。

実装方針を環境に合わせる

単純なWebサイトでは、次のような実装が使われることがあります。

session_regenerate_id(true);

並列リクエストが多いアプリケーションでは、次のように古いデータを即時削除しない方法を検討します。

session_regenerate_id(false);

ただし、古いセッションを無期限に残してよいわけではありません。

本格的に対応する場合は、古いセッションに破棄時刻などを保存し、一定の猶予期間後に無効化する仕組みを実装します。

session_destroy()だけでは完全にログアウトできない

次の処理だけでは、完全なログアウトにならない場合があります。

session_destroy();

session_destroy()が削除するのは、現在のセッションに関連付けられたサーバー側データです。

次のものは、自動的には削除されません。

  • $_SESSIONの内容
  • ブラウザ側のセッションCookie

そのため、ログアウト処理では次の3つを行います。

  1. $_SESSIONを空にする
  2. セッションCookieを削除する
  3. サーバー側セッションを破棄する

ログアウト処理の例

<?php

session_start();

$_SESSION = [];

if (filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOL)) {
    $params = session_get_cookie_params();

    setcookie(session_name(), '', [
        'expires' => time() - 3600,
        'path' => $params['path'],
        'domain' => $params['domain'],
        'secure' => $params['secure'],
        'httponly' => $params['httponly'],
        'samesite' => $params['samesite'] ?: 'Lax',
    ]);
}

session_destroy();

header('Location: /login.php');
exit;

Ajaxや並列通信が多いアプリケーションでは、ログアウト時にも同時リクエストとの競合を考慮する必要があります。

セッションCookieのセキュリティ設定

セッションの有効期限だけでなく、セッションIDを保護する設定も重要です。

session.use_strict_mode

session.use_strict_mode = 1

PHPが生成していない未初期化のセッションIDを受け入れにくくする設定です。

セッション固定攻撃への対策として、有効化が推奨されます。

PHPコードで設定する場合は、session_start()より前に実行します。

ini_set('session.use_strict_mode', '1');

session.use_only_cookies

session.use_only_cookies = 1

セッションIDをCookieだけで受け渡す設定です。

URLにセッションIDが含まれると、次の場所から漏れる可能性があります。

  • アクセスログ
  • Referer
  • ブラウザ履歴
  • ブックマーク
  • 共有URL

そのため、Cookieのみを使用する構成が基本です。

session.cookie_httponly

session.cookie_httponly = 1

JavaScriptからセッションCookieを直接読み取れないようにします。

ただし、HttpOnlyはXSSそのものを防止する機能ではありません。

XSSが成立した場合、Cookieを読み取れなくても、ユーザー権限で不正なリクエストを送信される可能性があります。

session.cookie_secure

session.cookie_secure = 1

HTTPS通信時だけセッションCookieを送信します。

本番環境がHTTPSのみで構成されている場合は、有効化するべき設定です。

ただし、HTTPで動作するローカル開発環境で有効にすると、Cookieが送信されなくなる場合があります。

開発環境と本番環境で設定を分ける必要があります。

session.cookie_samesite

一般的な設定例は次のとおりです。

session.cookie_samesite = Lax

より厳格に制限する場合は、次の設定もあります。

session.cookie_samesite = Strict

SameSite属性は、外部サイトからのリクエスト時にCookieを送るかどうかを制御します。

CSRF対策の一部として有効ですが、Strictでは外部サービスから戻る認証や決済処理に影響する可能性があります。

たとえば、次のような処理です。

  • OAuthログイン
  • 外部決済サービス
  • 外部フォームからの遷移
  • 他サイトから戻る認証フロー

一般的なWebサイトでは、Laxのほうが扱いやすい場合があります。

実運用向けのセッション設定例

基本的なセキュリティ設定をまとめると、次のようになります。

session.use_strict_mode = 1
session.use_cookies = 1
session.use_only_cookies = 1

session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax

session.gc_maxlifetime = 7200
session.gc_probability = 1
session.gc_divisor = 100

アプリケーション側では、たとえば次のように設定します。

const SESSION_IDLE_TIMEOUT = 1800;
const SESSION_ABSOLUTE_TIMEOUT = 28800;

この例では、次の役割分担になります。

Cookie
ブラウザセッション用として発行

アイドルタイムアウト
30分でアクセスを拒否

絶対有効期限
ログイン後8時間でアクセスを拒否

サーバー側データ
2時間を超えるとGCの削除対象

ただし、gc_maxlifetime = 7200は一例です。

適切な値は、システムの要件によって異なります。

gc_maxlifetimeの適切な値はシステムごとに異なる

session.gc_maxlifetimeを、すべてのサイトで1時間や2時間にすればよいわけではありません。

値を決める際は、次の条件を考慮します。

  • アイドルタイムアウト
  • 絶対有効期限
  • 通信断から復帰させる必要性
  • セッション数
  • 保存容量
  • 保存先
  • 管理画面か一般サイトか
  • 並列リクエストの有無
  • GCをPHPに任せるかcronで実行するか
  • セッションデータを監査に利用するか

不要なセッションデータを長期間保存すると、保存容量や検索性能に影響する可能性があります。

一方で、短くしすぎると、通信遅延や一時的なアクセス停止によってセッションが失われやすくなります。

複数サイトで同じセッション保存先を共有する場合

複数のPHPアプリケーションが同じsession.save_pathを使用している場合は注意が必要です。

アプリケーションごとに異なるsession.gc_maxlifetimeを設定していても、保存先が同じであれば、短い値を設定したアプリケーションのGCが、別アプリケーションのセッションデータまで削除する可能性があります。

たとえば、次のような構成です。

サイトA
gc_maxlifetime = 1800

サイトB
gc_maxlifetime = 86400

保存先
両方とも同じディレクトリ

この場合、サイトAのGCによって、サイトBのセッションデータが早期に削除される可能性があります。

アプリケーションごとに保存先を分ける

session.save_path = "/var/lib/php/sessions/myapp"

PHPコードから設定する場合は、session_start()より前に指定します。

<?php

session_save_path('/var/lib/php/sessions/myapp');
session_start();

保存ディレクトリの所有者やアクセス権限も適切に設定する必要があります。

RedisやMemcachedを使用する場合

PHPのセッション保存先は、ローカルファイルだけではありません。

代表的な保存先には次のものがあります。

  • ローカルファイル
  • Redis
  • Memcached
  • データベース
  • 独自セッションハンドラー

保存先によって、有効期限や削除処理の挙動が異なります。

Redisの場合

Redisでは、セッションキーにTTLを設定する構成が一般的です。

ただし、実際の動作は使用する拡張機能やライブラリに依存します。

確認すべき項目は次のとおりです。

  • どのRedis拡張機能を使用しているか
  • セッションキーにTTLが設定されるか
  • アクセス時にTTLが更新されるか
  • session.gc_maxlifetimeがTTLに反映されるか
  • セッションロックが有効か
  • 複数サーバーで同じRedisを参照しているか

Memcachedの場合

Memcachedでも、PHP標準のファイルセッションと同じGC処理になるとは限りません。

保存ハンドラーによっては、session.gc_maxlifetimeを確率的GCではなく、キャッシュエントリの有効期限として利用する場合があります。

利用している拡張機能やフレームワークの仕様を確認する必要があります。

ログイン状態を長期間保持する場合

「30日間ログイン状態を保持する」といった機能を、セッションCookieの有効期限を長くするだけで実装するのは推奨されません。

たとえば、次のような設定です。

session.cookie_lifetime = 2592000

2592000秒は30日です。

同じセッションIDを長期間使い続けると、セッションIDが漏えいした場合の影響も長く続きます。

Remember Meトークンを別に実装する

長期ログインは、通常のセッションとは分けて実装します。

一般的な構成は次のとおりです。

  1. 通常のセッションは短期間で失効させる
  2. 十分にランダムな再認証トークンを発行する
  3. トークンのハッシュをデータベースに保存する
  4. 元のトークンをCookieに保存する
  5. セッション切れ後にトークンを検証する
  6. 検証成功後に新しいセッションを発行する
  7. 使用したトークンをローテーションする
  8. ログアウト時にトークンを無効化する

CookieにユーザーIDだけを保存したり、固定トークンを長期間使い続けたりする設計は避けるべきです。

よくある誤解

gc_maxlifetime = 1800なら必ず30分でログアウトする

誤りです。

30分を超えたセッションデータがGCの削除対象になるだけであり、30分後に必ず削除されるわけではありません。

実際のログアウト期限は、アプリケーション側で判定する必要があります。

cookie_lifetime = 1800なら無操作30分でログアウトする

必ずしもそうではありません。

Cookieの発行時刻からの期限と、ユーザーが最後に操作した時刻は別です。

無操作時間を基準にする場合は、last_activityなどを保存して判定します。

ブラウザを閉じれば必ずログアウトする

保証されません。

セッションCookieがブラウザによって復元される場合があります。

また、ブラウザ側のCookieが消えても、サーバー側のセッションデータはGCが実行されるまで残る可能性があります。

session_destroy()だけで完全にログアウトできる

不十分です。

$_SESSIONの初期化と、セッションCookieの削除も必要です。

長期ログインにはセッションを長くすればよい

推奨されません。

長期ログインには、専用の再認証トークンを使用するべきです。

session_regenerate_id(true)は常に安全である

常に安全とは限りません。

古いセッションデータを即時削除すると、並行リクエストや不安定なネットワークでセッションが失われる可能性があります。

推奨される設計方針

一般的な会員サイトや管理画面では、次のように役割を分けると管理しやすくなります。

セッションID Cookie
ブラウザがIDを保持するために使用

session.gc_maxlifetime
古いサーバー側データを整理するために使用

アイドルタイムアウト
最終操作からの有効期限を管理

絶対有効期限
ログイン開始からの最大継続時間を管理

Remember Meトークン
長期間のログイン保持に使用

設定例は次のとおりです。

アイドルタイムアウト:30分
絶対有効期限:8時間
session.cookie_lifetime:0
session.gc_maxlifetime:システム要件に合わせて設定
長期ログイン:専用トークンで実装

重要なのは、session.cookie_lifetimesession.gc_maxlifetimeだけで認証期限を管理しないことです。

正確なログアウト時刻を保証するには、アプリケーション側で現在時刻と有効期限を比較し、期限切れの場合はアクセスを拒否する必要があります。

以上、PHPのセッションの有効期限についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次