PHP Session 完整指南:原理、安全性與實戰應用
  編輯:PHP 綜合知識
PHP Session 機制深度解析與最佳實踐
Session 是 Web 開發中維持用戶狀態的核心機制。本文詳細說明 Session 原理、為何比 Cookie 安全、配置細節、登入驗證實作、常見陷阱與優化建議。
Session vs Cookie:核心差異
| 特性 |
Cookie |
Session |
| 儲存位置 |
客戶端瀏覽器 |
伺服器端 (檔案/資料庫/Redis) |
| 資料可見性 |
用戶可看、改、竊取 |
用戶無法直接存取 |
| 容量限制 |
單個 4KB,總量約 300 個 |
受限於伺服器磁碟/記憶體 |
| 生命週期 |
可設定持久化 |
通常隨瀏覽器關閉失效 (可調整) |
| 安全性 |
低 (需加密、簽名、HttpOnly) |
高 (資料不經網路傳輸) |
Session 運作流程
session_start() → 檢查 Cookie 中 PHPSESSID
- 有 ID → 讀取對應 Session 檔案還原
$_SESSION
- 無 ID → 產生新 ID、建立空 Session 檔案、設定 Cookie
- 腳本執行中操作
$_SESSION
- 腳本結束 /
session_write_close() → 序列化寫回儲存
基礎配置 (php.ini)
; 儲存路徑 (預設系統暫存目錄,建議自訂)
session.save_path = "/var/lib/php/sessions"
; 垃圾回收機率 (1/1000 請求觸發)
session.gc_probability = 1
session.gc_divisor = 1000
; Session 過期秒數 (24分鐘)
session.gc_maxlifetime = 1440
; Cookie 參數
session.cookie_lifetime = 0 ; 0 = 瀏覽器關閉失效
session.cookie_httponly = 1 ; 防 XSS 竊取
session.cookie_secure = 1 ; 僅 HTTPS 傳輸 (需 SSL)
session.use_strict_mode = 1 ; 防 Session Fixation
登入驗證完整實作
<?php
// login.php
session_start();
// 已登入導向首頁
if (isset($_SESSION['admin']) && $_SESSION['admin'] === true) {
header('Location: index.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// 1. 驗證輸入
if (empty($username) || empty($password)) {
$error = '請輸入帳號密碼';
} else {
// 2. 查詢資料庫 (使用 PDO 預處理)
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 3. 驗證密碼
if ($user && password_verify($password, $user['password_hash'])) {
// 4. 登入成功:重新產生 Session ID (防 Fixation)
session_regenerate_id(true);
// 5. 設定 Session 變數
$_SESSION['admin'] = true;
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $username;
$_SESSION['login_time'] = time();
header('Location: dashboard.php');
exit;
} else {
$error = '帳號或密碼錯誤';
}
}
}
?>
<form method="post">
帳號: <input name="username"><br>
密碼: <input name="password" type="password"><br>
<button type="submit">登入</button>
$error
"; ?>
</form>
受保護頁面驗證
<?php
// dashboard.php
session_start();
// 檢查登入狀態
if (!isset($_SESSION['admin']) || $_SESSION['admin'] !== true) {
header('Location: login.php');
exit;
}
// 選擇性:檢查 Session 逾時 (30 分鐘無操作)
if (time() - $_SESSION['login_time'] > 1800) {
session_destroy();
header('Location: login.php?timeout=1');
exit;
}
// 更新最後活動時間
$_SESSION['login_time'] = time();
?>
<h1>歡迎, = htmlspecialchars($_SESSION['username']) ?></h1>
<a href="logout.php">登出</a>
登出處理
<?php
// logout.php
session_start();
$_SESSION = []; // 清空所有 Session 變數
session_destroy(); // 刪除伺服器端 Session 檔案
setcookie(session_name(), '', time()-3600, '/'); // 刪除客戶端 Cookie
header('Location: login.php');
exit;
?>
常見問題與解決方案
| 問題 |
原因 |
解決方案 |
| Session 變數消失 |
未呼叫 session_start() |
每支需用 Session 的腳本開頭必須呼叫 |
| Headers already sent |
session_start() 前有輸出 (空白、BOM、echo) |
確保 session_start() 在最前面,檔案存 UTF-8 無 BOM |
| 多伺服器 Session 不共享 |
預設存檔案系統,各伺服器獨立 |
使用 Redis/Memcached/資料庫共享儲存 |
| Session 劫持 |
Session ID 被竊取 (XSS、中間人) |
HTTPS + HttpOnly + Secure + 定期重生 ID |
| Session 固定攻擊 |
攻擊者強迫受害者使用已知 Session ID |
session_regenerate_id(true) 登入後重生 ID |
高階:自訂 Session Handler (Redis 範例)
<?php
class RedisSessionHandler implements SessionHandlerInterface {
private $redis;
private $prefix = 'session:';
private $ttl = 1800; // 30 分鐘
public function __construct($host='127.0.0.1', $port=6379) {
$this->redis = new Redis();
$this->redis->connect($host, $port);
}
public function open($savePath, $sessionName) { return true; }
public function close() { return true; }
public function read($id) {
$data = $this->redis->get($this->prefix . $id);
return $data ?: '';
}
public function write($id, $data) {
return $this->redis->setex($this->prefix . $id, $this->ttl, $data);
}
public function destroy($id) {
return $this->redis->del($this->prefix . $id);
}
public function gc($maxlifetime) { return true; } // Redis 自動過期
}
// 註冊
ini_set('session.save_handler', 'user');
session_set_save_handler(new RedisSessionHandler(), true);
session_start();
?>