首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 自定檔案儲存 Session 實作教學:原理解析與完整代碼

     編輯:PHP 綜合知識

前言

PHP 內建 Session 機制預設將資料儲存在伺服器臨時目錄的檔案中(session.save_handler = files)。但在某些場景下,開發者需要完全掌控 Session 儲存邏輯:例如分散式部署共享 Session、儲存至資料庫/Redis、自訂加密、或單純為了學習 Session 底層運作。本文將詳細講解如何實作一套自定檔案儲存 Session 類別,包含 Session ID 管理、資料序列化、檔案鎖定、垃圾回收等核心機制。

Session 運作核心流程

  1. 開啟:讀取 Cookie 中的 Session ID → 開啟對應儲存檔案 → 反序列化載入 `$_SESSION`
  2. 讀寫:腳本執行期間操作 `$_SESSION` 超全域陣列
  3. 關閉/寫入:腳本結束時序列化 `$_SESSION` → 寫回儲存介質 → 釋放鎖
  4. 銷毀:刪除儲存檔案、清除 Cookie、重置 `$_SESSION`

自定 Session 類別完整實作

<?php
/**
 * 自定檔案儲存 Session 類別
 * 功能:Cookie 傳遞 ID、檔案讀寫、序列化、簡易鎖定、GC
 */
class FileSessionHandler implements SessionHandlerInterface
{
    private string $savePath;
    private int    $lifetime;
    private ?string $sessionId = null;
    private ?resource $fileHandle = null;
    private array $data = [];

    public function __construct(string $savePath = '', int $lifetime = 1440)
    {
        $this->savePath = $savePath ?: sys_get_temp_dir() . '/custom_sessions';
        $this->lifetime = $lifetime;
        if (!is_dir($this->savePath)) {
            mkdir($this->savePath, 0700, true);
        }
    }

    // ---------- SessionHandlerInterface 方法 ----------

    public function open(string $path, string $name): bool
    {
        // 1. 從 Cookie 取得 Session ID
        $this->sessionId = $_COOKIE[$name] ?? null;

        // 2. 若無 ID 或檔案不存在,產生新 ID
        if (!$this->sessionId || !$this->_fileExists($this->sessionId)) {
            $this->sessionId = $this->_generateId();
            setcookie($name, $this->sessionId, [
                'expires' => time() + $this->lifetime,
                'path'    => '/',
                'httponly'=> true,
                'secure'  => isset($_SERVER['HTTPS']),
                'samesite'=> 'Lax',
            ]);
        }

        // 3. 開啟檔案(讀寫模式,建立獨佔鎖)
        $file = $this->_getFilePath($this->sessionId);
        $this->fileHandle = fopen($file, 'c+'); // c+ 不截斷,若不存在則建立
        if (!$this->fileHandle) return false;

        // 獨佔鎖防止併發寫入衝突
        if (!flock($this->fileHandle, LOCK_EX)) {
            fclose($this->fileHandle);
            return false;
        }

        // 4. 讀取並反序列化資料
        $content = stream_get_contents($this->fileHandle);
        $this->data = $content ? @unserialize($content) : [];
        if (!is_array($this->data)) $this->data = [];

        // 5. 註冊關閉時自動寫入
        register_shutdown_function([$this, 'close']);
        return true;
    }

    public function close(): bool
    {
        if ($this->fileHandle) {
            // 寫入前截斷檔案
            ftruncate($this->fileHandle, 0);
            rewind($this->fileHandle);
            fwrite($this->fileHandle, serialize($this->data));
            flock($this->fileHandle, LOCK_UN);
            fclose($this->fileHandle);
            $this->fileHandle = null;
        }
        return true;
    }

    public function read(string $id): string|false
    {
        // 由 open() 已載入 $this->data,此處僅供介面相容
        return serialize($this->data);
    }

    public function write(string $id, string $data): bool
    {
        // 實際寫入在 close() 統一處理,這裡更新記憶體資料
        $this->data = @unserialize($data);
        if (!is_array($this->data)) $this->data = [];
        return true;
    }

    public function destroy(string $id): bool
    {
        $file = $this->_getFilePath($id);
        if (file_exists($file)) {
            @unlink($file);
        }
        // 清除 Cookie
        if (isset($_COOKIE[session_name()])) {
            setcookie(session_name(), '', time() - 3600, '/');
        }
        $this->data = [];
        return true;
    }

    public function gc(int $maxLifetime): int|false
    {
        $deleted = 0;
        $now = time();
        $dir = new DirectoryIterator($this->savePath);
        foreach ($dir as $file) {
            if ($file->isFile() && ($now - $file->getMTime()) > $maxLifetime) {
                @unlink($file->getPathname());
                $deleted++;
            }
        }
        return $deleted;
    }

    // ---------- 輔助方法 ----------

    private function _generateId(): string
    {
        // 使用隨機位元組 + 時間 + 進程 ID,極低衝突機率
        return bin2hex(random_bytes(16)) . '_' . time() . '_' . getmypid();
    }

    private function _getFilePath(string $id): string
    {
        // 為避免單一目錄檔案過多,可用前 2 字元做子目錄
        $prefix = substr($id, 0, 2);
        $dir = $this->savePath . '/' . $prefix;
        if (!is_dir($dir)) mkdir($dir, 0700, true);
        return $dir . '/sess_' . $id;
    }

    private function _fileExists(string $id): bool
    {
        return file_exists($this->_getFilePath($id));
    }

    // ---------- 對外便利方法 ----------

    public function __set(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function __get(string $key): mixed
    {
        return $this->data[$key] ?? null;
    }

    public function __isset(string $key): bool
    {
        return isset($this->data[$key]);
    }

    public function __unset(string $key): void
    {
        unset($this->data[$key]);
    }

    public function getId(): ?string
    {
        return $this->sessionId;
    }

    public function regenerateId(bool $deleteOld = false): bool
    {
        $oldId = $this->sessionId;
        $this->sessionId = $this->_generateId();
        // 更新 Cookie
        setcookie(session_name(), $this->sessionId, [
            'expires' => time() + $this->lifetime,
            'path'    => '/',
            'httponly'=> true,
            'secure'  => isset($_SERVER['HTTPS']),
            'samesite'=> 'Lax',
        ]);
        if ($deleteOld && $oldId) {
            @unlink($this->_getFilePath($oldId));
        }
        return true;
    }
}

啟用自定 Session 處理器

在專案入口檔案(如 index.php 或 bootstrap.php)最早期呼叫:

<?php
require_once 'FileSessionHandler.php';

$handler = new FileSessionHandler(
    savePath: '/var/www/myapp/sessions', // 自訂儲存目錄,需可寫入
    lifetime: 7200                       // 2 小時
);

// 註冊為 PHP 內建 session 機制的處理器
session_set_save_handler($handler, true); // true = 關閉時自動呼叫 shutdown
session_start();

// 現在可以正常使用 $_SESSION
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'admin';
$_SESSION['permissions'] = ['read', 'write'];

// 測試跨頁面
echo "Session ID: " . $handler->getId() . "
"; echo "User: " . $_SESSION['username'] . "
"; echo '<a href="page2.php">前往第 2 頁</a>';

page2.php 驗證 Session 持久化

<?php
require_once 'FileSessionHandler.php';
$handler = new FileSessionHandler('/var/www/myapp/sessions', 7200);
session_set_save_handler($handler, true);
session_start();

echo "Session ID: " . $handler->getId() . "
"; echo "User ID: " . ($_SESSION['user_id'] ?? 'null') . "
"; echo "Permissions: " . implode(', ', $_SESSION['permissions'] ?? []) . "
";

進階主題:分散式環境下的 Session 共享

檔案儲存不適合多伺服器負載平衡。若需分散式共享,只需替換儲存層實作 SessionHandlerInterface:

  • Redis:高效能、支援原子操作、自動過期(TTL),最推薦。
  • Database (MySQL/PostgreSQL):利用交易確保一致性,適合已有資料庫且流量中等。
  • Memcached:簡單快速,但無持久化、資料可能被驅逐。

以 Redis 為例,核心 read/write/destroy/gc 分別對應 GET/SET/DEL/掃描過期鍵,效能遠優於檔案 I/O。

安全性檢查清單

項目建議設定
Session ID 長度/熵值≥ 128 bits(本類別用 16 bytes random + 時間 + PID)
Cookie 參數HttpOnly、Secure(HTTPS)、SameSite=Lax/Strict
儲存目錄權限0700,僅擁有者可讀寫,防止其他用戶竊取
檔案鎖定使用 flock(LOCK_EX) 防止併發寫入資料損毀
Session 劫持防護驗證 User-Agent、IP 一致性;關鍵操作重新驗證密碼
定期輪換 ID登入後、權限變更後呼叫 regenerateId(true)

結語

自定檔案儲存 Session 雖非生產環境首選(建議直接用 Redis),但完整實作過程能深入理解 PHP Session 生命週期、序列化、鎖定、GC 等核心機制。本文提供的 FileSessionHandler 類別已實作 SessionHandlerInterface,可直接替換內建處理器,並具備生產級的安全性與擴充性。開發者可在此基礎上快速遷移至資料庫或 Redis 儲存,實現分散式 Session 共享。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved