Cookie 儲存在客戶端且可被竄改,直接儲存用戶識別資訊極易遭偽造登入。本文介紹兩種進階防護策略:對稱加密儲存 與 隨機 Token 綁定驗證,並提供可直接落地的 PHP 實作代碼。
user_id=123 明文,攻擊者只需修改 ID 即可冒充任意用戶核心原理:使用動態金鑰加密,相同明文每次產生不同密文,包含時間戳驗證有效期、完整性校驗碼。
<?php
/**
* Discuz! 經典加密/解密函數
* @param string $string 明文或密文
* @param string $operation 'ENCODE' 加密 | 'DECODE' 解密
* @param string $key 加密金鑰
* @param int $expiry 有效期秒數(0=永久)
* @return string 加密後字串 | 解密後明文 | 空字串(失敗)
*/
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
$ckey_length = 4; // 動態金鑰長度
$key = md5($key ? $key : $GLOBALS['discuz_auth_key']);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE'
? base64_decode(substr($string, $ckey_length))
: sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = [];
for ($i = 0; $i <= 255; $i++) $rndkey[$i] = ord($cryptkey[$i % $key_length]);
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if ($operation == 'DECODE') {
if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) &&
substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
return substr($result, 26);
}
return '';
} else {
return $keyc . str_replace('=', '', base64_encode($result));
}
}
// 使用範例:設定加密 Cookie
$user = array('uid'=>$uid, 'username'=>$username);
$user = base64_encode(serialize($user));
$user = authcode($user, 'ENCODE', 'your_secret_key', 0);
setcookie('user', $user, time() + 3600 * 24);
// 讀取驗證
$cookie = $_COOKIE['user'] ?? '';
$userData = authcode($cookie, 'DECODE', 'your_secret_key', 0);
if ($userData) {
$user = @unserialize(base64_decode($userData));
// $user['uid'], $user['username'] 可用
}
?>
核心原理:Cookie 僅儲存隨機 Token,真實用戶狀態儲存在伺服器端(資料庫/Redis/Memcache),Token 定期輪換。
<?php
// 登入成功時
$uid = 123; // 驗證通過的用戶 ID
$token = md5($uid . time() . mt_rand()); // 生成唯一 Token
$expire = time() + 3600 * 24; // 24 小時有效
// 1. 寫入資料庫 users 表
// UPDATE users SET login_token='$token', token_expire=$expire WHERE id=$uid
// 2. 設定 Cookie(僅儲存 Token)
setcookie('auth_token', $token, $expire, '/', '', false, true); // HttpOnly + Secure
// 3. Session 同步(可選,雙重保障)
$_SESSION['uid'] = $uid;
$_SESSION['token'] = $token;
// 驗證中介層(每次請求執行)
function checkAuth() {
$token = $_COOKIE['auth_token'] ?? '';
if (!$token) return false;
// 從資料庫/快取查詢
$row = query("SELECT uid FROM users WHERE login_token='$token' AND token_expire > " . time());
if (!$row) return false;
// 可選:延長有效期(滑動過期)
// UPDATE users SET token_expire=" . (time() + 3600 * 24) . " WHERE uid={$row['uid']}
return $row['uid'];
}
?>
| 維度 | 對稱加密 (authcode) | Token 綁定驗證 |
|---|---|---|
| 伺服器端狀態 | 無狀態 | 有狀態(需儲存 Token) |
| 防偽造能力 | 強(需金鑰才能構造合法密文) | 極強(Token 無規律、伺服器驗證) |
| 撤銷登入 | 困難(需更換金鑰導致所有用戶失效) | 容易(刪除資料庫 Token 即可) |
| 多裝置管理 | 不支援 | 支援(可儲存多 Token) |
| 效能 | 極高(純運算) | 需查詢儲存層(可用 Redis 優化) |
| 適用場景 | 中小型、無撤銷需求、追求極致效能 | 中大型、需登出控制、多裝置、安全合規 |
setcookie(..., true) 防止 JS 竊取setcookie(..., '', true, true)setcookie(..., ['samesite'=>'Lax']) 防 CSRF根據專案規模與安全等級選擇適合方案,Token 綁定驗證是現代 Web 應用的主流最佳實踐。