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

PHP session_set_save_handler 將 Session 存入 MySQL 完整實作:原生函式、類別封裝、分散式部署與安全加固

     編輯:PHP 相關知識

PHP session_set_save_handler 將 Session 存入 MySQL 完整實作:原生函式、類別封裝、分散式部署與安全加固

預設 PHP Session 以檔案儲存 (session.save_handler = files),單機無問題,但多伺服器負載均衡下會導致 Session 不共享、用戶頻繁掉線。將 Session 存入 MySQL/Redis 可實現分散式共享。本文提供 session_set_save_handler() 完整回調實作、PDO 類別封裝、資料表設計、垃圾回收策略與生產環境檢查清單。

一、資料表設計(支援鎖、過期、用戶代理綁定)

CREATE TABLE `php_sessions` (
    `id` CHAR(32) NOT NULL PRIMARY KEY,           -- Session ID
    `data` MEDIUMBLOB NOT NULL,                   -- 序列化資料
    `expires` INT UNSIGNED NOT NULL,              -- 過期 Unix 時間戳
    `user_id` INT UNSIGNED DEFAULT NULL,          -- 關聯用戶 ID(可選)
    `user_agent` VARCHAR(255) DEFAULT NULL,       -- 綁定 UA 防劫持
    `ip` VARCHAR(45) DEFAULT NULL,                -- 綁定 IP(可選)
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX `idx_expires` (`expires`),
    INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

二、原生函式回調實作(PHP 7.0+,PDO 預處理)

<?php
// session_mysql_handler.php - 引入即用
$dsn = 'mysql:host=127.0.0.1;dbname=session_db;charset=utf8mb4';
$pdo = new PDO($dsn, 'session_user', 'strong_password', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$lifetime = ini_get('session.gc_maxlifetime') ?: 1440; // 秒

// 1. open($savePath, $sessionName) -> bool
function sessionOpen($savePath, $sessionName) { return true; }

// 2. close() -> bool
function sessionClose() { return true; }

// 3. read($id) -> string|false
function sessionRead($id) {
    global $pdo;
    $stmt = $pdo->prepare('SELECT `data` FROM `php_sessions` WHERE `id` = ? AND `expires` > ?');
    $stmt->execute([$id, time()]);
    $row = $stmt->fetch();
    return $row ? $row['data'] : '';
}

// 4. write($id, $data) -> bool
function sessionWrite($id, $data) {
    global $pdo, $lifetime;
    $expires = time() + $lifetime;
    // REPLACE INTO 或 INSERT ... ON DUPLICATE KEY UPDATE
    $stmt = $pdo->prepare('
        INSERT INTO `php_sessions` (`id`, `data`, `expires`) VALUES (?, ?, ?)
        ON DUPLICATE KEY UPDATE `data` = VALUES(`data`), `expires` = VALUES(`expires`), `updated_at` = CURRENT_TIMESTAMP
    ');
    return $stmt->execute([$id, $data, $expires]);
}

// 5. destroy($id) -> bool
function sessionDestroy($id) {
    global $pdo;
    $stmt = $pdo->prepare('DELETE FROM `php_sessions` WHERE `id` = ?');
    return $stmt->execute([$id]);
}

// 6. gc($maxLifetime) -> bool
function sessionGC($maxLifetime) {
    global $pdo;
    $stmt = $pdo->prepare('DELETE FROM `php_sessions` WHERE `expires` < ?');
    return $stmt->execute([time()]);
}

// 註冊處理器(必須在 session_start() 之前)
session_set_save_handler(
    'sessionOpen', 'sessionClose', 'sessionRead', 'sessionWrite', 'sessionDestroy', 'sessionGC'
);
// 關鍵:PHP 7.0+ 支援 register_shutdown_function 自動 write/close
session_start();

三、現代類別封裝(PSR-4、介面導向、依賴注入友好)

<?php
namespace App\Session;

use PDO;
use SessionHandlerInterface;

class PdoSessionHandler implements SessionHandlerInterface {
    private PDO $pdo;
    private int $lifetime;
    private string $table;

    public function __construct(PDO $pdo, string $table = 'php_sessions', int $lifetime = 0) {
        $this->pdo = $pdo;
        $this->table = $table;
        $this->lifetime = $lifetime ?: (ini_get('session.gc_maxlifetime') ?: 1440);
    }

    public function open($savePath, $sessionName): bool { return true; }
    public function close(): bool { return true; }

    public function read($id): string|false {
        $stmt = $this->pdo->prepare("SELECT `data` FROM `{$this->table}` WHERE `id` = ? AND `expires` > ?");
        $stmt->execute([$id, time()]);
        return $stmt->fetchColumn() ?: '';
    }

    public function write($id, $data): bool {
        $expires = time() + $this->lifetime;
        $stmt = $this->pdo->prepare("
            INSERT INTO `{$this->table}` (`id`, `data`, `expires`) VALUES (?, ?, ?)
            ON DUPLICATE KEY UPDATE `data` = VALUES(`data`), `expires` = VALUES(`expires`)
        ");
        return $stmt->execute([$id, $data, $expires]);
    }

    public function destroy($id): bool {
        $stmt = $this->pdo->prepare("DELETE FROM `{$this->table}` WHERE `id` = ?");
        return $stmt->execute([$id]);
    }

    public function gc($maxLifetime): int|false {
        $stmt = $this->pdo->prepare("DELETE FROM `{$this->table}` WHERE `expires` < ?");
        $stmt->execute([time()]);
        return $stmt->rowCount();
    }
}

// 使用(Laravel/Symfony/自建容器皆可注入)
$handler = new PdoSessionHandler($pdo);
session_set_save_handler($handler, true); // true = register_shutdown_function
session_start();

四、進階功能:用戶代理/IP 綁定、並發鎖、閒置延長

<?php
// 在 write() 中加入綁定與樂觀鎖
public function write($id, $data): bool {
    $expires = time() + $this->lifetime;
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $ip = $_SERVER['REMOTE_ADDR'] ?? '';
    $userId = $_SESSION['user_id'] ?? null;

    // 使用 ROW_LOCK 避免並發寫入覆蓋(InnoDB 行鎖)
    $this->pdo->beginTransaction();
    try {
        // SELECT ... FOR UPDATE 鎖定該行
        $stmt = $this->pdo->prepare("SELECT `id` FROM `{$this->table}` WHERE `id` = ? FOR UPDATE");
        $stmt->execute([$id]);
        $exists = $stmt->fetchColumn();

        if ($exists) {
            $stmt = $this->pdo->prepare("
                UPDATE `{$this->table}` 
                SET `data` = ?, `expires` = ?, `user_agent` = ?, `ip` = ?, `user_id` = ?, `updated_at` = CURRENT_TIMESTAMP
                WHERE `id` = ?
            ");
            $stmt->execute([$data, $expires, $ua, $ip, $userId, $id]);
        } else {
            $stmt = $this->pdo->prepare("
                INSERT INTO `{$this->table}` (`id`, `data`, `expires`, `user_agent`, `ip`, `user_id`)
                VALUES (?, ?, ?, ?, ?, ?)
            ");
            $stmt->execute([$id, $data, $expires, $ua, $ip, $userId]);
        }
        $this->pdo->commit();
        return true;
    } catch (Exception $e) {
        $this->pdo->rollBack();
        return false;
    }
}

// read() 時驗證 UA/IP(防 Session 劫持)
public function read($id): string|false {
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $ip = $_SERVER['REMOTE_ADDR'] ?? '';
    $stmt = $this->pdo->prepare("
        SELECT `data` FROM `{$this->table}` 
        WHERE `id` = ? AND `expires` > ? AND `user_agent` = ? AND `ip` = ?
    ");
    $stmt->execute([$id, time(), $ua, $ip]);
    return $stmt->fetchColumn() ?: '';
}

五、生產環境部署檢查清單

項目建議配置
資料庫連線持久連線 PDO::ATTR_PERSISTENT => true、連線池 (PdoPool/ProxySQL)
GC 執行機率session.gc_probability = 1, session.gc_divisor = 1000 (1/1000 請求觸發,減少鎖爭用)
Session ID 長度session.sid_length = 32, session.sid_bits_per_character = 5 (更安全)
Cookie 設定session.cookie_httponly = 1, session.cookie_secure = 1 (HTTPS), session.cookie_samesite = 'Lax'
嚴格模式session.use_strict_mode = 1 (防 Session Fixation)
備選方案高併發首選 Redis Cluster (phpredis)、Memcached,MySQL 適合中小流量/需關聯查詢場景
監控指標Session 表行數、GC 耗時、鎖等待、寫入 QPS

六、為何不直接用框架內建?

  • Laravel: config/session.php → 'driver' => 'database',自動建表、支援 Redis/Memcached/Array/Cookie。
  • Symfony: framework.session.handler_id: session.handler.native 或 pdo。
  • ThinkPHP: session 配置 type: pdo。
  • 自建可完全控制表結構、鎖策略、綁定邏輯,適合非框架專案或特殊需求。

總結: session_set_save_handler() + MySQL/PDO 實現分散式 Session 共享,核心在於「正確實作六個回調、資料表含過期索引、GC 機率調優、寫入加鎖、讀取驗證 UA/IP」。中大型專案建議直接使用 Redis Cluster 或框架內建驅動,僅在需關聯用戶查詢、審計日誌時選 MySQL。

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