Cookie 存儲在客戶端,面臨三大核心風險:
本文提供 對稱加密 + HMAC 簽名 與 服務端令牌驗證 兩套互補方案。
<?php
namespace App\Security;
class CookieCrypt {
/**
* 加密/解密統一入口
* @param string $data 明文/密文
* @param string $operation ENCODE|DECODE
* @param string $key 密鑰
* @param int $ttl 過期時間(秒),0=永不過期
* @return string|false 密文/明文,解密失敗返回 false
*/
public static function authcode(string $data, string $operation = 'DECODE', string $key = '', int $ttl = 0): string|false {
$ckeyLength = 4; // 動態密鑰長度
$key = md5($key ?: config('app.cookie_secret'));
// 密鑰派生
$keya = md5(substr($key, 0, 16)); // 加解密密鑰
$keyb = md5(substr($key, 16, 16)); // 完整性校驗密鑰
$keyc = $ckeyLength
? ($operation === 'DECODE' ? substr($data, 0, $ckeyLength) : substr(md5(microtime()), -$ckeyLength))
: '';
$cryptkey = $keya . md5($keya . $keyc);
$keyLength = strlen($cryptkey);
// 明文預處理
if ($operation === 'DECODE') {
$data = base64_decode(substr($data, $ckeyLength));
} else {
// 明文結構:10位時間戳 + 16位keyb校驗 + 實際數據
$data = sprintf('%010d', $ttl ? $ttl + time() : 0)
. substr(md5($data . $keyb), 0, 16)
. $data;
}
$dataLength = strlen($data);
// RC4 核心:密匙簿初始化與打亂
$box = range(0, 255);
$rndkey = [];
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $keyLength]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
[$box[$i], $box[$j]] = [$box[$j], $box[$i]];
}
// 加解密主循環
$result = '';
for ($a = $j = $i = 0; $i < $dataLength; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
[$box[$a], $box[$j]] = [$box[$j], $box[$a]];
$result .= chr(ord($data[$i]) ^ $box[($box[$a] + $box[$j]) % 256]);
}
if ($operation === 'DECODE') {
// 驗證時間戳與完整性
$timestamp = (int) substr($result, 0, 10);
$checkKeyb = substr($result, 10, 16);
$payload = substr($result, 26);
if (($timestamp === 0 || $timestamp - time() > 0)
&& $checkKeyb === substr(md5($payload . $keyb), 0, 16)) {
return $payload;
}
return false;
} else {
// 密文輸出:動態密鑰 + Base64編碼
return $keyc . str_replace('=', '', base64_encode($result));
}
}
}
?>
<?php
// 設置加密 Cookie
$userData = ['uid' => 12345, 'username' => 'zhangsan', 'role' => 'admin'];
$payload = base64_encode(json_encode($userData));
$encrypted = CookieCrypt::authcode($payload, 'ENCODE', '', 86400); // 1天有效
setcookie('auth_data', $encrypted, [
'expires' => time() + 86400,
'path' => '/',
'httponly' => true,
'secure' => true,
'samesite' => 'Lax'
]);
// 讀取並驗證
if (isset($_COOKIE['auth_data'])) {
$decrypted = CookieCrypt::authcode($_COOKIE['auth_data'], 'DECODE', '');
if ($decrypted !== false) {
$userData = json_decode(base64_decode($decrypted), true);
// 驗證通過,$userData 可信
} else {
// 解密失敗:數據被竄改、過期或密鑰不匹配
setcookie('auth_data', '', time() - 3600, '/');
}
}
?>
graph LR
A[用戶登錄] --> B[生成隨機 Token]
B --> C[存入 Redis: token -> user_id + 過期時間]
C --> D[設置 Cookie: auth_token=token]
D --> E[後續請求]
E --> F[讀取 Cookie Token]
F --> G[Redis 查詢驗證]
G --> H{有效?}
H -->|是| I[信任用戶身份]
H -->|否| J[清除 Cookie + 重定向登錄]
<?php
namespace App\Auth;
use Redis;
class TokenAuth {
private Redis $redis;
private string $prefix = 'auth_token:';
private int $defaultTtl = 2592000; // 30 天
public function __construct(Redis $redis) {
$this->redis = $redis;
}
/**
* 創建認證令牌
*/
public function createToken(int $userId, array $extra = [], int $ttl = 0): string {
$token = bin2hex(random_bytes(32)); // 256-bit 隨機數
$payload = [
'uid' => $userId,
'created' => time(),
'extra' => $extra,
];
$this->redis->setex(
$this->prefix . $token,
$ttl ?: $this->defaultTtl,
json_encode($payload)
);
return $token;
}
/**
* 驗證令牌並返回用戶信息
*/
public function validateToken(string $token): ?array {
$data = $this->redis->get($this->prefix . $token);
if (!$data) return null;
$payload = json_decode($data, true);
if (!$payload) return null;
// 可選:滑動過期(活躍用戶自動續期)
$this->redis->expire($this->prefix . $token, $this->defaultTtl);
return $payload;
}
/**
* 撤銷令牌(登出/修改密碼/安全事件)
*/
public function revokeToken(string $token): bool {
return (bool) $this->redis->del($this->prefix . $token);
}
/**
* 撤銷用戶所有令牌(強制全設備下線)
*/
public function revokeAllUserTokens(int $userId): int {
// 掃描模式:auth_token:* -> 解析 payload -> 匹配 uid
// 生產環境建議維护用戶->Token 索引集合
$pattern = $this->prefix . '*';
$count = 0;
foreach ($this->redis->scan($pattern) as $key) {
$data = $this->redis->get($key);
$payload = json_decode($data, true);
if ($payload && ($payload['uid'] ?? 0) === $userId) {
$this->redis->del($key);
$count++;
}
}
return $count;
}
}
?>
| 層級 | 機制 | 防護目標 | 性能開銷 |
|---|---|---|---|
| 第1層 | Cookie 加密 | 防竄改、防信息洩露、客戶端無狀態驗證 | 極低(純 CPU) |
| 第2層 | 服務端 Token | 防偽造、支持撤銷、審計追蹤、多設備管理 | 低(Redis 讀寫) |
<?php
// 組合使用示例
class HybridAuth {
public function issueCredentials(int $userId): array {
// 1. 服務端創建 Token
$token = $this->tokenAuth->createToken($userId);
// 2. 客戶端加密載荷(包含 token 引用)
$payload = ['token' => $token, 'uid' => $userId];
$encrypted = CookieCrypt::authcode(json_encode($payload), 'ENCODE');
return ['token' => $token, 'cookie' => $encrypted];
}
public function validateRequest(): ?array {
// 1. 解密 Cookie
$decrypted = CookieCrypt::authcode($_COOKIE['auth'] ?? '', 'DECODE');
if (!$decrypted) return null;
$data = json_decode($decrypted, true);
if (!$data || empty($data['token'])) return null;
// 2. 驗證服務端 Token
return $this->tokenAuth->validateToken($data['token']);
}
}
?>
Cookie 加密解決數據機密性與完整性,服務端 Token 解決身份權威性與可撤銷性。兩者結合,構建纵深防禦體系:即使用戶竄改 Cookie 解密出的 Token 也無法通過 Redis 驗證;即使用戶竊取 Token 也無法構造合法加密 Cookie。關鍵在於密鑰管理與Token 生命周期控制。