CodeIgniter 提供 Input::set_cookie()、Input::cookie() 封裝原生 setcookie(),支援加密、HttpOnly、Secure、SameSite 等現代 Cookie 屬性。本文實作一套完整的「記住我」Cookie 登入機制,涵蓋登入驗證、Cookie 寫入/讀取/續期、多控制器基類共用、登出銷毀與安全加固。
$config['encryption_key'] = 'your_32_char_random_key_here'; // 必填,用於 Cookie 加密 $config['cookie_prefix'] = 'ci_'; // 前綴避免衝突 $config['cookie_domain'] = ''; // 空 = 當前域名;跨子域可設 '.example.com' $config['cookie_path'] = '/'; $config['cookie_secure'] => TRUE; // 僅 HTTPS 傳輸 (生產環境必開) $config['cookie_httponly'] => TRUE; // 禁止 JS 讀取 (防 XSS 竊取) $config['cookie_samesite'] => 'Lax'; // CSRF 保護:Lax/Strict/None
class Auth extends CI_Controller {
public function login() {
// 1. 顯示登入頁 (GET)
if ($this->input->method() === 'get') {
$this->load->view('auth/login');
return;
}
// 2. 處理表單提交 (POST)
$username = $this->input->post('username', TRUE);
$password = $this->input->post('password');
$remember = (bool) $this->input->post('remember'); // 記住我
if (empty($username) || empty($password)) {
$this->session->set_flashdata('error', '帳號密碼不可為空');
redirect('auth/login');
}
// 3. 驗證用戶 (模型)
$this->load->model('User_model');
$user = $this->User_model->verify($username, $password);
if (!$user) {
$this->session->set_flashdata('error', '帳號或密碼錯誤');
redirect('auth/login');
}
// 4. 寫入 Session (當前請求即時生效)
$sessionData = [
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role,
'login_at' => time(),
];
$this->session->set_userdata($sessionData);
// 5. 寫入 Cookie (記住我)
if ($remember) {
$cookieData = [
'user_id' => $user->id,
'token' => $this->_generate_token($user), // 簽名 token
'expire' => time() + 30 * 86400, // 30 天
];
// 加密儲存單一 Cookie 值 (JSON + encrypt)
$this->input->set_cookie([
'name' => 'remember_me',
'value' => $this->encryption->encrypt(json_encode($cookieData)),
'expire' => 30 * 86400, // 秒
'secure' => TRUE,
'httponly' => TRUE,
'samesite' => 'Lax',
]);
}
redirect('dashboard');
}
public function logout() {
// 清除 Session
$this->session->sess_destroy();
// 刪除 Cookie (設定過期時間為過去)
$this->input->set_cookie('remember_me', '', -3600);
redirect('auth/login');
}
// 產生簽名 Token (防竄改)
private function _generate_token($user) {
$payload = $user->id . '|' . $user->password . '|' . $user->salt; // password 已是 hash
return hash_hmac('sha256', $payload, $this->config->item('encryption_key'));
}
// 驗證 Token
private function _verify_token($user, $token) {
return hash_equals($this->_generate_token($user), $token); // 時序攻擊安全比較
}
}
class MY_Controller extends CI_Controller {
protected $currentUser = NULL;
public function __construct() {
parent::__construct();
$this->_init_auth();
}
private function _init_auth() {
// 1. 優先檢查 Session (已登入狀態)
if ($this->session->userdata('user_id')) {
$this->currentUser = (object) [
'user_id' => $this->session->userdata('user_id'),
'username' => $this->session->userdata('username'),
'role' => $this->session->userdata('role'),
];
return;
}
// 2. 嘗試從 Cookie 自動登入
$cookieVal = $this->input->cookie('remember_me', TRUE); // TRUE = XSS clean + decrypt
if ($cookieVal) {
try {
$data = json_decode($this->encryption->decrypt($cookieVal), TRUE);
if ($data && isset($data['user_id'], $data['token'], $data['expire'])) {
// 檢查過期
if ($data['expire'] > time()) {
$this->load->model('User_model');
$user = $this->User_model->get_by_id($data['user_id']);
if ($user && $this->_verify_token($user, $data['token'])) {
// 登入成功:重建 Session、續期 Cookie
$this->_regenerate_session($user);
$this->_refresh_cookie($user);
return;
}
}
}
} catch (Exception $e) {
log_message('error', 'Cookie 解密失敗: ' . $e->getMessage());
}
}
// 3. 皆失敗:導向登入頁 (可選,視頁面是否需強制登入)
// $this->_require_login();
}
private function _regenerate_session($user) {
$this->session->sess_regenerate(TRUE); // 產生新 Session ID,防止 Session Fixation
$this->session->set_userdata([
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role,
'login_at' => time(),
]);
$this->currentUser = (object) [
'user_id' => $user->id,
'username' => $user->username,
'role' => $user->role,
];
}
private function _refresh_cookie($user) {
$cookieData = [
'user_id' => $user->id,
'token' => $this->_generate_token($user),
'expire' => time() + 30 * 86400,
];
$this->input->set_cookie([
'name' => 'remember_me',
'value' => $this->encryption->encrypt(json_encode($cookieData)),
'expire' => 30 * 86400,
'secure' => TRUE,
'httponly' => TRUE,
'samesite' => 'Lax',
]);
}
// 子類可覆寫決定是否強制登入
protected function _require_login() {
if (!$this->currentUser) {
redirect('auth/login?redirect=' . urlencode(current_url()));
}
}
// 共用的 Token 生成/驗證 (Auth 也可繼承或提取到 Trait)
private function _generate_token($user) { /* 同 Auth::_generate_token */ }
private function _verify_token($user, $token) { /* 同 Auth::_verify_token */ }
}
class Dashboard extends MY_Controller {
public function __construct() {
parent::__construct();
$this->_require_login(); // 強制登入
}
public function index() {
$data['user'] = $this->currentUser;
$this->load->view('dashboard/index', $data);
}
}
class User_model extends CI_Model {
public function verify($username, $password) {
$this->db->where('username', $username);
$query = $this->db->get('users', 1);
$user = $query->row();
if ($user && password_verify($password, $user->password_hash)) {
return $user;
}
return FALSE;
}
public function get_by_id($id) {
return $this->db->get_where('users', ['id' => $id], 1)->row();
}
}
encryption_key 必須 32 字元隨機字串,勿硬編碼,建議放環境變數。user_id|password_hash|salt 的 HMAC,防止用戶偽造/提權。sess_regenerate(TRUE) 防 Session Fixation。$config['cookie_domain'] = '.example.com'; (前導點),確保所有子域 HTTPS。lvh.me (解析到 127.0.0.1) + mkcert 產生信任憑證,或暫時關閉 cookie_secure (僅開發)。encryption_key 長度、PHP openssl 擴展、CI encryption 驅動設定 ($config['encryption_driver'] = 'openssl';)。