PHPの基礎 - セッション

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

Webセッションは、Webアプリケーション開発において、重要な概念の1つである。

セッションは、ユーザがWebサイトやアプリケーションとの対話中に維持される状態情報を管理する仕組みである。
これにより、ユーザがWebページ間を移動する際に、一時的なデータや認証情報を保持することができる。
セッションは、ユーザの操作の一貫性を維持するものである。

Webセッションの役割として、ユーザがWebサイト上で何か特定の操作を行う間、その操作のコンテキストや状態を保持するのに役立つ。
例えば、ショッピングサイトのカートに商品を追加する場合、セッションはそのカート内の商品リストや数量を保持することができる。

このように、セッションは、ユーザエクスペリエンスを向上させるのに役立つ重要なものである。


セッションの仕組み

一般的に、セッションはユーザがWebサイトにアクセスした時に開始される。
その後、一定の条件が満たされた時に終了する。

セッションの開始時には、ユーザに一意のセッションIDが割り当てられる。
このセッションIDは、サーバサイドでセッションデータを識別するために使用される。

セッションIDとは、ユーザを一意に識別するためのランダムな文字列または数字の組み合わせである。
セッションIDは、クッキー、URLパラメータ、またはヘッダ等の方法でクライアントに送信されて、サーバでセッションデータと関連付けられる。

クッキーは、セッション管理のために広く使用されるメカニズムの1つである。
WebブラウザはセッションIDをクッキーとして保存して、次回のリクエスト時にサーバに送信することにより、サーバは正確なセッションを特定して、ユーザのセッション情報を維持することができる。


セッションとクッキーの違い

セッションは、データの一時的な保存と管理を担当するものである。
クッキーは、クライアント側にデータを保存するためのメカニズムである。

セッションはクッキーを使用して実現されることが多いが、異なる役割を果たす。


セッションの使い方

セッションの設定

Webアプリケーションでは、セッションを設定して管理する方法が重要である。 一般的に、プログラミング言語やフレームワークに組み込まれたセッション管理機能を使用している。

セッションのデータ保存

セッションを活用するには、ユーザの操作に関連するデータをセッションに保存する必要がある。

これには、ショッピングカート内の商品、ユーザの個人情報、設定オプション等が含まれている。

セッションの有効期限管理

セッションは一時的なものであるため、有効期限を設定することが重要である。

セッションが長時間アクティブである場合、セキュリティリスクが高まる可能性があるため、セッションのタイムアウトを設定することが推奨される。

セッションのセキュリティ

セッションデータのセキュリティは非常に重要である。

セッションIDを保護し、不正アクセスから保護するためのセキュリティ対策が必要となる。
例えば、セッションハイジャッキング等の攻撃からの保護策を実装することが不可欠である。


セッションの活用方法

セッションは、Webアプリケーション開発において非常に重要な要素である。
正しく管理することにより、ユーザエクスペリエンスの向上、セキュリティの強化、効果的なデータ収集等が可能となる。

ログイン

セッションはログインシステムにおいて広く使用されている。

例えば、ユーザがWebサイトにログインする時、セッションに認証情報やユーザコンテキストが保存される。
その後、リクエストにおいて、セッションを介してユーザを識別することにより、特定の機能やデータにアクセスできるようになる。

セッションを使用することにより、ユーザはログインしたままWebサイト内を移動することができる。

ショッピングカート

ショッピングサイトでは、セッションがショッピングカートの実装に不可欠である。

例えば、ユーザが商品をカートに追加する時、その情報はセッションに保存される。
ユーザがチェックアウトするまで、一貫性のあるショッピング体験を提供することができる。

トラッキングと分析

Webサイトのトラフィックを分析する際にもセッション情報は役立つ。

例えば、ユーザのセッションごとに以下のデータを取得して追跡することができる。

  • 訪問時間
  • 閲覧ページ
  • アクション


つまり、ユーザエンゲージメントやコンバージョン率の向上に役立つデータを収集できる。


セッション管理

セッションの保存場所

デフォルトでは、一時ファイルとしてサーバのファイルシステムに保存される。
一般的には、以下に示すディレクトリのいずれかに保存される。

  • /tmp
  • /var/lib/php/sessions
  • /tmp/sessions


セッションファイルの名前は"sess_"で始まり、その後にセッションIDが続く。

保存場所の設定は、php.iniファイルのsession.save_path項目で設定できる。

補足

  • セッションの保存方法は変更可能で、データベースやRedisなどに保存することもできる。
  • セキュリティ上、本番環境では適切なパーミッションの設定が重要である。
  • 定期的なセッションクリーンアップは、サーバリソースの効率的な管理に重要である。


セッションの削除方法

プログラムで削除
 // 現在のセッションのデータを全て削除
 session_unset();
 
 // セッションを破棄
 session_destroy();
 
 // セッションクッキーも削除する場合
 if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time()-42000, '/');
 }


手動での削除
  • セッションファイルを直接削除する場合
# セッション情報が/tmpディレクトリに保存されている場合 (デフォルト)
rm /tmp/sess_*


  • セッションガベージコレクションの設定する場合
    php.iniファイルにおいて、以下に示すパラメータが設定できる。
    • session.gc_maxlifetime
      セッションの最大生存時間
    • session.gc_probability / session.gc_divisor
      ガベージコレクション実行の確率



セッションの開始

セッションとは、クッキーは管理する値をクライアントに保存するのに対して、セッションは管理する値をサーバに保存する。
クライアントには、どのセッションを使用しているかを識別するためのセッションIDのみをクライアントに保存する。

一般的に、セッションIDをクッキーの値としてクライアントに保存する。(セッションを使用する場合、クッキーも同時に使用する)
また、重要なデータを扱う場合はセッションを使用する。

クライアントから初めてアクセスがある場合、session_start関数を使用して、新しいセッションを作成してセッションを開始する必要がある。

bool session_start()

session_start関数は、セッションを作成する。
もしくは、リクエスト上でGET、POST、クッキーにより渡されたセッションIDに基づき、
現在のセッションを復帰する。

戻り値:
   常にTRUEを返す。


セッションが開始されていない状態でsession_start関数が呼ばれる場合、新しいセッションを開始してセッションIDを割り当てる。
セッションIDは、クライアントにクッキー名PHPSESSIDで保存される。
(クライアントがクッキーを利用できない場合、次のセクションに記載する)

既にセッションが開始されているクライアント(既にセッションIDがクッキーとして保存されているクライアント)からアクセスがある場合、
session_start関数を実行しても新たにセッションを開始せずに、サーバに保存されているセッション変数を利用できるように準備する。

以下の例では、セッションを開始して、クライアントにセッションIDがクッキーとして保存しているか確認している。
初めてアクセスする場合、セッションIDが割り当てられて、クライアントは自動的にクッキーを利用してセッションIDを保存する。
次回以降は、クッキー名PHPSESSIDがクライアントから送信されて、新たにセッションは開始されずに、以前のものを利用する。

また、セッションIDにはランダムな値が設定される。

 <?php
    session_start();
 ?>
 
 <?php
    if(!isset($_COOKIE["PHPSESSID"]))
    {
       print('初回の訪問です。セッションを開始します。');
    }
    else
    {
       print('セッションは開始しています。<br>');
       print('セッションIDは '.$_COOKIE["PHPSESSID"].' です。');
    }
 ?>



セッション変数の書き込みと読み込み

セッションを開始することで、サーバに管理する値(セッション変数と呼ぶ)を複数保存することができる。
クライアントごとに様々な値が書き込まれるため、セッションIDを識別子として、クライアントを識別することができる。

セッション変数に値を書き込む場合は、定義済みの変数$_SESSIONを使用する。

$_SESSION[セッション変数名] = 値


例えば、セッション変数visitedに値を書き込む場合、以下のように記述する。

$_SESSION["visited"] = 1;


以下の例では、セッションを開始して、セッション変数visitedに値を保存している。
同一のWebページにアクセスするごとにセッション変数の値を増加させて、前回アクセスした日時も表示している。

 <?php
    session_start();
 ?>
 
 <?php
    if(!isset($_SESSION["visited"]))
    {
       print('初回の訪問です。セッションを開始します。');
 
       $_SESSION["visited"] = 1;
       $_SESSION["date"] = date('c');
    }
    else
    {
       $visited = $_SESSION["visited"];
       $visited++;
 
       print('訪問回数は'.$visited.'です。<br>');
 
       $_SESSION["visited"] = $visited;
 
       if(isset($_SESSION["date"]))
       {
          print('前回の訪問日時は'.$_SESSION["date"].'です。<br>');
       }
 
       $_SESSION["date"] = date('c');
    }
 ?>



セッション変数の削除とセッションIDの削除

保存されたセッション変数を削除するには、unset関数を使用する。

void unset(mixed var [, mixed var [, mixed ...]])

unset関数は、指定した変数を破棄する。

パラメータ:
   var  削除したい変数


以下の例では、セッション変数visitedを削除している。

 unset($_SESSION("visited"));


また、全てのセッション変数を削除するには、以下のように空の配列を変数$_SESSIONに代入する。

 $_SESSION = array();


※注意
変数$_SESSION自体を削除しないこと。

ログアウト等でセッション自体を破棄する場合、クライアントに保存されているセッションIDを削除した後、セッションを破棄する。
まず、クライアントにはクッキーが保存されているため、クッキーを削除する。
(クッキーの削除については、PHPの基礎 - クッキー#クッキーの削除を参照すること)

 if(isset($_COOKIE["PHPSESSID"]))
 {
    setcookie("PHPSESSID", '', time() - 1800, '/');
 }


次に、session_destroy関数を使用して、セッションに登録された値を全て破棄する。

bool session_destroy(void)

session_destroy関数は、現在のセッションに 関連づけられた全てのデータを破棄する。
この関数は、セッションに関するグローバル変数を破棄しない。
また、セッションクッキーを破棄しない。

戻り値:
   成功した場合はtrue、失敗した場合はfalseを返す。


以下の例では、Sample1.phpのログアウトのリンクを選択して、Sample2.phpでセッションとクッキーを削除している。
次に、Sample3.phpでセッションとクッキーが正常に削除されているかを確認している。

 // Sample1.php
 
 <?php
    session_start();
 ?>
 
 <?php
    if(!isset($_SESSION["visited"]))
    {
       print('初回の訪問です。セッションを開始します。');
 
       $_SESSION["visited"] = 1;
       $_SESSION["date"] = date('c');
    }
    else
    {
       $visited = $_SESSION["visited"];
       $visited++;
 
       print('訪問回数は'.$_SESSION["visited"].'です。<br>');
 
       $_SESSION["visited"] = $visited;
 
       if(isset($_SESSION["date"]))
       {
          print('前回の訪問日時は'.$_SESSION["date"].'です。<br>');
       }
 
       $_SESSION["date"] = date('c');
    }
 ?>
 
 <p><a href="./Sample2.php">ログアウトする</a></p>


 // Sample2.php
 
 <?php
    session_start();
 ?>
 
 <?php
    print('セッション変数の一覧を表示します。<br>');
    print_r($_SESSION);
    print('<br>');
 
    print('セッションIDを表示します。<br>');
    print($_COOKIE["PHPSESSID"].'<br>');
 
    print('<p>ログアウトします</p>');
 
    $_SESSION = array();
 
    if(isset($_COOKIE["PHPSESSID"]))
    {
       setcookie("PHPSESSID", '', time() - 1800, '/');
    }
 
    session_destroy();
 
    print('<p>ログアウトしました</p>');
 ?>
 
 <p><a href="./Sample3.php">ログアウトの確認</a></p>


 // Sample3.php
 
 <?php
    session_start();
 ?>
 
 <?php
    print('セッション変数の確認をします。<br>');
    if(!isset($_SESSION["visited"]))
    {
       print('セッション変数visitedは登録されていません。<br>');
    }
    else
    {
       print($_SESSION["visited"].'<br>');
    }
 
    print('セッションIDの確認をします。<br>');
    if(!isset($_COOKIE["PHPSESSID"]))
    {
       print('セッションは登録されていません。<br>');
    }
    else
    {
       print($_COOKIE["PHPSESSID"].'<br>');
    }
 ?>



セッション名の取得と変更

セッションを開始した時、クライアントのクッキー名として使用する変数 (セッション名) において、初期値はPHPSESSIDである。

この初期値を変更する場合は、php.iniファイルのsession.name項目を編集する。
セッション名には、英数字を使用することができる。

# php.iniファイル

例. session.name = HOGE


セッション名がPHPSESSIDではない可能性があるため、session_name関数を使用してセッション名を取得する。

string session_name(<string name>)

session_name関数は、カレントのセッション名を返す。
nameを指定する場合、カレントのセッション名はその値に変更される。

セッション名は、リクエストが開始された際に セッション名に保存されたsession.nameの値にリセットされる。
したがって、各リクエストごとに(session_start関数またはsession_register関数を呼ぶ前に)、session_name関数を実行する必要がある。

パラメータ:
   name  新しく設定したいセッション名

戻り値:
   現在のセッション名


session_name関数を引数無しで実行することにより、現在のセッション名を取得できる。

 // セッションを開始
 session_start();
 
 // 現在のセッション名を取得
 $current_session_name = session_name();
 echo $current_session_name; // デフォルトでは "PHPSESSID" が出力されます


また、session_name関数の引数に別のセッション名を指定する場合は、セッション名を変更することができる。

 // セッション名を変更する場合
 // 新しいセッション名を設定して、以前のセッション名を返す
 $prev_session_name = session_name("new_session");
 session_start();
 
 print('以前のセッション名は'.$prev_session_name.'です。');
 print('現在のセッション名は'.session_name().'です。');


※注意
セッション名を変更する場合は、必ず、session_start関数を実行する前に行う必要がある。
セッション名の変更は、セキュリティ上の理由から慎重に行うべきである。


セッションIDの取得と変更

セッションIDは自動的にランダムな値が設定されるが、session_id関数を使用して、任意の値に設定することもできる。

string session_id([string id])

session_id関数は、カレントのセッションIDを取得・設定するために使用される。

パラメータ:
   id  カレントのセッションIDを置換する。その際、session_start関数の前に呼ぶ必要がある。
       セッションハンドラによっては、セッションIDとして使用できる文字に制限がある場合がある。
       例えば、ファイルによるセッションハンドラにおいて、セッションIDとして使用できる文字は、a-z、A-Z、0-9に限られる。

戻り値:
   session_id関数は、カレントのセッションのセッションID・カレントセッションが存在しない(カレントのセッションIDが存在しない)場合、
   空文字列""を返す。


session_id関数を引数無しで使うことで現在のセッションIDを取得することができる。
また、引数に値を指定する場合は、セッションIDを指定した値に変更できる。

ただし、セッションIDは、セッションハイジャック等を防ぐためにも簡単な値を設定してはならない。
したがって、任意でセッションIDを指定しない方がよいと考えられる。

 <?php
    session_start();
 ?>
 
 <?php
    if(!isset($_COOKIE[session_name()]))
    {
       print('初回の訪問です。セッションを開始します。');
    }
    else
    {
       print('セッションは開始しています。<br>');
       print('クッキーは '.$_COOKIE[session_name()].' です。<br>');
       print('session_id関数は '.session_id().' です。<br>');
    }
 ?>



セッションIDの自動変更

セッションIDが漏洩すると、セッションを乗っ取られる可能性がある。
そのため、セッションIDを常に変更することにより、セキュリティを向上させることができる。

session_regenerate_id関数を使用して、現在のセッションを終了させることなくセッションIDのみを新しい値に変更できる。

bool session_regenerate_id ( [bool delete_old_session] )

session_regenerate_id関数は、現在のセッションIDを新しい値に変更する。
その際、現在のセッション情報は維持される。

パラメータ:
   (PHP 5.1以降)
   delete_old_session  関連付けられた古いセッションを削除するかを決める。
                       初期値はfalseである。
                       古いセッション情報を残すことは資源の無駄かつセキュリティも悪くなるため、trueを指定することを推奨する。

   戻り値:
      成功した場合はtrue、失敗した場合はfalseを返す。


以下の例では、Webページを読み込むたびに、セッションIDを変更している。

 <?php
    session_start();
    $old_id = session_id();
 
    session_regenerate_id(true);
    $new_id = session_id();
 ?>
 
 <?php
    if(!isset($_COOKIE[session_name()]))
    {
       print('初回の訪問です。セッションを開始します。');
    }
    else
    {
       print('前のセッションIDは '.$old_id.' です。<br>');
       print('今のセッションIDは '.$new_id.' です。<br>');
    }
 ?>



期限切れのセッションファイルの削除

以下の例では、期限切れのセッションファイルを安全に削除している。

  • セッションの保存パスを自動検出する。
  • PHP.iniで設定された最大生存時間を考慮する。
  • 期限切れセッションのみを削除する。
    セッションファイルのみを対象とする。
  • 現在のセッション状態を確認できる。


 /**
  * PHPセッションクリーンアップを管理するクラス
  * 
  * このクラスは、古くなったPHPセッションファイルを検出し、安全に削除するための機能を提供する
  * セッションの保存場所とライフタイムの設定に基づいて動作する。
  */
 class SessionCleaner
 {
    /** @var string セッションファイルが保存されているディレクトリパス */
    private $sessionPath;
 
    /** @var int セッションの最大生存時間(秒) */
    private $maxLifetime;
 
    /**
     * SessionCleanerクラスのコンストラクタ
     * 
     * @param string|null $customPath カスタムセッション保存パス(オプション)
     *                                nullの場合、PHPのデフォルト設定を使用
     */
    public function __construct($customPath = null)
    {
       // PHPの設定からセッション保存パスを取得
       // session_save_path()はPHP.iniまたはruntime設定で指定されたパスを返す
       $this->sessionPath = $customPath ?? session_save_path();
 
       // セッションの最大生存時間をPHP設定から取得
       // session.gc_maxlifetimeはガベージコレクションが実行される際のセッションの最大生存時間を秒単位で指定する
       $this->maxLifetime = ini_get('session.gc_maxlifetime');
    }
 
    /**
     * 期限切れのセッションファイルを削除する
     * 
     * @throws Exception セッション保存ディレクトリが存在しない場合
     * @return array クリーンアップの結果を含む連想配列
     *         cleaned: 削除されたファイル数
     *         path: セッション保存パス
     *         maxLifetime: セッション最大生存時間
     */
    public function cleanup()
    {
       // セッション保存ディレクトリの存在確認
       if (!is_dir($this->sessionPath)) {
          throw new Exception("セッション保存ディレクトリが存在しません: {$this->sessionPath}");
       }
 
       $count = 0;
       // sess_プレフィックスを持つすべてのセッションファイルを検索
       foreach (glob($this->sessionPath . "/sess_*") as $file) {
          if (is_file($file)) {
             // ファイルの最終更新時刻 + 最大生存時間が現在時刻を過ぎていれば削除
             if (filemtime($file) + $this->maxLifetime < time()) {
                // unlinkでファイルを削除し、成功した場合はカウントを増やす
                if (unlink($file)) {
                   $count++;
                }
             }
          }
       }
 
       // クリーンアップ結果を返す
       return [
          'cleaned'     => $count,
          'path'        => $this->sessionPath,
          'maxLifetime' => $this->maxLifetime
       ];
    }
 
    /**
     * 現在のセッション状態に関する情報を取得する
     * 
     * @return array セッション情報を含む連想配列
     *               - total: 総セッションファイル数
     *               - expired: 期限切れセッションファイル数
     *               - path: セッション保存パス
     *               - maxLifetime: セッション最大生存時間
     */
    public function getSessionInfo()
    {
       $totalSessions = 0;    // 総セッション数
       $oldSessions = 0;      // 期限切れセッション数
       $currentTime = time(); // 現在のUNIXタイムスタンプ
 
       // 全てのセッションファイルをスキャン
       foreach (glob($this->sessionPath . "/sess_*") as $file) {
          if (is_file($file)) {
             $totalSessions++;
             // ファイルの最終更新時刻 + 最大生存時間 < 現在時刻 の場合、
             // そのセッションは期限切れと判定
             if (filemtime($file) + $this->maxLifetime < $currentTime) {
                $oldSessions++;
             }
          }
       }
 
       // セッション情報を返す
       return [
          'total'       => $totalSessions,
          'expired'     => $oldSessions,
          'path'        => $this->sessionPath,
          'maxLifetime' => $this->maxLifetime
       ];
    }
 }


 // 使用例
 
 $cleaner = new SessionCleaner();
 
 // セッション情報の取得
 $info = $cleaner->getSessionInfo();
 echo "総セッション数: " . $info['total'];
 echo "期限切れセッション数: " . $info['expired'];
 
 // クリーンアップの実行
 $result = $cleaner->cleanup();
 echo "削除されたセッション数: " . $result['cleaned'];



セッションとクッキーの使用例

以下の例では、セッション管理、セキュリティ対策、ユーザ設定管理等の実務的な実装パターンを示している。

  • セキュリティ対策
    • HTTPOnly設定によるXSS対策 (重要)
    • Secure属性によるHTTPS強制
    • SameSite属性によるCSRF対策 (重要)
    • セッションID再生成によるセッション固定攻撃対策 (重要)
    • セッションハイジャック対策 (重要)
      ログイン成功時に必ず新しいセッションIDを発行する。
      セッションIDをURLパラメータで渡さない。
      セッションIDの推測を困難にする。
      HTTPSを使用してセッションIDの盗聴を防ぐ。


  • セッション管理
    ユーザ認証状態の管理
    セッションタイムアウトの実装
    適切なログアウト処理


  • ショッピングカート機能
    カートの状態をセッションで管理
    商品の追加・削除機能
    カート内容の保持


  • ユーザ設定管理
    クッキーを使用した長期的な設定の保存
    セキュアなクッキー設定
    デフォルト値の設定


設計においては、クラスベースの整理された実装、責任の分離が重要となる。
また、セッション切れの場合の処理、バリデーションの実装も同様である。

必要最小限のデータのみをセッションに保存することや適切なタイムアウト設定も必要である。

 <?php
 /**
  * セッションのセキュリティ設定
  * 
  * 以下に示す設定は、本番環境では可能な限りphp.iniで設定することが推奨される
  *
  * 一部の設定はサーバーの設定によって変更が制限されている場合がある (php.ini_all等)
  * セッション開始後に変更しても効果がない設定がある
  * 実行時の設定変更 (ini_set関数) は、サーバ設定で無効化されている可能性がある
  *
  * session.cookie_httponly: JavaScriptからのクッキーアクセスを防止し、XSS攻撃によるセッションID漏洩を防ぐ
  * session.cookie_secure: HTTPS接続時のみクッキーを送信し、通信経路上での盗聴を防ぐ
  * session.cookie_samesite: CSRF (クロスサイトリクエストフォージェリ) 攻撃からの保護
  *   - 'Lax'設定:他サイトからのGETリクエストのみクッキーを送信許可 (POSTは制限)
  */
 ini_set('session.cookie_httponly', 1);
 ini_set('session.cookie_secure', 1);   
 ini_set('session.cookie_samesite', 'Lax');
 
 /**
  * セッション管理クラス
  * ユーザの認証状態とセッションのライフサイクルを管理する
  */
 class SessionManager
 {
    /**
     * コンストラクタ:セッションの初期化と設定
     * 
     * cookie_lifetime: セッションクッキーの有効期限 (ブラウザ終了後も持続)
     * gc_maxlifetime: セッションデータの有効期限 (サーバ側)
     * cookie_secure: HTTPSでの通信を強制
     * cookie_httponly: JavaScriptからのアクセスを禁止
     */
    public function __construct()
    {
       if (session_status() === PHP_SESSION_NONE) {
          session_start([
             'cookie_lifetime' => 3600,  // 1時間でセッション切れ
             'gc_maxlifetime'  => 3600,  // サーバ側でも1時間で破棄
             'cookie_secure'   => true,
             'cookie_httponly' => true
          ]);
       }
    }
 
    /**
     * ユーザログイン処理
     * 
     * @param string $userId ユーザID
     * @param array $userData ユーザ情報の配列
     * 
     * セキュリティ対策:
     * - セッションIDの再生成によるセッション固定攻撃の防止
     * - 最終アクティビティ時刻の記録による不正利用の検知
     */
    public function login($userId, $userData)
    {
       $_SESSION['user_id'] = $userId;
       $_SESSION['user_data'] = $userData;
       $_SESSION['last_activity'] = time();
 
       // セッション固定攻撃対策 : ログイン時に必ずセッションIDを再生成
       // true引数により古いセッションファイルを確実に削除
       session_regenerate_id(true);
    }
 
    /**
     * ログアウト処理
     * 
     * セキュリティ対策:
     * - セッション変数の完全な削除
     * - セッションクッキーの削除
     * - セッションストレージの破棄
     */
    public function logout()
    {
       // セッション変数を全て削除
       $_SESSION = array();
 
       // セッションクッキーの削除
       // 過去の時間を設定することで確実にクッキーを無効化
       if (isset($_COOKIE[session_name()])) {
          setcookie(session_name(), '', time() - 42000, '/');
       }
 
       // セッションストレージを破棄
       session_destroy();
    }
 
    /**
     * セッションの有効性検証
     * 
     * 以下の場合にセッションを無効と判定:
     * - セッション変数が存在しない
     * - 最終アクティビティから30分以上経過
     * 
     * @return boolean セッションが有効な場合はtrue
     */
    public function isValidSession()
    {
       if (!isset($_SESSION['last_activity'])) return false;
 
       // 30分 (1800秒) 以上の未アクティビティでセッション無効化
       if (time() - $_SESSION['last_activity'] > 1800) {
          $this->logout();
          return false;
       }
 
       // 最終アクティビティ時刻を更新
       $_SESSION['last_activity'] = time();
       return true;
    }
 }
 
 /**
  * ショッピングカート管理クラス
  * セッションを利用してカートの状態を管理する
  */
 class ShoppingCart
 {
    private $sessionManager;
 
    /**
     * コンストラクタ : カートセッションの初期化
     * 
     * @param SessionManager $sessionManager セッション管理インスタンス
     */
    public function __construct(SessionManager $sessionManager)
    {
       $this->sessionManager = $sessionManager;
 
       // カートが未初期化の場合は空配列で初期化
       if (!isset($_SESSION['cart'])) $_SESSION['cart'] = array();
    }
 
    /**
     * 商品をカートに追加
     * 
     * @param string $productId 商品ID
     * @param int $quantity 数量
     * 
     * 注意:実務では以下の処理も必要
     * - 在庫チェック
     * - 数量の上限チェック
     * - 商品IDの有効性検証
     */
    public function addItem($productId, $quantity)
    {
       if (isset($_SESSION['cart'][$productId])) $_SESSION['cart'][$productId] += $quantity;
       else                                      $_SESSION['cart'][$productId] = $quantity;
    }
 
    /**
     * カートから商品を削除
     * 
     * @param string $productId 商品ID
     */
    public function removeItem($productId)
    {
       if (isset($_SESSION['cart'][$productId])) unset($_SESSION['cart'][$productId]);
    }
 
    /**
     * カートの内容を取得
     * 
     * @return array カート内の商品配列
     */
    public function getCart()
    {
       return $_SESSION['cart'] ?? array();
    }
 }
 
 /**
  * ユーザ設定管理クラス
  * クッキーを使用してユーザの長期的な設定を保存
  */
 class UserPreferences
 {
    /**
     * ユーザ設定をクッキーに保存
     * 
     * @param string $key 設定キー
     * @param mixed $value 設定値
     * @param int $expires 有効期限(秒)
     * 
     * セキュリティ設定:
     * - secure: HTTPS接続でのみ送信
     * - httponly: JavaScriptからのアクセス禁止
     * - samesite: CSRF対策
     * - domain: 現在のドメインのみで有効
     */
    public function savePreference($key, $value, $expires = 2592000)
    {
       // デフォルトは30日
       setcookie(
          "pref_" . $key,
          $value,
          [
             'expires'  => time() + $expires,
             'path'     => '/',
             'domain'   => $_SERVER['HTTP_HOST'],
             'secure'   => true,
             'httponly' => true,
             'samesite' => 'Lax'
          ]
       );
    }
 
    /**
     * クッキーから設定を読み込み
     * 
     * @param string $key 設定キー
     * @param mixed $default デフォルト値
     * @return mixed 設定値(未設定の場合はデフォルト値)
     */
    public function getPreference($key, $default = null)
    {
       return $_COOKIE["pref_" . $key] ?? $default;
    }
 }


 /**
  * 使用例
  */
 // ログイン処理の例
 $sessionManager = new SessionManager();
 if ($authenticated) {  // 認証処理は別途実装必要
    $sessionManager->login($userId, [
       'name' => 'テストユーザ',
       'email' => 'test@example.com'
    ]);
 }


 /**
  * 使用例
  */
 // ショッピングカート操作の例
 $cart = new ShoppingCart($sessionManager);
 $cart->addItem('product123', 2);


 /**
  * 使用例
  */
 // ユーザ設定の保存例
 $preferences = new UserPreferences();
 $preferences->savePreference('theme', 'dark');
 $preferences->savePreference('language', 'ja');


 /**
  * 使用例
  */
 /**
  * セッション確認用ミドルウェア
  * 
  * 保護が必要なページで実行し、無効なセッションの場合はログインページにリダイレクト
  * 
  * @param SessionManager $sessionManager セッション管理インスタンス
  */
 function checkSession($sessionManager)
 {
    if (!$sessionManager->isValidSession()) {
       header('Location: /login.php');
       exit;
    }
 }