首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 用戶登錄 Cookie 安全防護:加密與令牌雙重保障方案

     編輯:PHP 綜合知識

威脅模型分析

Cookie 存儲在客戶端,面臨三大核心風險:

  1. 竄改:用戶修改 Cookie 內容(如 user_id)冒充他人
  2. 偷取:XSS 攻擊竊取 Cookie,重放攻擊
  3. 偽造:構造合法格式的 Cookie 繞過驗證

本文提供 對稱加密 + HMAC 簽名 與 服務端令牌驗證 兩套互補方案。

方案一:Discuz 風格 Authcode 加密(對稱加密 + 完整性校驗)

核心算法解析

<?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_secret`,舊密鑰保留驗證舊 Cookie
  • ✅ HttpOnly + Secure + SameSite=Lax 三屬性全開
  • ✅ 關鍵操作(修改密碼、綁定手機)要求二次驗證
  • ✅ 異常登錄檢測:新設備/新地點/新 IP 觸發郵件/短信確認
  • ✅ 審計日誌:記錄 Token 創建、使用、撤銷事件
  • ✅ 速率限制:登錄接口、Token 驗證接口防暴力破解

總結

Cookie 加密解決數據機密性與完整性,服務端 Token 解決身份權威性與可撤銷性。兩者結合,構建纵深防禦體系:即使用戶竄改 Cookie 解密出的 Token 也無法通過 Redis 驗證;即使用戶竊取 Token 也無法構造合法加密 Cookie。關鍵在於密鑰管理與Token 生命周期控制。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved