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

PHP 安全登入系統設計:雙 Cookie 驗證 + MD5 簽章機制完整實作

     編輯:PHP 相關知識

PHP 安全登入系統設計:雙 Cookie 驗證 + MD5 簽章機制完整實作

本文實作一個不依賴資料庫 Session 儲存的登入系統,使用雙 Cookie (使用者名稱 + 簽章) 搭配伺服器端密鑰驗證,達成無狀態認證,並支援 Email 變更驗證流程。

核心設計概念

  • 無資料庫 Session:不在資料庫儲存 Session,減輕 DB 負載
  • 雙 Cookie 機制:user_name (明文) + id_hash (簽章)
  • 伺服器端密鑰:$hidden_hash_var 僅存在伺服器程式碼中
  • 單向簽章:md5(user_name . hidden_hash_var) 驗證 Cookie 完整性
  • 公開金鑰/私鑰類比:密鑰為私鑰,簽章為公開可驗證憑證

核心程式庫 (auth.lib.php)

<?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);
}

登入頁面 (login.php)

<?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>

受保護頁面 (dashboard.php)

<?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>

Email 變更流程 (change_email.php)

<?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>

Email 驗證頁面 (verify_email.php)

<?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 無狀態認證

現代替代方案

新專案強烈建議採用:

  • PHP Session + Redis:傳統但穩健,支援叢集、資料持久化
  • JWT (JSON Web Token):無狀態、跨服務驗證、支援非對稱加密 (RS256)
  • Laravel Sanctum / Passport、Symfony Security:框架級完整認證授權系統
  • OAuth2 / OpenID Connect:第三方登入、單一登入 (SSO) 標準

本文實作展示「雙 Cookie 簽章」的經典設計思路,理解其原理有助於審閱舊系統與掌握無狀態認證核心概念,但生產環境請採用經過社群驗證的現代方案。

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