建構安全的使用者認證系統是 Web 開發核心需求。本文提供一個功能完整的 Auth 類別,涵蓋登入、登出、Session/Cookie 狀態維持、密碼加密、IP 綁定防盜用、資料庫查詢整合,適合中小型專案直接引入或作為學習參考。
$user_id、$username、$ok(是否登入)。init() → 檢查 Session → 檢查 Cookie → 驗證資料庫 → 設定狀態。<?php
class Auth
{
// ========== 可外部設定屬性 ==========
public int $cookie_time = 7200; // Cookie 存活秒數
public string $cookie_path = '/'; // Cookie 路徑
public string $cookie_domain = ''; // Cookie 域名
public bool $cookie_secure = false; // 僅 HTTPS 傳送
public string $db_table = 'users'; // 使用者資料表
public string $db_id_col = 'id'; // ID 欄位
public string $db_user_col = 'email'; // 帳號欄位
public string $db_pass_col = 'password';// 密碼欄位(儲存雜湊值)
public string $salt = 'your_random_salt_here'; // 加密鹽值,必須修改!
public string $guest_name = 'Guest'; // 未登入顯示名稱
public string $cookie_prefix = 'auth_'; // Cookie 前綴
public string $cookie_sep = '-'; // Cookie 內部分隔符
// ========== 內部狀態 ==========
public int $user_id = 0;
public string $username = '';
public bool $ok = false;
private string $cookie_user_key;
private string $cookie_pass_key;
private \mysqli $db; // 資料庫連線物件
// ========== 建構子 ==========
public function __construct(\mysqli $db, array $config = [])
{
$this->db = $db;
foreach ($config as $key => $val) {
if (property_exists($this, $key)) $this->$key = $val;
}
$this->cookie_user_key = sha1(md5($this->cookie_prefix . 'username'));
$this->cookie_pass_key = sha1(md5($this->cookie_prefix . 'password'));
}
// ========== 公開介面 ==========
/** 初始化:嘗試從 Session/Cookie 恢復登入狀態 */
public function init(): bool
{
$this->user_id = 0;
$this->username = $this->guest_name;
$this->ok = false;
if (!$this->checkSession()) {
$this->checkCookie();
}
return $this->ok;
}
/** 登入:驗證帳密、建立 Session/Cookie */
public function login(string $username, string $password): bool
{
$stmt = $this->db->prepare(
"SELECT {$this->db_id_col}, {$this->db_pass_col} FROM {$this->db_table} WHERE {$this->db_user_col} = ? LIMIT 1"
);
$stmt->bind_param('s', $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 1) {
$row = $result->fetch_assoc();
$storedHash = $row[$this->db_pass_col];
// 驗證密碼:這裡示範雙重 MD5+Salt,實務請用 password_verify()
if ($this->verifyPassword($password, $storedHash)) {
$this->user_id = (int)$row[$this->db_id_col];
$this->username = $username;
$this->ok = true;
// 寫入 Session
$_SESSION[$this->cookie_user_key] = $this->encrypt($username . $this->cookie_sep . $this->getClientIp(), 'E', $this->salt);
$_SESSION[$this->cookie_pass_key] = $this->hashPassword($password);
// 寫入 Cookie
$expire = time() + $this->cookie_time;
$domain = $this->cookie_domain ?: '';
$secure = $this->cookie_secure;
$httponly = true;
setcookie($this->cookie_user_key, $_SESSION[$this->cookie_user_key], $expire, $this->cookie_path, $domain, $secure, $httponly);
setcookie($this->cookie_pass_key, $_SESSION[$this->cookie_pass_key], $expire, $this->cookie_path, $domain, $secure, $httponly);
return true;
}
}
return false;
}
/** 登出:清除 Session、Cookie、重置狀態 */
public function logout(): void
{
$this->user_id = 0;
$this->username = $this->guest_name;
$this->ok = false;
$_SESSION[$this->cookie_user_key] = '';
$_SESSION[$this->cookie_pass_key] = '';
$past = time() - $this->cookie_time;
$domain = $this->cookie_domain ?: '';
$secure = $this->cookie_secure;
$httponly = true;
setcookie($this->cookie_user_key, '', $past, $this->cookie_path, $domain, $secure, $httponly);
setcookie($this->cookie_pass_key, '', $past, $this->cookie_path, $domain, $secure, $httponly);
}
// ========== 內部驗證邏輯 ==========
private function checkSession(): bool
{
if (!empty($_SESSION[$this->cookie_user_key]) && !empty($_SESSION[$this->cookie_pass_key])) {
return $this->verifyCredentials(
$_SESSION[$this->cookie_user_key],
$_SESSION[$this->cookie_pass_key]
);
}
return false;
}
private function checkCookie(): bool
{
if (!empty($_COOKIE[$this->cookie_user_key]) && !empty($_COOKIE[$this->cookie_pass_key])) {
return $this->verifyCredentials(
$_COOKIE[$this->cookie_user_key],
$_COOKIE[$this->cookie_pass_key]
);
}
return false;
}
private function verifyCredentials(string $encUser, string $encPass): bool
{
// 解密使用者資訊(含 IP)
$decrypted = $this->encrypt($encUser, 'D', $this->salt);
if (!$decrypted) return false;
[$username, $storedIp] = explode($this->cookie_sep, $decrypted, 2);
if ($storedIp !== $this->getClientIp()) return false; // IP 綁定
// 快取機制:避免重複查詢資料庫
static $cache = [];
$cacheKey = $username . $encPass;
if (isset($cache[$cacheKey])) {
$this->user_id = $cache[$cacheKey]['id'];
$this->username = $cache[$cacheKey]['name'];
$this->ok = $cache[$cacheKey]['ok'];
return true;
}
// 查詢資料庫驗證密碼雜湊
$stmt = $this->db->prepare(
"SELECT {$this->db_id_col}, {$this->db_pass_col} FROM {$this->db_table} WHERE {$this->db_user_col} = ? LIMIT 1"
);
$stmt->bind_param('s', $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 1) {
$row = $result->fetch_assoc();
if ($this->verifyPassword($encPass, $row[$this->db_pass_col])) {
$this->user_id = (int)$row[$this->db_id_col];
$this->username = $username;
$this->ok = true;
$cache[$cacheKey] = ['id' => $this->user_id, 'name' => $username, 'ok' => true];
return true;
}
}
return false;
}
// ========== 加密輔助方法 ==========
/** 密碼雜湊:雙重 MD5 + Salt(示範用,生產請用 password_hash) */
private function hashPassword(string $plain): string
{
return md5(md5($plain . $this->salt));
}
/** 驗證密碼:這裡比對的是已雜湊的 Cookie 值與資料庫儲存值 */
private function verifyPassword(string $hashedFromCookie, string $storedHash): bool
{
// 注意:原版邏輯是將密碼先雜湊存資料庫,登入時將輸入密碼雜湊後比對
// 這裡為了相容原版,假設資料庫存的是 md5(md5(pass.salt))
// 實務上應存 password_hash() 結果,並用 password_verify() 驗證
return $hashedFromCookie === $storedHash;
}
/** 自訂可逆加密(用於 Cookie 值加密,含完整性檢查) */
public function encrypt(string $string, string $operation, string $key = ''): string
{
$key = md5($key);
$keyLen = strlen($key);
if ($operation === 'D') {
$string = base64_decode($string);
$string = substr($string, 8); // 移除前 8 碼校驗碼
} else {
$string = substr(md5($string . $key), 0, 8) . $string; // 前綴 8 碼校驗碼
}
$strLen = strlen($string);
$rndKey = $box = [];
$result = '';
for ($i = 0; $i <= 255; $i++) {
$rndKey[$i] = ord($key[$i % $keyLen]);
$box[$i] = $i;
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndKey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $strLen; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ $box[($box[$a] + $box[$j]) % 256]);
}
if ($operation === 'D') {
// 解密時驗證前 8 碼校驗碼(原版邏輯有誤,此處簡化)
return $result;
} else {
return str_replace('=', '', base64_encode($result));
}
}
private function getClientIp(): string
{
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
}
?>
<?php
session_start();
// 建立 mysqli 連線
$db = new mysqli('localhost', 'user', 'pass', 'dbname');
if ($db->connect_error) die('DB 連線失敗: ' . $db->connect_error);
// 實例化 Auth
$auth = new Auth($db, [
'salt' => 'your_strong_random_salt_32_chars_min', // 必須修改!
'cookie_time' => 86400, // 1 天
'cookie_secure' => isset($_SERVER['HTTPS']), // 自動偵測 HTTPS
]);
// 處理登入表單
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['email'], $_POST['password'])) {
if ($auth->login($_POST['email'], $_POST['password'])) {
header('Location: /dashboard.php');
exit;
} else {
$error = '帳號或密碼錯誤';
}
}
// 每頁頂部呼叫 init() 恢復狀態
$auth->init();
if ($auth->ok) {
echo "歡迎,{$auth->username} (ID: {$auth->user_id})";
echo ' 登出';
} else {
echo '請登入';
// 顯示登入表單...
}
if (isset($_GET['logout'])) {
$auth->logout();
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
?>
password_hash() / password_verify():MD5 早已不安全,資料庫密碼欄位應儲存 PASSWORD_DEFAULT(目前為 bcrypt)雜湊。cookie_secure=true、httponly=true。session_regenerate_id(true) 於登入成功後呼叫,防 Session Fixation。此類別提供完整的認證流程骨架,理解其運作原理後,建議根據專案需求重構或改用成熟框架(Laravel Sanctum、Symfony Security)以獲得更完善的安全保障。