本文實作一個不依賴資料庫 Session 儲存的登入系統,使用雙 Cookie (使用者名稱 + 簽章) 搭配伺服器端密鑰驗證,達成無狀態認證,並支援 Email 變更驗證流程。
user_name (明文) + id_hash (簽章)$hidden_hash_var 僅存在伺服器程式碼中md5(user_name . hidden_hash_var) 驗證 Cookie 完整性<?php /** * 安全登入系統核心庫 * 使用雙 Cookie + MD5 簽章驗證 */ // 唯一密鑰:請替換為強隨機字串,並儲存於環境變數或設定檔 (勿提交版控) $hidden_hash_var = 'your-super-secret-key-change-this-immediately-2024!@#$%'; $LOGGED_IN = null; // 快取驗證結果 /** * 檢查使用者是否已登入 * @return bool */ function user_is_logged_in() { global $user_name, $id_hash, $hidden_hash_var, $LOGGED_IN; // 已驗證過直接返回快取 if (isset($LOGGED_IN)) { return $LOGGED_IN; } // 檢查雙 Cookie 是否存在 if (empty($user_name) || empty($id_hash)) { $LOGGED_IN = false; return false; } // 重新計算簽章並比對 $expectedHash = md5($user_name . $hidden_hash_var); if (hash_equals($expectedHash, $id_hash)) { // 恆定時間比較防時序攻擊 $LOGGED_IN = true; return true; } else { $LOGGED_IN = false; return false; } } /** * 登入成功後設定雙 Cookie * @param string $username * @param int $expire 過期秒數 (0=瀏覽器關閉失效) */ function user_set_tokens($username, $expire = 0) { global $hidden_hash_var; $hash = md5($username . $hidden_hash_var); // 設定 Cookie (HttpOnly 防 XSS 竊取、Secure 僅 HTTPS、SameSite=Strict 防 CSRF) setcookie('user_name', $username, [ 'expires' => $expire ? time() + $expire : 0, 'path' => '/', 'domain' => '', // 當前域名 'secure' => true, // 僅 HTTPS 'httponly' => true, // 防 JavaScript 存取 'samesite' => 'Strict' ]); setcookie('id_hash', $hash, [ 'expires' => $expire ? time() + $expire : 0, 'path' => '/', 'domain' => '', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict' ]); } /** * 登出:清除 Cookie */ function user_logout() { setcookie('user_name', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict']); setcookie('id_hash', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict']); $GLOBALS['LOGGED_IN'] = false; } /** * 產生 Email 變更驗證 Token (同樣機制) * @param string $email * @return string Token */ function generate_email_token($email) { global $hidden_hash_var; return md5($email . $hidden_hash_var); } /** * 驗證 Email Token * @param string $email * @param string $token * @return bool */ function verify_email_token($email, $token) { global $hidden_hash_var; $expected = md5($email . $hidden_hash_var); return hash_equals($expected, $token); }
<?php require_once 'auth.lib.php'; require_once 'db.php'; // PDO 連線 if (user_is_logged_in()) { header('Location: dashboard.php'); exit; } $errors = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; if (!$username || !$password) { $errors[] = '請輸入帳號密碼'; } else { $stmt = $pdo->prepare('SELECT id, username, password_hash FROM users WHERE username = ?'); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user && password_verify($password, $user['password_hash'])) { // 登入成功:設定雙 Cookie (保持 30 天) user_set_tokens($user['username'], 30 * 24 * 3600); header('Location: dashboard.php'); exit; } else { $errors[] = '帳號或密碼錯誤'; } } } ?> <!DOCTYPE html> <html><head><title>登入</title></head><body> <h2>會員登入</h2> <?php if ($errors): ?<ul style="color:red"><?php foreach($errors as $e): ?><li><?=$e?></li><?php endforeach; ?></ul><?php endif; ?> <form method="post"> <p>帳號: <input type="text" name="username" required></p> <p>密碼: <input type="password" name="password" required></p> <button type="submit">登入</button> </form> </body></html>
<?php require_once 'auth.lib.php'; if (!user_is_logged_in()) { header('Location: login.php'); exit; } // 取得當前使用者 (從 Cookie 讀取) $currentUser = $_COOKIE['user_name'] ?? 'Unknown'; ?> <!DOCTYPE html> <html><head><title>儀表板</title></head><body> <h1>歡迎, <?= htmlspecialchars($currentUser) ?></h1> <p><a href="profile.php">個人資料</a> | <a href="logout.php">登出</a></p> </body></html>
<?php require_once 'auth.lib.php'; require_once 'db.php'; if (!user_is_logged_in()) { header('Location: login.php'); exit; } $currentUser = $_COOKIE['user_name']; $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $newEmail = trim($_POST['new_email'] ?? ''); if ($newEmail && filter_var($newEmail, FILTER_VALIDATE_EMAIL)) { // 產生驗證 Token 並寄送郵件 $token = generate_email_token($newEmail); $verifyLink = "https://{$_SERVER['HTTP_HOST']}/verify_email.php?email=" . urlencode($newEmail) . "&token=" . $token; // 寄送郵件 (略) // mail($newEmail, '驗證新 Email', "請點擊連結驗證: $verifyLink"); $message = "驗證信已寄送至 $newEmail,請至信箱點擊連結完成變更。"; } else { $message = '請輸入有效 Email'; } } ?> <!DOCTYPE html> <html><body> <h2>變更 Email</h2> <p><?= $message ?></p> <form method="post"> 新 Email: <input type="email" name="new_email" required> <button type="submit">送出驗證信</button> </form> </body></html>
<?php require_once 'auth.lib.php'; require_once 'db.php'; $email = $_GET['email'] ?? ''; $token = $_GET['token'] ?? ''; if ($email && $token && verify_email_token($email, $token)) { // Token 驗證通過,更新資料庫 // 需確認當前登入用戶身份 (從 Cookie 讀取 user_name) $currentUser = $_COOKIE['user_name'] ?? ''; if ($currentUser) { $stmt = $pdo->prepare('UPDATE users SET email = ? WHERE username = ?'); $stmt->execute([$email, $currentUser]); echo "Email 變更成功!返回儀表板"; } else { echo "請先登入"; } } else { echo "驗證連結無效或已過期"; }
| 面向 | 現狀 | 風險 | 改進建議 |
|---|---|---|---|
| 簽章演算法 | MD5 | 碰撞攻擊理論風險、速度快利於暴力破解 | 改用 HMAC-SHA256: hash_hmac('sha256', $username, $secret) |
| 密鑰管理 | 硬編碼在程式碼 | 版控洩露、多機部署不一致 | 使用環境變數 getenv('AUTH_SECRET') 或 Vault 管理 |
| Cookie 存活期 | 30 天 / Session | 長期有效增加竊取風險 | 短期 Access Token + Refresh Token 機制 (JWT/OAuth2) |
| 防重放攻擊 | 無 | Cookie 被竊取可無限期使用 | 加入時間戳、IP 綁定、一次性 Nonce |
| 擴展性 | 單機共享密鑰 | 多機叢集需同步密鑰 | 共享 Redis Session 或 JWT 無狀態認證 |
新專案強烈建議採用:
本文實作展示「雙 Cookie 簽章」的經典設計思路,理解其原理有助於審閱舊系統與掌握無狀態認證核心概念,但生產環境請採用經過社群驗證的現代方案。