Session 是 PHP 實現有狀態會話的核心機制。儘管用法看似簡單(session_start()、$_SESSION),但其底層涉及 **ID 生成與傳遞、儲存介質、垃圾回收、跨域/跨伺服器共享、安全防護** 等多個關鍵環節。本文系統梳理 Session 完整生命週期,解析常見跨頁失效原因,提供分散式共享方案與安全加固清單,助你徹底掌握 Session 核心技術。
session_start() 檢查請求中是否攜帶 Session ID(Cookie PHPSESSID 或 URL 參數)。/tmp/sess_)還原 $_SESSION。session_create_id()),建立空 $_SESSION,並透過 Set-Cookie 送給客戶端。$_SESSION 的操作暫存於記憶體。session_write_close() 時,將 $_SESSION 序列化寫回儲存介質,釋放鎖定。session.gc_probability/divisor) 掃描儲存目錄,刪除修改時間超過 session.gc_maxlifetime 的檔案。預設 files handler 以 flock() 鎖定 Session 檔案,**同一 Session ID 的並發請求會串列執行**(第二個請求阻塞等待第一個釋放鎖)。長時間運行的請求(如大檔下載、慢速 API)會卡住同用戶其他請求。解法:session_write_close() 提前釋放鎖,或改用 Redis/Memcached 無鎖 handler。
| 函數 | 用途 | 注意事項 |
|---|---|---|
session_start([$options]) | 啟動/恢復會話 | PHP 7.0+ 支援陣列選項覆蓋 ini 設定 |
session_id([$id]) | 取得/設定當前 ID | 設定時需在 session_start() **之前** |
session_name([$name]) | 取得/設定 Cookie 名稱(預設 PHPSESSID) | 設定需在 session_start() 之前;名稱需含字母 |
session_regenerate_id($delete_old=false) | 產生新 ID,選擇性刪除舊檔 | 登入/權限變更**必須**呼叫,防 Session Fixation |
session_destroy() | 刪除伺服器端儲存資料 | 不會清除 $_SESSION 變數,需配合 $_SESSION=[] + 刪 Cookie |
session_write_close() | 提前寫入並釋放鎖 | 長耗時操作前呼叫,避免併發阻塞 |
session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly) | 設定 Session Cookie 參數 | 必須在 session_start() 之前;PHP 7.3+ 支援 samesite |
session_get_cookie_params() | 取得當前 Cookie 參數陣列 | 用於刪除 Cookie 時復用參數 |
session_status() | 回傳狀態常數:PHP_SESSION_DISABLED/ACTIVE/NONE | 避免重複 session_start() 拋出 Notice |
<?php
session_start();
// 註冊
$_SESSION['user'] = ['id'=>1001, 'name'=>'Alice', 'roles'=>['admin']];
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// 讀取
$userName = $_SESSION['user']['name'] ?? 'Guest';
// 檢查存在
if (isset($_SESSION['user'])) { /* 已登入 */ }
// 刪除單鍵
unset($_SESSION['csrf_token']);
// 清空所有會話資料
$_SESSION = [];
// 徹底銷毀(登出標準流程)
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
session_destroy();
?>
棄用函數:session_register()、session_unregister()、session_is_registered()、$HTTP_SESSION_VARS 已在 PHP 5.4 移除,**嚴禁使用**。
session.use_cookies = 1、session.use_only_cookies = 1(強制僅用 Cookie,關閉 URL 傳遞)。lifetime=0(瀏覽器關閉失效)、path=/、domain=.example.com(共享子域)、secure=1、httponly=1、samesite=Lax。session.use_trans_sid = 1 + 編譯 --enable-trans-sid。<a href="page.php"> → <a href="page.php?PHPSESSID=abc123">。<?php
// 客戶端攜帶 Header
// Authorization: Bearer
// 或自訂 Header: X-Session-ID: abc123
// 伺服器端讀取
$sid = $_SERVER['HTTP_X_SESSION_ID'] ?? $_GET['sid'] ?? null;
if ($sid) {
session_id($sid); // 必須在 session_start() 之前
}
session_start();
?>
| 原因 | 現象 | 解決方案 |
|---|---|---|
| 客戶端禁用/不支援 Cookie | 每次請求生成新 Session ID | 1. 引導用戶開啟 Cookie 2. 降級用 URL 參數(風險自負) 3. API 改用 Token (JWT) 無狀態認證 |
| 子域/跨域 Cookie 不共享 | a.example.com 登入,b.example.com 失效 | session_set_cookie_params(0, '/', '.example.com', true, true) 設定根域 |
| 多伺服器負載均衡,Session 檔案本地儲存 | 請求分發到不同機器,讀不到 Session | **集中式儲存**:Redis / Memcached / Database(見下節) |
session.auto_start = 1 導致類別未載入就序列化 | 物件變成 __PHP_Incomplete_Class | 關閉 session.auto_start,在 bootstrap 手動 session_start() |
| 輸出內容前未啟動 Session(Header already sent) | session_start() 拋出 Warning,Cookie 未送達 | 確保 session_start() 在**任何輸出前**(包括 BOM、空白行) |
<?php
// php.ini 或執行期設定
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://redis-cluster:6379?auth=password&database=1&prefix=session_');
// 選項:timeout, read_timeout, retry_interval, weight 等
session_start();
?>
session.gc_maxlifetime 仍需設定,Redis 端以 TTL 為準;生產環境建議啟用 session.use_strict_mode=1。CREATE TABLE `php_sessions` (
`sess_id` VARCHAR(128) NOT NULL PRIMARY KEY,
`data` BLOB NOT NULL,
`expiry` INT UNSIGNED NOT NULL,
INDEX `idx_expiry` (`expiry`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
class DbSessionHandler implements SessionHandlerInterface {
private $pdo;
public function __construct(PDO $pdo) { $this->pdo = $pdo; }
public function open($path, $name) { return true; }
public function close() { return true; }
public function read($id) {
$stmt = $this->pdo->prepare("SELECT data FROM php_sessions WHERE sess_id = ? AND expiry > ?");
$stmt->execute([$id, time()]);
return $stmt->fetchColumn() ?: '';
}
public function write($id, $data) {
$expiry = time() + (int)ini_get('session.gc_maxlifetime');
$stmt = $this->pdo->prepare("INSERT INTO php_sessions (sess_id, data, expiry) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE data = VALUES(data), expiry = VALUES(expiry)");
return $stmt->execute([$id, $data, $expiry]);
}
public function destroy($id) {
return $this->pdo->prepare("DELETE FROM php_sessions WHERE sess_id = ?")->execute([$id]);
}
public function gc($maxlifetime) {
return $this->pdo->prepare("DELETE FROM php_sessions WHERE expiry < ?")->execute([time()]);
}
}
// 註冊
$handler = new DbSessionHandler($pdo);
session_set_save_handler($handler, true);
session_start();
?>
將 session.save_path 指向掛載的 NFS 目錄。**不推薦**高併發場景,檔案鎖在網路檔案系統上可靠性低。
session.cookie_secure=1、session.cookie_httponly=1、session.cookie_samesite=Lax。session.use_strict_mode=1,拒絕未初始化的 Session ID,防 Fixation。session_regenerate_id(true)。session.gc_maxlifetime=1440(24h: 86400),session.cookie_lifetime=0(瀏覽器關閉即失效)。session.sid_length/session.sid_bits_per_character 相關設定。| 現象 | 可能原因 | 排查步驟 |
|---|---|---|
| 每次請求 $_SESSION 都是空的 | session_start() 前有輸出 / Cookie 被瀏覽器拒 / 域名不匹配 | 檢查 error_log 是否有 "headers already sent";開發者工具 Network 觀察 Set-Cookie;確認 domain 設定 |
| 登入後跳轉卻未登入 | 重導向時 Session 尚未寫入 / 跨域 Cookie 遺失 | 登入邏輯結束呼叫 session_write_close() 再 header(); 設定根域 Cookie |
| Session 資料莫名消失 | GC 過於激進 / 多伺服器未共享 / 磁碟滿 / Redis 記憶體淘汰 | 調大 gc_maxlifetime / 檢查 save_handler 共享 / 監控磁碟/Redis 記憶體 |
| 並發請求嚴重卡頓 | Session 檔案鎖阻塞 | 長任務前 session_write_close() / 改用 Redis handler |
| 物件變成 __PHP_Incomplete_Class | session.auto_start=1 且類別定義在 start 之後載入 | 關閉 auto_start,bootstrap 載入類別後再 session_start() |
Session 看似簡單,實則涉及 **ID 生命週期、儲存引擎、鎖機制、跨域傳遞、分散式共享、安全防禦** 等多層技術棧。掌握核心原理、熟悉 session_set_save_handler() 自訂儲存、配合 Redis/Database 實現無狀態水平擴展、落實安全加固清單,才能構建高併發、高可用、安全的會話系統。記住:**Session 是伺服器端狀態,ID 只是鑰匙;保護好鑰匙傳輸管道(HTTPS + HttpOnly + SameSite),集中管理鎖的資源(Redis),才是正道。**