在 Web 開發中,Cookie 常被用來儲存用戶會話、偏好設定或追蹤資訊。然而,預設的 Cookie 以明文傳輸與儲存,極易遭竄改或竊取。本文將示範如何以 PHP 實作一個可重複使用的 Cookie 加密類別,採用 Rijndael-256 (AES) 演算法搭配 ECB 模式,並透過 Base64 編碼確保資料在 HTTP 標頭中安全傳遞。
加密流程分為三個步驟:
Config::get('secret_key'))mcrypt_encrypt 以 Rijndael-256 / ECB 模式加密明文base64_encode,產生可安全存入 Cookie 的字串解密則為逆向操作:Base64 解碼 → mcrypt 解密 → 去除填充字元。
<?php
class Cookie
{
/**
* 解密已加密的 Cookie 值
* @param string $encryptedText Base64 編碼的密文
* @return string 解密後的明文
*/
private static function _decrypt($encryptedText)
{
$key = Config::get('secret_key');
$cryptText = base64_decode($encryptedText);
$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($ivSize, MCRYPT_RAND);
$decryptText = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $cryptText, MCRYPT_MODE_ECB, $iv);
return trim($decryptText);
}
/**
* 加密 Cookie 值
* @param string $plainText 待加密的明文
* @return string Base64 編碼的密文
*/
private static function _encrypt($plainText)
{
$key = Config::get('secret_key');
$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($ivSize, MCRYPT_RAND);
$encryptText = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $plainText, MCRYPT_MODE_ECB, $iv);
return trim(base64_encode($encryptText));
}
/**
* 刪除指定 Cookie
* @param array $args ['name' => string, 'domain' => string?]
* @return bool
*/
public static function del($args)
{
$name = $args['name'];
$domain = isset($args['domain']) ? $args['domain'] : null;
return isset($_COOKIE[$name]) ? setcookie($name, '', time() - 86400, '/', $domain) : true;
}
/**
* 取得並解密 Cookie 值
* @param string $name Cookie 名稱
* @return string|null 解密後的值,不存在則回傳 null
*/
public static function get($name)
{
return isset($_COOKIE[$name]) ? self::_decrypt($_COOKIE[$name]) : null;
}
/**
* 設定加密 Cookie
* @param array $args ['name','value','expire?','path?','domain?','secure?']
* @return bool
*/
public static function set($args)
{
$name = $args['name'];
$value = self::_encrypt($args['value']);
$expire = isset($args['expire']) ? $args['expire'] : null;
$path = isset($args['path']) ? $args['path'] : '/';
$domain = isset($args['domain']) ? $args['domain'] : null;
$secure = isset($args['secure']) ? $args['secure'] : 0;
return setcookie($name, $value, $expire, $path, $domain, $secure);
}
}
?>
<?php
// 寫入加密 Cookie(有效期 1 天)
Cookie::set([
'name' => 'user_token',
'value' => 'user_12345|role_admin',
'expire' => time() + 86400,
'secure' => true, // 僅 HTTPS 傳輸
'path' => '/',
'domain' => '.example.com'
]);
// 讀取並自動解密
$token = Cookie::get('user_token');
if ($token) {
list($uid, $role) = explode('|', $token);
echo "歡迎回來,使用者 $uid ($role)";
}
// 登出時刪除
Cookie::del(['name' => 'user_token', 'domain' => '.example.com']);
?>
secret_key 應為 32 位元組(256-bit)高熵亂數,切勿硬編碼於程式碼,建議存放於環境變數或獨立設定檔(.env)。mcrypt 已移除,現代專案請改用 openssl_encrypt / openssl_decrypt 搭配 AES-256-CBC 或 AES-256-GCM。<?php
class SecureCookie
{
private const CIPHER = 'AES-256-GCM';
private const KEY_LEN = 32; // 256-bit
private const IV_LEN = 12; // GCM 建議 96-bit
private const TAG_LEN = 16;
public static function set(string $name, string $value, int $expire = 0, string $path = '/', ?string $domain = null, bool $secure = true, bool $httponly = true): bool
{
$key = Config::get('secret_key'); // 必須為 32 bytes
$iv = random_bytes(self::IV_LEN);
$ciphertext = openssl_encrypt($value, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);
$payload = base64_encode($iv . $tag . $ciphertext);
return setcookie($name, $payload, $expire, $path, $domain, $secure, $httponly);
}
public static function get(string $name): ?string
{
if (!isset($_COOKIE[$name])) return null;
$key = Config::get('secret_key');
$raw = base64_decode($_COOKIE[$name], true);
if ($raw === false) return null;
$iv = substr($raw, 0, self::IV_LEN);
$tag = substr($raw, self::IV_LEN, self::TAG_LEN);
$ciphertext = substr($raw, self::IV_LEN + self::TAG_LEN);
$plaintext = openssl_decrypt($ciphertext, self::CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag);
return $plaintext !== false ? $plaintext : null;
}
}
?>
採用 GCM 模式可同時提供機密性與完整性,且 openssl 為 PHP 內建擴展,無需額外安裝,是現代專案的推薦選擇。