myAdBanner

2026年5月12日 星期二

通用資料庫類別 Database.php

 我們將 Database 類別擴充,加入一個通用的 run() 方法來處理所有 SQL 執行。

<?php
class Database {
    private $host = "localhost";
    private $db_name = "your_db";
    private $username = "root";
    private $password = "";
    private $conn;

    public function getConnection() {
        if ($this->conn === null) {
            try {
                $this->conn = new PDO(
                    "mysql:host=$this->host;dbname=$this->db_name;charset=utf8mb4",
                    $this->username,
                    $this->password,
                    [
                        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
                    ]
                );
            } catch(PDOException $e) {
                die("連線失敗: " . $e->getMessage());
            }
        }
        return $this->conn;
    }

    // 通用的執行方法
    public function run($sql, $args = []) {
        $stmt = $this->getConnection()->prepare($sql);
        $stmt->execute($args);
        return $stmt;
    }
}

你可以直接使用 run() 方法來處理各種情況:

A. 讀取 (Read)
php
$db = new Database();

// 讀取多筆
$users = $db->run("SELECT * FROM users WHERE status = ?", [1])->fetchAll();
foreach ($users as $user) {
    echo $user['name'];
}

// 讀取單筆
$user = $db->run("SELECT * FROM users WHERE id = ?", [1])->fetch();
B. 新增 (Create)
php
$sql = "INSERT INTO users (name, email) VALUES (?, ?)";
$db->run($sql, ['張小明', 'ming@example.com']);
echo "新增成功,ID 為:" . $db->getConnection()->lastInsertId();
C. 更新 (Update)
php
$sql = "UPDATE users SET name = ? WHERE id = ?";
$db->run($sql, ['新名字', 1]);
D. 刪除 (Delete)
php
$sql = "DELETE FROM users WHERE id = ?";
$db->run($sql, [1]);

為什麼這樣寫比較好?
防止 SQL 注入:透過 ? 佔位符與 run() 的第二個參數(陣列)傳值,PDO 會自動過濾惡意代碼。
程式碼簡潔:你不需要每次都寫 prepare、bind、execute,一行 run() 搞定。
單例模式 (Singleton) 概念:getConnection() 內做了判斷,確保同一個請求中只會開啟一個資料庫連線,節省效能。


logout.php

 <?php
// 1. 初始化 Session
if (!isset($_SESSION)) {
    session_start();
}

// 2. 引入你的資料庫連線檔 (假設變數名稱為 $pdo)
// require_once('Connections/your_db_connection.php'); 

if ((isset($_GET['doLogout'])) && ($_GET['doLogout'] == "true")) {
    
    // --- [修正重點 A] 刪除資料庫中的 Remember Me Token ---
    if (!empty($_COOKIE['remember_me'])) {
        $parts = explode(':', $_COOKIE['remember_me']);
        if (count($parts) === 2) {
            $selector = $parts[0];
            // 執行刪除 (請根據你的資料庫結構調整)
            try {
                $stmt = $pdo->prepare("DELETE FROM user_tokens WHERE selector = ?");
                $stmt->execute([$selector]);
            } catch (Exception $e) {
                // 靜默失敗,不影響登出流程
            }
        }
    }

    // --- [修正重點 B] 清除 Session 變數 ---
    $_SESSION = array(); // 直接將 Session 陣列清空是最徹底的做法

    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }
    session_destroy(); // 銷毀 Session

    // --- [修正重點 C] 清除所有相關 Cookie ---
    $past = time() - 3600;
    
    // 清除舊有的欄位 (原本你存放在 Cookie 的敏感資料)
    setcookie('MM_Username', '', $past, '/');
    setcookie('MM_UserCname', '', $past, '/');
    setcookie('MM_UserGroup', '', $past, '/');
    setcookie('MM_UserGroupName', '', $past, '/');
    
    // 清除新的 Remember Me 安全 Token
    setcookie('remember_me', '', $past, '/', '', true, true);

    // 3. 導向首頁
    $logoutGoTo = "index.php";
    header("Location: $logoutGoTo");
    exit;
}
?>

主要修正說明:
  1. 資料庫連動刪除:安全機制的重點在於「Token 一次性」。當使用者主動點擊登出時,必須從資料庫 user_tokens 表中將該 selector 刪除,讓原本的 Cookie 徹底失效,防止他人盜用。
  2. 徹底銷毀 Session:原程式逐個 unset 較為繁瑣,使用 $_SESSION = array() 配合 session_destroy() 是目前更標準的寫法。
  3. 安全性 Cookie 設定
    • 在刪除 remember_me Cookie 時,建議帶上與當初發送時相同的參數(如 / 路徑)。
    • 提醒:既然你已經要實作安全的 remember_me 流程,建議不要再把 MM_Username(帳號)或 MM_UserGroup(權限)存進 Cookie 了,因為 Cookie 是可以被使用者隨意修改的,這會造成安全漏洞。



php remember me 正規寫法

 

在 PHP 中實現「記住我」(Remember Me)功能,絕對不能直接將帳號或密碼存入 Cookie。目前業界公認最安全的「正規寫法」是採用雙重權限驗證(Selector & Validator)機制。
這種方法將原本的一個憑證拆成兩部分,即便資料庫外洩,攻擊者也無法輕易仿造 Cookie;即便 Cookie 被竊取,也能透過「一次性驗證碼」機制偵測到異常。
1. 資料表準備
你需要在資料庫中準備一張專用的 user_tokens 表,用來儲存持久化的登入狀態。
  • id: 流水號。
  • selector: 長度約 12 字元的隨機字串(作為資料庫查詢索引,唯一值)。
  • validator_hash: 驗證碼的雜湊值(類似密碼雜湊)。
  • user_id: 對應的使用者 ID。
  • expires_at: 到期時間。
2. 登入成功時:產生 Cookie
當使用者勾選「記住我」時,執行以下流程:
  1. 產生隨機數:產生兩個強隨機字串(建議使用 random_bytes())。
    • $selector: 查詢標籤(例如 12 位元)。
    • $validator: 實際驗證金鑰(例如 32 位元)。
  2. 存入資料庫:將 $selector  雜湊後  $validator_hash(使用 password_hash())存入表中。
  3. 發送 Cookie:將 selector:validator 組合成字串存入使用者瀏覽器。
    • 安全性設定:必須開啟 HttpOnly(防 XSS)、Secure(限 HTTPS)與 SameSite=Lax
3. 自動登入時:驗證流程
當使用者帶著 Cookie 重新訪問網站,且 Session 已過期時:
  1. 解析 Cookie:將內容拆回 $selector  $validator
  2. 查詢資料庫:根據 $selector 找出一筆未過期的紀錄。
  3. 比對驗證碼:使用 password_verify($validator, $row['validator_hash']) 檢查是否匹配。
  4. 重新核發(推薦):為了防範 Cookie 側錄(Session Hijacking),驗證成功後應刪除舊紀錄並核發一組全新的 Selector/Validator 給使用者。
為什麼這比存 ID/密碼安全?
  • 資料庫外洩防護:資料庫只存 Validator 的雜湊值。駭客即便拿到資料表,也無法反推回原始 Cookie 內容。
  • 防止計時攻擊:透過 Selector 快速定位,再使用時序安全的函數比對 Validator。
  • 降低風險範圍:這個 Token 應僅限於「自動登入」。當使用者要執行修改密碼、結帳等敏感操作時,應強制要求重新輸入完整密碼。
對於更大型的系統,開發者通常會選用如 Laravel Auth  Symfony Remember Me 等成熟框架,它們內部已內建上述的安全實作。
這是一個基於 Selector & Validator 機制的實作範例。這個寫法能確保即便資料庫被脫庫(Dump),攻擊者也無法偽造 Cookie。
1. 資料庫結構 (MySQL)
sql
CREATE TABLE user_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    selector CHAR(12) NOT NULL,
    validator_hash CHAR(64) NOT NULL,
    user_id INT NOT NULL,
    expires_at DATETIME NOT NULL,
    UNIQUE(selector)
);

2. 登入成功時:產生並存儲 Token
當用戶勾選「記住我」並登入成功後,執行此段程式碼:
php
// 1. 產生隨機 Token
$selector = bin2hex(random_bytes(6)); // 12 字元
$validator = bin2hex(random_bytes(32)); // 64 字元
$token = $selector . ':' . $validator;

// 2. 設定過期時間 (例如 30 天)
$days = 30;
$expiresAt = date('Y-m-d H:i:s', time() + ($days * 86400));

// 3. 存入資料庫 (只存雜湊後的 Validator)
$validatorHash = password_hash($validator, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO user_tokens (selector, validator_hash, user_id, expires_at) VALUES (?, ?, ?, ?)");
$stmt->execute([$selector, $validatorHash, $userId, $expiresAt]);

// 4. 發送 Cookie (安全性設定至關重要)
setcookie('remember_me', $token, [
    'expires' => time() + ($days * 86400),
    'path' => '/',
    'domain' => '', // 填寫你的域名
    'secure' => true,     // 僅限 HTTPS
    'httponly' => true,   // 防止 JS 讀取 (防 XSS)
    'samesite' => 'Lax'   // 防止 CSRF
]);
3. 使用者回訪時:驗證 Token
當 $_SESSION['user_id'] 不存在,但有 $_COOKIE['remember_me'] 時觸發:

if (empty($_SESSION['user_id']) && !empty($_COOKIE['remember_me'])) {
    $parts = explode(':', $_COOKIE['remember_me']);
    
    if (count($parts) === 2) {
        [$selector, $validator] = $parts;

        // 1. 根據 Selector 查找
        $stmt = $pdo->prepare("SELECT * FROM user_tokens WHERE selector = ? AND expires_at > NOW()");
        $stmt->execute([$selector]);
        $tokenRecord = $stmt->fetch();

        // 2. 比對 Validator 雜湊值
        if ($tokenRecord && password_verify($validator, $tokenRecord['validator_hash'])) {
            
            // 驗證成功!登入使用者
            $_SESSION['user_id'] = $tokenRecord['user_id'];

            // 3. 重要:輪替 Token (防止側錄攻擊)
            // 刪除舊的,並重新執行上面的「產生並存儲 Token」步驟發送新 Cookie
            $stmt = $pdo->prepare("DELETE FROM user_tokens WHERE id = ?");
            $stmt->execute([$tokenRecord['id']]);
            
            // (此處呼叫步驟 2 的邏輯產生新 Token...)
        }
    }
}
 

4. 登出時:清除 Token
登出不只是刪除 Session,也要記得清掉資料庫的紀錄。
// 刪除資料庫紀錄
if (!empty($_COOKIE['remember_me'])) {
    $parts = explode(':', $_COOKIE['remember_me']);
    $selector = $parts[0];
    $stmt = $pdo->prepare("DELETE FROM user_tokens WHERE selector = ?");
    $stmt->execute([$selector]);
}

// 刪除 Cookie
setcookie('remember_me', '', time() - 3600, '/');