用戶認證是 Web 應用的基石。本文在上一篇雙重驗證方案基礎上,進一步深入剖析 Session 與 Cookie 的本質區別、安全攻防細節、分佈式架構下的 Session 共享方案,並提供生產級代碼架構參考。
| 維度 | Session | Cookie |
|---|---|---|
| 存儲位置 | 服務端(文件/Redis/Memcached/數據庫) | 客戶端瀏覽器(內存/硬盤) |
| 生命周期 | 瀏覽器關閉失效(默認),可配置 gc_maxlifetime | 可設置 Expires/Max-Age 持久化 |
| 容量限制 | 受服務端存儲限制,通常無實際上限 | 單個 4KB,域名下總計 ~300 個 |
| 安全性 | 高(數據不離開服務端) | 低(可被竄改、偷取、XSS 讀取) |
| 網絡開銷 | 僅傳 Session ID(~32 字節) | 每請求攜帶完整數據 |
| 跨域支持 | 需配合 CORS + SameSite=None+Secure | 原生支持跨域(受 SameSite 限制) |
| 典型用途 | 用戶身份、權限、購物車、臨時數據 | 偏好設置、追蹤標識、記住我 Token |
<?php
// 攻擊者構造鏈接誘導受害者點擊
// https://victim.com/login.php?PHPSESSID=attacker_known_id
// 受害者登錄後,攻擊者使用同一 Session ID 即可劫持會話
// 防護:啟用 strict_mode + 登錄後強制重生成 ID
ini_set('session.use_strict_mode', '1');
// 登錄成功後
session_regenerate_id(true); // 刪除舊 Session 文件
?>
<?php
namespace App\Middleware;
class SessionSecurityMiddleware {
public function handle(callable $next): void {
// 1. 啟用安全配置(僅執行一次)
static $initialized = false;
if (!$initialized) {
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']) ? '1' : '0');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', '1');
ini_set('session.use_trans_sid', '0'); // 禁用 URL 透傳 SID
ini_set('session.hash_function', 'sha256');
ini_set('session.hash_bits_per_character', '5');
$initialized = true;
}
// 2. 檢測 Session 劫持指標
$this->detectHijacking();
// 3. 定期輪換 Session ID(每 30 分鐘或關鍵操作後)
$this->rotateIfNeeded();
$next();
}
private function detectHijacking(): void {
$fp = $_SESSION['__fingerprint'] ?? null;
$current = $this->generateFingerprint();
if ($fp && !hash_equals($fp, $current)) {
// 指紋不匹配:可能被劫持
error_log("Session 劫持嘗試: " . session_id());
session_destroy();
throw new \Exception('會話異常,請重新登錄');
}
$_SESSION['__fingerprint'] = $current;
}
private function generateFingerprint(): string {
// 綁定 User-Agent + IP 前綴(避免動態 IP 導致誤判)
$ipPrefix = implode('.', array_slice(explode('.', $_SERVER['REMOTE_ADDR'] ?? ''), 0, 2));
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
return hash('sha256', $ipPrefix . '|' . $ua);
}
private function rotateIfNeeded(): void {
$lastRotate = $_SESSION['__rotate_at'] ?? 0;
if (time() - $lastRotate > 1800) { // 30 分鐘
session_regenerate_id(true);
$_SESSION['__rotate_at'] = time();
}
}
}
?>
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Redis 集中存儲 | 高性能、原子操作、支持 TTL、集群模式 | 需維護 Redis 集群、單點風險 | 中大型應用、微服務 |
| 數據庫持久化 | ACID、無額外組件、便於審計 | 寫入延遲高、連接池壓力大 | 強一致性要求、低併發 |
| Sticky Session (Nginx ip_hash) | 無代碼改動、簡單 | 擴展性差、節點故障導致 Session 丟失 | 遷移過渡期、有狀態服務 |
| JWT 無狀態 | 水平擴展完美、無存儲依賴 | 無法主動失效、Payload 體積大、簽名驗證 CPU 開銷 | API 服務、單頁應用 |
<?php
namespace App\Session;
class RedisSessionHandler implements \SessionHandlerInterface {
private \Redis $redis;
private string $prefix;
private int $ttl;
public function __construct(\Redis $redis, string $prefix = 'sess:', int $ttl = 7200) {
$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($sessionId): string {
$key = $this->prefix . $sessionId;
$data = $this->redis->get($key);
return $data ?? '';
}
public function write($sessionId, $data): bool {
$key = $this->prefix . $sessionId;
return $this->redis->setex($key, $this->ttl, $data);
}
public function destroy($sessionId): bool {
$key = $this->prefix . $sessionId;
return (bool) $this->redis->del($key);
}
public function gc($maxLifetime): int|false {
// Redis 自動過期,無需掃描
return 0;
}
}
// 註冊
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$handler = new RedisSessionHandler($redis);
session_set_save_handler($handler, true);
session_start();
?>
<?php
function setSecureCookie(
string $name,
string $value,
int $expire = 0,
string $path = '/',
string $domain = '',
bool $secure = true,
bool $httponly = true,
string $samesite = 'Lax'
): void {
$options = [
'expires' => $expire,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => $httponly,
'samesite' => $samesite,
];
// PHP 7.3+ 支持 options 參數
if (PHP_VERSION_ID >= 70300) {
setcookie($name, $value, $options);
} else {
// 兼容舊版本
$cookie = $name . '=' . rawurlencode($value);
$cookie .= '; Max-Age=' . ($expire - time());
$cookie .= '; Path=' . $path;
if ($domain) $cookie .= '; Domain=' . $domain;
if ($secure) $cookie .= '; Secure';
if ($httponly) $cookie .= '; HttpOnly';
$cookie .= '; SameSite=' . $samesite;
header('Set-Cookie: ' . $cookie, false);
}
}
?>
graph TD
A[項目類型] -->|傳統 Web 多頁面| B[Session + Redis]
A -->|API/SPA/微服務| C[JWT + 短期 Access Token + Refresh Token]
A -->|混合架構| D[Session(Web) + JWT(API)]
B --> E{併發量}
E -->|高| F[Redis Cluster + 本地緩存]
E -->|中低| G[單機 Redis + 持久化]
C --> H{撤銷需求}
H -->|強撤銷| I[Redis 黑名單 + 短過期]
H -->|弱撤銷| J[純 JWT 短過期]
核心原則:敏感操作用 Session,跨域無狀態用 JWT,記住我用簽名 Cookie,集群必上 Redis,安全屬性全開啟。