默認 Session 以文件存儲,分布式/集群環境下無法共享。使用 session_set_save_handler() 註冊自定義回調,可將 Session 寫入 MySQL,實現多服務器 Session 共享、在線用戶統計、精準過期控制。
Session 完整實現<?php
/**
* 數據庫 Session 處理器
* 表結構:sessions(session_id CHAR(32) PK, uid INT, last_visit INT)
* 過期策略:1 小時無操作視為過期
*/
class Session {
private $db;
private $lasttime = 3600; // 過期秒數
public function __construct(&$db) {
$this->db = &$db;
session_module_name('user'); // 必須:聲明使用用戶級處理器
session_set_save_handler(
[$this, 'open'],
[$this, 'close'],
[$this, 'read'],
[$this, 'write'],
[$this, 'destroy'],
[$this, 'gc']
);
session_start(); // 必須在註冊後調用
}
// 反序列化 Session 數據(PHP 內部格式)
private function unserializes($data) {
$vars = preg_split('/([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\|/', $data, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$result = [];
for ($i = 0; isset($vars[$i]); $i++) {
$result[$vars[$i++]] = unserialize($vars[$i]);
}
return $result;
}
// 打開 Session(初始化連接等)
public function open($path, $name) { return true; }
// 關閉 Session(腳本結束時觸發)
public function close() {
$this->gc($this->lasttime); // 關閉時執行一次 GC
return true;
}
// 讀取 Session 數據
public function read($sessionId) {
$sql = "SELECT uid FROM sessions WHERE session_id = '" . $sessionId . "' LIMIT 1";
$query = $this->db->query($sql);
if ($row = $this->db->fetch_array($query)) {
return $row['uid']; // 返回字符串,PHP 會自動反序列化
}
return '';
}
// 寫入/更新 Session 數據
public function write($sessionId, $data) {
// 重新建立 DB 連接(長連接可能已斷開)
require_once(MRoot . DIR_WS_CLASSES . 'db_mysql_class.php');
$db1 = new DbCom();
$db1->connect(DB_SERVER, DB_SERVER_USERNAME, DB_SERVER_PASSWORD, DB_DATABASE);
$db1->query("SET NAMES utf8");
$this->db = $db1;
$data = addslashes($data);
$arr = $this->unserializes($data);
$sql0 = "SELECT uid FROM sessions WHERE session_id = '" . $sessionId . "' LIMIT 1";
$query0 = $this->db->query($sql0);
if ($this->db->num_rows($query0) <= 0) {
// 新 Session:插入記錄
if (isset($arr['webid']) && !empty($arr['webid'])) {
$this->db->query("INSERT INTO `sessions` SET `session_id` = '$sessionId', uid = '" . $arr['webid'] . "', last_visit = " . time());
}
} else {
// 存在則可選擇更新 last_visit 或 uid(此處示例略過更新以減少寫入)
}
return true;
}
// 銷毀 Session(登出調用)
public function destroy($sessionId) {
$this->db->query("DELETE FROM `sessions` WHERE `session_id` = '$sessionId'");
return true;
}
// 垃圾回收(清理過期 Session)
public function gc($lifetime) {
$this->db->query("DELETE FROM `sessions` WHERE UNIX_TIMESTAMP(NOW()) - `last_visit` > " . $this->lasttime);
return true;
}
}
?>
| 參數 | 默認值 | 說明與建議 |
|---|---|---|
| session.save_handler | files | 改為 user 啟用自定義處理器 |
| session.save_path | /tmp | 文件模式下的存儲路徑;用戶模式下忽略 |
| session.gc_probability | 1 | GC 概率分子,建議 1 |
| session.gc_divisor | 100 | GC 概率分母,建議 1000~5000 降低頻率 |
| session.gc_maxlifetime | 1440 | 過期秒數,需與代碼中 $lasttime 一致 |
| session.use_cookies | On | 是否用 Cookie 傳 SID |
| session.use_only_cookies | Off | 建議 On 防 URL 洩露 SID |
| session.cookie_httponly | Off | 建議 On 防 XSS 竊取 SID |
| session.hash_function | 0 (MD5) | 建議 1 (SHA-1) 更安全 |
| session.hash_bits_per_character | 4 | 建議 5 或 6 縮短 SID 長度 |
query() 改為 prepare()/execute()。write 重新連接,使用持久連接或單例模式管理 DB 句柄。gc_probability=1, gc_divisor=5000,並配合 cron 定時清理過期數據,避免請求阻塞。SELECT ... FOR UPDATE 或 Redis 鎖)。session.save_handler = redis(需安裝 phpredis 擴展),無需自寫類。