Session 是 Web 應用狀態保持的核心機制。ThinkPHP 封裝了 Session 類提供靜態介面,但底層仍依賴 PHP 原生 session_* 函數。本文系統梳理 ThinkPHP Session 用法、配置優化、常見失效原因、安全加固、以及 Redis/Memcached 分散式儲存擴展方案。
| 方法 | 說明 | 範例 |
|---|---|---|
Session::set($name, $value) | 寫入 session | Session::set('user_id', 123); |
Session::get($name, $default = null) | 讀取 session | $uid = Session::get('user_id'); |
Session::has($name) | 檢查是否存在 (TP5+) | if (Session::has('user_id')) ... |
Session::pull($name, $default = null) | 讀取並刪除 (TP5+) | $msg = Session::pull('flash_message'); |
Session::delete($name) | 刪除指定鍵 | Session::delete('temp_data'); |
Session::clear() | 清空當前 session 所有資料 | Session::clear(); |
Session::destroy() | 銷毀 session (含檔案/資料庫記錄) | Session::destroy(); |
Session::id($id = null) | 獲取/設定 session ID | Session::id($newId); |
Session::regenerate($destroy = false) | 重新生成 ID (防固定攻擊) | Session::regenerate(true); |
<!-- ThinkPHP 3.x 模板標籤 -->
<present name="_SESSION.user_id">
歡迎,<?php echo $_SESSION['username']; ?>
<else />
<a href="<?php echo U('User/login'); ?>">登入</a>
</present>
<!-- ThinkPHP 5/6/8 模板 (原生 PHP) -->
<?php if (session('user_id')): ?>
歡迎,<?php echo session('username'); ?>
<?php else: ?>
<a href="<?php echo url('user/login'); ?>">登入</a>
<?php endif; ?>
<?php
return [
// 驅動: file (預設), redis, memcache, database, custom
'type' => 'redis',
// Redis 連接參數 (type=redis 時生效)
'host' => '127.0.0.1',
'port' => 6379,
'password' => '',
'database' => 0,
'timeout' => 2,
'persistent' => false,
// 通用參數
'prefix' => 'tp_session_', // 鍵前綴,避免衝突
'expire' => 7200, // 生命期 (秒),預設 2 小時
'use_cookies' => true, // 使用 Cookie 傳遞 ID
'cookie_name' => 'PHPSESSID', // Cookie 名稱
'cookie_path' => '/', // Cookie 路徑
'cookie_domain' => '', // Cookie 域 (跨子域共享設 .example.com)
'cookie_secure' => false, // 僅 HTTPS 傳輸 (生產必開 true)
'cookie_httponly' => true, // 禁止 JS 讀取 (防 XSS)
'cookie_samesite' => 'Lax', // CSRF 防護: Lax/Strict/None
'gc_probability' => 1, // GC 機率分子
'gc_divisor' => 1000, // GC 機率分母 (1/1000 請求觸發)
'cache_limiter' => 'nocache', // 防止頁面被緩存
// 自動啟動 (預設 true,控制器構造前已啟動)
'auto_start' => true,
];
Session::set() 必須大寫 S,session::set() 會報錯「Class not found」。session_start() (框架自動調用) 之前。檢查 index.php 首行是否為 <?php 且無前導空行。session.save_path 目錄需 Web 使用者可寫 (Linux: chown www:www /var/lib/php/session; chmod 700 ...)。example.com 登入,子域 api.example.com 讀取失敗。解決:'cookie_domain' => '.example.com' (前綴點號)。cookie_samesite = 'Lax' 可能阻擋跨站 POST 請求攜帶 Cookie。API 介面建議改用 Token 認證 (JWT),不依賴 Session Cookie。Session::regenerate(true)。<?php
// 登入控制器
public function login() {
if ($this->request->isPost()) {
$user = User::verify($this->request->post('account'), $this->request->post('password'));
if ($user) {
// 1. 重新生成 Session ID (防 Session Fixation)
Session::regenerate(true);
// 2. 寫入最小必要資訊 (避免存放敏感資料)
Session::set('user_id', $user->id);
Session::set('username', $user->username);
Session::set('role_ids', $user->role_ids); // 陣列
Session::set('last_login_ip', $this->request->ip());
Session::set('last_login_time', time());
// 3. 設定較短的閒置超時 (配合 remember_me 延長)
if ($this->request->post('remember')) {
// 延長 Cookie 存活 (需配合 session.cookie_lifetime)
Session::set('remember_me', true);
}
$this->success('登入成功', url('index/index'));
}
$this->error('帳號或密碼錯誤');
}
return $this->fetch();
}
// 登出控制器
public function logout() {
Session::destroy(); // 徹底銷毀
$this->success('已登出', url('user/login'));
}
// 基礎控制器權限檢查
class BaseController extends Controller {
protected function initialize() {
if (!Session::has('user_id') && !$this->isPublicAction()) {
$this->redirect(url('user/login')->with('redirect', $this->request->url()));
}
// 刷新最後活動時間 (可選)
Session::set('last_activity', time());
}
}
composer require topthink/think-redis
<?php
// config/session.php
return [
'type' => 'redis',
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD', ''),
'database' => env('REDIS_SESSION_DB', 1), // 獨立 DB 隔離
'prefix' => 'sess:',
'expire' => 7200,
// 連接池選項 (think-redis 5.0+)
'options' => [
'timeout' => 2.0,
'read_timeout' => 2.0,
'retry_interval' => 100,
],
];
<?php
// extend/session/driver/RedisCompressed.php
namespace extend\session\driver;
use think\SessionHandlerInterface;
class RedisCompressed implements SessionHandlerInterface {
private \Redis $redis;
private string $prefix;
private int $expire;
public function __construct(array $config) {
$this->redis = new \Redis();
$this->redis->connect($config['host'], $config['port'], $config['timeout'] ?? 2);
if ($config['password']) $this->redis->auth($config['password']);
if ($config['database']) $this->redis->select($config['database']);
$this->prefix = $config['prefix'] ?? 'sess:';
$this->expire = $config['expire'] ?? 7200;
}
public function open($savePath, $sessionName): bool { return true; }
public function close(): bool { return $this->redis->close(); }
public function read($sessionId): string|false {
$data = $this->redis->get($this->prefix . $sessionId);
if ($data === false) return '';
// 解壓縮 (假設存儲時用 gzcompress)
return gzuncompress($data) ?: '';
}
public function write($sessionId, $data): bool {
// 壓縮大數據 (超過 1KB)
$compressed = (strlen($data) > 1024) ? gzcompress($data) : $data;
return $this->redis->setex($this->prefix . $sessionId, $this->expire, $compressed);
}
public function destroy($sessionId): bool {
return $this->redis->del($this->prefix . $sessionId) > 0;
}
public function gc($maxLifetime): int|false { return true; } // Redis 自動過期
}
配置使用:'type' => '\\extend\\session\\driver\\RedisCompressed'
純 Session 模式在微服務、前後端分離、多端 (Web/App/小程式) 場景下受限。推薦:
Set-Cookie: PHPSESSID=... 和 access_token,前端根據場景選擇攜帶方式。| 指標 | 建議值 | 說明 |
|---|---|---|
| session.gc_probability / gc_divisor | 1 / 1000 | 降低 GC 頻率,高併發下減少鎖競爭 |
| Redis 連接池大小 | 50-200 | 根據 PHP-FPM 進程數 * 1.5 估算 |
| Session 資料大小 | < 64 KB | 超過建議拆分至 Redis Hash/資料庫,Session 僅存 ID |
| 序列化格式 | igbinary / msgpack | 比 PHP 原生 serialize 快 2-3 倍、體積小 30-50% |
| 啟用 Redis 壓縮 | 大值 > 1KB | 網路傳輸與記憶體雙重節省 |
ThinkPHP Session 封裝雖薄,但掌握其配置細節、失效排查邏輯、安全加固要點與分散式擴展路徑,是構建高可用 PHP 應用的必備功力。隨著架構演進,Session 歸 Session (有狀態 Web),JWT 歸 JWT (無狀態 API),根據業務場景選擇合適的狀態管理方案,而非一味追求「統一」。