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

PHP 自定義 Session 處理器實戰:文件、數據庫、Redis 存儲靈活切換

     編輯:PHP 綜合知識

為什麼需要自定義 Session Handler?

預設 session.save_handler = files 將 Session 存為文件,存在明顯短板:

  • 單機綁定:多服務器負載均衡時 Session 不共享
  • 性能瓶頸:文件 I/O 鎖競爭、inode 耗盡、GC 掃描慢
  • 無法查詢:難以實現「踢出用戶」、「在線用戶統計」、「會話審計」
  • 無法集群:不支援水平擴展

通過實現 SessionHandlerInterface 並註冊 session_set_save_handler(),可將 Session 存儲重定向到Redis、MySQL、MongoDB、Etcd、自定義加密文件等任意介質。

核心接口:SessionHandlerInterface

interface SessionHandlerInterface {
    public function open(string $savePath, string $sessionName): bool;
    public function close(): bool;
    public function read(string $id): string|false;
    public function write(string $id, string $data): bool;
    public function destroy(string $id): bool;
    public function gc(int $maxLifetime): int|false;
}

實現一:Redis 高性能 Handler(推薦生產)

class RedisSessionHandler implements SessionHandlerInterface {
    private Redis $redis;
    private string $prefix;
    private int $ttl;

    public function __construct(Redis $redis, string $prefix = 'sess:', int $ttl = 1800) {
        $this->redis = $redis;
        $this->prefix = $prefix;
        $this->ttl = $ttl;
    }

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

    public function read(string $id): string|false {
        $data = $this->redis->get($this->prefix . $id);
        return $data !== false ? $data : '';
    }

    public function write(string $id, string $data): bool {
        // 使用 SETEX 原子設置值與過期時間
        return $this->redis->setex($this->prefix . $id, $this->ttl, $data);
    }

    public function destroy(string $id): bool {
        return (bool)$this->redis->del($this->prefix . $id);
    }

    public function gc(int $maxLifetime): int|false {
        // Redis 自動過期,無需手動 GC
        return true;
    }
    
    // 額外業務方法
    public function getAllSessions(): array {
        $keys = $this->redis->keys($this->prefix . '*');
        return $keys ?: [];
    }
    
    public function getSessionData(string $id): ?array {
        $data = $this->read($id);
        return $data ? @unserialize($data) : null;
    }
    
    public function deleteUserSessions(int $userId): int {
        $deleted = 0;
        foreach ($this->getAllSessions() as $key) {
            $data = $this->getSessionData(str_replace($this->prefix, '', $key));
            if (isset($data['user_id']) && $data['user_id'] === $userId) {
                $this->destroy(str_replace($this->prefix, '', $key));
                $deleted++;
            }
        }
        return $deleted;
    }
}

註冊與使用

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 集群模式: $redis->connect('redis-cluster', 6379); 或使用 RedisCluster

$handler = new RedisSessionHandler($redis, 'myapp:sess:', 1800);

// PHP 7.0+ 推薦寫法
session_set_save_handler($handler, true); // true = register_shutdown_function
// 或
ini_set('session.save_handler', 'user');
session_set_save_handler($handler);

session_start();
// 正常使用 $_SESSION
$_SESSION['user_id'] = 123;

實現二:MySQL 數據庫 Handler(持久化、可審計)

CREATE TABLE `sessions` (
    `id` VARCHAR(128) NOT NULL PRIMARY KEY,
    `data` BLOB NOT NULL,
    `expires` INT UNSIGNED NOT NULL,
    `user_id` INT UNSIGNED NULL,
    `last_activity` INT UNSIGNED NOT NULL,
    INDEX `idx_expires` (`expires`),
    INDEX `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
class MySQLSessionHandler implements SessionHandlerInterface {
    private MySQLi $db;
    private int $ttl;

    public function __construct(MySQLi $db, int $ttl = 1800) {
        $this->db = $db;
        $this->ttl = $ttl;
    }

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

    public function read(string $id): string|false {
        $stmt = $this->db->prepare("SELECT `data` FROM `sessions` WHERE `id` = ? AND `expires` > UNIX_TIMESTAMP()");
        $stmt->bind_param("s", $id);
        $stmt->execute();
        $stmt->bind_result($data);
        $stmt->fetch();
        $stmt->close();
        return $data ?? '';
    }

    public function write(string $id, string $data): bool {
        $expires = time() + $this->ttl;
        $userId = $_SESSION['user_id'] ?? null;
        $stmt = $this->db->prepare("
            INSERT INTO `sessions` (`id`, `data`, `expires`, `user_id`, `last_activity`)
            VALUES (?, ?, ?, ?, UNIX_TIMESTAMP())
            ON DUPLICATE KEY UPDATE 
                `data` = VALUES(`data`),
                `expires` = VALUES(`expires`),
                `user_id` = VALUES(`user_id`),
                `last_activity` = UNIX_TIMESTAMP()
        ");
        $stmt->bind_param("ssisi", $id, $data, $expires, $userId);
        $result = $stmt->execute();
        $stmt->close();
        return $result;
    }

    public function destroy(string $id): bool {
        $stmt = $this->db->prepare("DELETE FROM `sessions` WHERE `id` = ?");
        $stmt->bind_param("s", $id);
        $result = $stmt->execute();
        $stmt->close();
        return $result;
    }

    public function gc(int $maxLifetime): int|false {
        $stmt = $this->db->prepare("DELETE FROM `sessions` WHERE `expires` < UNIX_TIMESTAMP()");
        $stmt->execute();
        $affected = $stmt->affected_rows;
        $stmt->close();
        return $affected;
    }
}

實現三:加密文件 Handler(安全合規、無外部依賴)

class EncryptedFileSessionHandler implements SessionHandlerInterface {
    private string $savePath;
    private string $key; // 32 字節密鑰

    public function __construct(string $savePath, string $key) {
        $this->savePath = rtrim($savePath, '/') . '/';
        $this->key = $key;
        if (!is_dir($this->savePath)) mkdir($this->savePath, 0700, true);
    }

    private function getFilePath(string $id): string {
        // 安全文件名:僅允許字母數字
        $safeId = preg_replace('/[^a-zA-Z0-9,-]/', '', $id);
        return $this->savePath . 'sess_' . $safeId;
    }

    private function encrypt(string $data): string {
        $iv = random_bytes(16);
        $ciphertext = openssl_encrypt($data, 'aes-256-gcm', $this->key, OPENSSL_RAW_DATA, $iv, $tag);
        return base64_encode($iv . $tag . $ciphertext);
    }

    private function decrypt(string $data): string {
        $raw = base64_decode($data);
        $iv = substr($raw, 0, 16);
        $tag = substr($raw, 16, 16);
        $ciphertext = substr($raw, 32);
        return openssl_decrypt($ciphertext, 'aes-256-gcm', $this->key, OPENSSL_RAW_DATA, $iv, $tag);
    }

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

    public function read(string $id): string|false {
        $file = $this->getFilePath($id);
        if (!file_exists($file)) return '';
        $encrypted = file_get_contents($file);
        try {
            return $this->decrypt($encrypted);
        } catch (Exception) {
            return ''; // 解密失敗視為無效
        }
    }

    public function write(string $id, string $data): bool {
        $file = $this->getFilePath($id);
        $encrypted = $this->encrypt($data);
        // 原子寫入
        $tmp = $file . '.tmp';
        file_put_contents($tmp, $encrypted);
        rename($tmp, $file);
        return true;
    }

    public function destroy(string $id): bool {
        $file = $this->getFilePath($id);
        return file_exists($file) ? unlink($file) : true;
    }

    public function gc(int $maxLifetime): int|false {
        $deleted = 0;
        foreach (glob($this->savePath . 'sess_*') as $file) {
            if (filemtime($file) + $maxLifetime < time()) {
                unlink($file);
                $deleted++;
            }
        }
        return $deleted;
    }
}

動態切換存儲策略

// config.php
return [
    'session' => [
        'handler' => env('SESSION_HANDLER', 'redis'), // file | mysql | redis | encrypted
        'redis'   => ['host' => '127.0.0.1', 'port' => 6379, 'prefix' => 'app:sess:', 'ttl' => 1800],
        'mysql'   => ['table' => 'sessions', 'ttl' => 1800],
        'file'    => ['path' => '/var/lib/php/sessions', 'encrypt_key' => ''],
    ],
];

// bootstrap.php
$config = require 'config.php';
$sessionCfg = $config['session'];

switch ($sessionCfg['handler']) {
    case 'redis':
        $redis = new Redis();
        $redis->connect($sessionCfg['redis']['host'], $sessionCfg['redis']['port']);
        $handler = new RedisSessionHandler($redis, $sessionCfg['redis']['prefix'], $sessionCfg['redis']['ttl']);
        break;
    case 'mysql':
        $db = new MySQLi(...);
        $handler = new MySQLSessionHandler($db, $sessionCfg['mysql']['ttl']);
        break;
    case 'encrypted':
        $handler = new EncryptedFileSessionHandler($sessionCfg['file']['path'], $sessionCfg['file']['encrypt_key']);
        break;
    default: // file
        ini_set('session.save_path', $sessionCfg['file']['path']);
        $handler = null; // 使用內建 files handler
}

if ($handler) {
    session_set_save_handler($handler, true);
}
session_start();

進階功能擴展

1. 會話數據結構化存儲(Redis Hash)

// 存儲為 Hash,支持字段級操作
$this->redis->hset($key, 'data', serialize($data));
$this->redis->hset($key, 'user_id', $userId);
$this->redis->hset($key, 'ip', $_SERVER['REMOTE_ADDR']);
$this->redis->expire($key, $this->ttl);

// 查詢用戶所有會話
$keys = $this->redis->keys($this->prefix . '*');
foreach ($keys as $key) {
    if ($this->redis->hget($key, 'user_id') == $targetUserId) {
        // 踢出、統計等
    }
}

2. 並發鎖保護(防止 Session 數據丟失)

public function write(string $id, string $data): bool {
    $lockKey = $this->prefix . 'lock:' . $id;
    $lock = $this->redis->set($lockKey, 1, ['nx', 'ex' => 10]); // 10秒鎖
    if (!$lock) return false; // 獲取鎖失敗
    
    try {
        return $this->redis->setex($this->prefix . $id, $this->ttl, $data);
    } finally {
        $this->redis->del($lockKey);
    }
}

3. 閒置續期 vs 絕對過期

  • 閒置續期(滑動窗口):每次 write 重置 TTL,活躍用戶永不掉線
  • 絕對過期:登入時設置固定過期時間,不管活躍度
  • Redis 可通過 Lua 腳本實現「續期但不超過最大壽命」

監控與運維指標

指標Redis 獲取方式告警閾值建議
活躍會話數DBSIZE 或 KEYS sess:* 計數> 預估峰值 80%
內存佔用INFO memory> 可用內存 70%
過期/淘汰率INFO stats expired_keys, evicted_keysevicted_keys > 0 即告警
慢查詢SLOWLOG GET> 10ms

總結

自定義 Session Handler 是 PHP 應用從單機走向分佈式、高可用、可審計的關鍵一步。Redis Handler 為首選(高性能、原生過期、豐富數據結構),MySQL Handler 適合強合規審計場景,加密文件 Handler 適合無外部依賴的安全敏感環境。通過統一接口封裝、配置驅動切換、擴展業務方法(踢出用戶、在線統計),即可構建企業級會話管理基礎設施。

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