myAdBanner

2026年5月12日 星期二

php remember me 正規寫法

 

在 PHP 中實現「記住我」(Remember Me)功能,絕對不能直接將帳號或密碼存入 Cookie。目前業界公認最安全的「正規寫法」是採用雙重權限驗證(Selector & Validator)機制。
這種方法將原本的一個憑證拆成兩部分,即便資料庫外洩,攻擊者也無法輕易仿造 Cookie;即便 Cookie 被竊取,也能透過「一次性驗證碼」機制偵測到異常。
1. 資料表準備
你需要在資料庫中準備一張專用的 user_tokens 表,用來儲存持久化的登入狀態。
  • id: 流水號。
  • selector: 長度約 12 字元的隨機字串(作為資料庫查詢索引,唯一值)。
  • validator_hash: 驗證碼的雜湊值(類似密碼雜湊)。
  • user_id: 對應的使用者 ID。
  • expires_at: 到期時間。
2. 登入成功時:產生 Cookie
當使用者勾選「記住我」時,執行以下流程:
  1. 產生隨機數:產生兩個強隨機字串(建議使用 random_bytes())。
    • $selector: 查詢標籤(例如 12 位元)。
    • $validator: 實際驗證金鑰(例如 32 位元)。
  2. 存入資料庫:將 $selector  雜湊後  $validator_hash(使用 password_hash())存入表中。
  3. 發送 Cookie:將 selector:validator 組合成字串存入使用者瀏覽器。
    • 安全性設定:必須開啟 HttpOnly(防 XSS)、Secure(限 HTTPS)與 SameSite=Lax
3. 自動登入時:驗證流程
當使用者帶著 Cookie 重新訪問網站,且 Session 已過期時:
  1. 解析 Cookie:將內容拆回 $selector  $validator
  2. 查詢資料庫:根據 $selector 找出一筆未過期的紀錄。
  3. 比對驗證碼:使用 password_verify($validator, $row['validator_hash']) 檢查是否匹配。
  4. 重新核發(推薦):為了防範 Cookie 側錄(Session Hijacking),驗證成功後應刪除舊紀錄並核發一組全新的 Selector/Validator 給使用者。
為什麼這比存 ID/密碼安全?
  • 資料庫外洩防護:資料庫只存 Validator 的雜湊值。駭客即便拿到資料表,也無法反推回原始 Cookie 內容。
  • 防止計時攻擊:透過 Selector 快速定位,再使用時序安全的函數比對 Validator。
  • 降低風險範圍:這個 Token 應僅限於「自動登入」。當使用者要執行修改密碼、結帳等敏感操作時,應強制要求重新輸入完整密碼。
對於更大型的系統,開發者通常會選用如 Laravel Auth  Symfony Remember Me 等成熟框架,它們內部已內建上述的安全實作。
這是一個基於 Selector & Validator 機制的實作範例。這個寫法能確保即便資料庫被脫庫(Dump),攻擊者也無法偽造 Cookie。
1. 資料庫結構 (MySQL)
sql
CREATE TABLE user_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    selector CHAR(12) NOT NULL,
    validator_hash CHAR(64) NOT NULL,
    user_id INT NOT NULL,
    expires_at DATETIME NOT NULL,
    UNIQUE(selector)
);

2. 登入成功時:產生並存儲 Token
當用戶勾選「記住我」並登入成功後,執行此段程式碼:
php
// 1. 產生隨機 Token
$selector = bin2hex(random_bytes(6)); // 12 字元
$validator = bin2hex(random_bytes(32)); // 64 字元
$token = $selector . ':' . $validator;

// 2. 設定過期時間 (例如 30 天)
$days = 30;
$expiresAt = date('Y-m-d H:i:s', time() + ($days * 86400));

// 3. 存入資料庫 (只存雜湊後的 Validator)
$validatorHash = password_hash($validator, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO user_tokens (selector, validator_hash, user_id, expires_at) VALUES (?, ?, ?, ?)");
$stmt->execute([$selector, $validatorHash, $userId, $expiresAt]);

// 4. 發送 Cookie (安全性設定至關重要)
setcookie('remember_me', $token, [
    'expires' => time() + ($days * 86400),
    'path' => '/',
    'domain' => '', // 填寫你的域名
    'secure' => true,     // 僅限 HTTPS
    'httponly' => true,   // 防止 JS 讀取 (防 XSS)
    'samesite' => 'Lax'   // 防止 CSRF
]);
3. 使用者回訪時:驗證 Token
當 $_SESSION['user_id'] 不存在,但有 $_COOKIE['remember_me'] 時觸發:

if (empty($_SESSION['user_id']) && !empty($_COOKIE['remember_me'])) {
    $parts = explode(':', $_COOKIE['remember_me']);
    
    if (count($parts) === 2) {
        [$selector, $validator] = $parts;

        // 1. 根據 Selector 查找
        $stmt = $pdo->prepare("SELECT * FROM user_tokens WHERE selector = ? AND expires_at > NOW()");
        $stmt->execute([$selector]);
        $tokenRecord = $stmt->fetch();

        // 2. 比對 Validator 雜湊值
        if ($tokenRecord && password_verify($validator, $tokenRecord['validator_hash'])) {
            
            // 驗證成功!登入使用者
            $_SESSION['user_id'] = $tokenRecord['user_id'];

            // 3. 重要:輪替 Token (防止側錄攻擊)
            // 刪除舊的,並重新執行上面的「產生並存儲 Token」步驟發送新 Cookie
            $stmt = $pdo->prepare("DELETE FROM user_tokens WHERE id = ?");
            $stmt->execute([$tokenRecord['id']]);
            
            // (此處呼叫步驟 2 的邏輯產生新 Token...)
        }
    }
}
 

4. 登出時:清除 Token
登出不只是刪除 Session,也要記得清掉資料庫的紀錄。
// 刪除資料庫紀錄
if (!empty($_COOKIE['remember_me'])) {
    $parts = explode(':', $_COOKIE['remember_me']);
    $selector = $parts[0];
    $stmt = $pdo->prepare("DELETE FROM user_tokens WHERE selector = ?");
    $stmt->execute([$selector]);
}

// 刪除 Cookie
setcookie('remember_me', '', time() - 3600, '/');