CodeIgniter (CI) 的 Session **不使用 PHP 原生 $_SESSION**,而是實作 **Cookie-based Session**,可選擇資料庫儲存、支援閃存資料、IP/User-Agent 綁定、定期 GC。本文逐層剖析 CI_Session 核心邏輯。
class CI_Session {
var $sess_encrypt_cookie = FALSE; // 加密 Cookie
var $sess_use_database = FALSE; // 寫入資料庫
var $sess_table_name = ''; // Session 資料表
var $sess_expiration = 7200; // 過期秒數 (預設 2 小時)
var $sess_expire_on_close = FALSE; // 關閉瀏覽器即過期
var $sess_match_ip = FALSE; // 綁定 IP
var $sess_match_useragent = TRUE; // 綁定 User-Agent (前 120 字元)
var $sess_cookie_name = 'ci_session';
var $cookie_prefix = '';
var $cookie_path = '/';
var $cookie_domain = '';
var $cookie_secure = FALSE; // 僅 HTTPS
var $sess_time_to_update = 300; // Cookie 更新間隔 (5 分鐘)
var $encryption_key = ''; // 必填,config.php 設定
var $flashdata_key = 'flash';
var $gc_probability = 5; // GC 機率 5%
var $userdata = []; // 當前 Session 資料
var $CI; // CI 超級物件
var $now; // 當前時間戳
}
__construct()encryption_key 非空string helper、encrypt library (若加密)、database (若用 DB)$this->nowsess_read(),失敗則建立新 Session sess_create()_flashdata_sweep() 清除舊閃存,_flashdata_mark() 將新閃存標記為 old_sess_gc()function sess_read() {
// 1. 從 Cookie 取得資料
$session = $this->CI->input->cookie($this->sess_cookie_name);
if ($session === FALSE) return FALSE;
// 2. 解密或驗證 HMAC
if ($this->sess_encrypt_cookie) {
$session = $this->CI->encrypt->decode($session);
} else {
$hash = substr($session, -32); // 後 32 字元為 MD5
$session = substr($session, 0, -32);
if ($hash !== md5($session . $this->encryption_key)) {
$this->sess_destroy(); return FALSE; // 竄改偵測
}
}
// 3. 反序列化
$session = $this->_unserialize($session);
if (!is_array($session) || !isset($session['session_id'], $session['ip_address'], $session['user_agent'], $session['last_activity'])) {
$this->sess_destroy(); return FALSE;
}
// 4. 過期檢查
if (($session['last_activity'] + $this->sess_expiration) < $this->now) {
$this->sess_destroy(); return FALSE;
}
// 5. IP 綁定檢查
if ($this->sess_match_ip && $session['ip_address'] !== $this->CI->input->ip_address()) {
$this->sess_destroy(); return FALSE;
}
// 6. UA 綁定檢查
if ($this->sess_match_useragent && trim($session['user_agent']) !== trim(substr($this->CI->input->user_agent(), 0, 120))) {
$this->sess_destroy(); return FALSE;
}
// 7. 資料庫二次驗證 (若啟用)
if ($this->sess_use_database) {
$this->CI->db->where('session_id', $session['session_id']);
if ($this->sess_match_ip) $this->CI->db->where('ip_address', $session['ip_address']);
if ($this->sess_match_useragent) $this->CI->db->where('user_agent', $session['user_agent']);
$query = $this->CI->db->get($this->sess_table_name);
if ($query->num_rows() === 0) { $this->sess_destroy(); return FALSE; }
// 合併 user_data
$row = $query->row();
if ($row->user_data) {
$custom = $this->_unserialize($row->user_data);
if (is_array($custom)) $session = array_merge($session, $custom);
}
}
$this->userdata = $session;
return TRUE;
}
md5(uniqid(...)) 作為 session_iduserdata 陣列 (id, ip, ua, last_activity, user_data)INSERT 寫入資料表_set_cookie()last_activity + sess_time_to_update >= now,未到期直接返回session_id (Rotation 防固定)last_activityUPDATE session_id 與 last_activity_set_cookie() (所有資料在 Cookie)cookie_userdata (核心欄位) 與 custom_userdata (業務資料),後者序列化存 user_data 欄位// 設定:下一次請求可用,之後自動刪除
set_flashdata('msg', '登入成功');
// 讀取:只能讀取一次
$msg = $this->session->flashdata('msg');
// 保留:延長一個請求週期
keep_flashdata('msg');
// 內部實作:
// 設定時鍵名:flash:new:msg
// _flashdata_mark() 將 new 改為 old
// _flashdata_sweep() 刪除 old
// 下次請求 sess_read() 後再次 mark/sweep 完成單次生命週期
_serialize() / _unserialize()\ 替換為 {{slash}} 再 serialize(),避免資料庫/ Cookie 轉義衝突sess_use_database 時)CREATE TABLE ci_sessions (
session_id VARCHAR(40) NOT NULL DEFAULT '0',
ip_address VARCHAR(45) NOT NULL DEFAULT '0',
user_agent VARCHAR(120) NOT NULL,
last_activity INT(10) UNSIGNED NOT NULL DEFAULT 0,
user_data TEXT,
PRIMARY KEY (session_id),
KEY last_activity_idx (last_activity)
);
| 問題 | 原因 | 解決 |
|---|---|---|
| Session 異常中斷 | IP 變動 (行動網路)、UA 變動、Cookie 大小超過 4KB | 關閉 sess_match_ip、縮減資料、改用 DB 儲存 |
| 閃存讀不到 | 同請求多次讀取、重導向未保留 | 使用 keep_flashdata()、檢查重導向是否經過 Session 初始化 |
| 效能瓶頸 | 高流量下 DB 讀寫頻繁、GC 鎖表 | 改用 Redis/Memcached、調大 gc_probability 分母 |
| 加密金鑰遺失 | 更換 encryption_key 導致所有 Session 失效 | 部署時同步金鑰、滾動更新策略 |
// application/libraries/MY_Session.php
class MY_Session extends CI_Session {
protected function _sess_write() {
// 略過父類 DB 邏輯,改寫 Redis
$key = $this->sess_cookie_name . ':' . $this->userdata['session_id'];
$this->CI->redis->setex($key, $this->sess_expiration, serialize($this->userdata));
}
// 同樣重寫 _sess_read, _sess_destroy, _sess_gc
}
理解 CI Session 的 Cookie-based 架構、驗證鏈、閃存週期與序列化細節,能快速排查 Session 異常,並為分散式架構遷移奠定基礎。