方案一:Cookie 記住帳號自動登入(HMAC 簽名版)
<?php
// config.php
define('AUTH_SECRET', 'your_32_byte_random_secret_key_here'); // 從環境變數讀取最佳
define('AUTH_COOKIE_NAME', 'auth_token');
define('AUTH_COOKIE_LIFETIME', 30 * 86400); // 30 天
// 產生簽名 Token
function createAuthToken(string $username): string {
$expires = time() + AUTH_COOKIE_LIFETIME;
$data = "$username|$expires";
$hash = hash_hmac('sha256', $data, AUTH_SECRET);
return "$data|$hash";
}
// 驗證 Token
function validateAuthToken(string $token): ?string {
$parts = explode('|', $token, 3);
if (count($parts) !== 3) return null;
[$username, $expires, $hash] = $parts;
// 檢查過期
if ((int)$expires < time()) return null;
// 驗證簽名(防時序攻擊用 hash_equals)
$expected = hash_hmac('sha256', "$username|$expires", AUTH_SECRET);
if (!hash_equals($expected, $hash)) return null;
return $username; // 驗證通過,回傳用戶名
}
// 登入處理
function login(string $username, string $password, bool $remember): bool {
// 實務:查資料庫並 password_verify()
if ($username === 'demo' && $password === 'demo123') {
$_SESSION['user_id'] = 1;
$_SESSION['username'] = $username;
if ($remember) {
$token = createAuthToken($username);
setcookie(AUTH_COOKIE_NAME, $token, [
'expires' => time() + AUTH_COOKIE_LIFETIME,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
}
return true;
}
return false;
}
// 自動登入檢查(每頁頂端 include)
function checkAutoLogin(): void {
if (isset($_SESSION['user_id'])) return; // 已登入
if (empty($_COOKIE[AUTH_COOKIE_NAME])) return;
$username = validateAuthToken($_COOKIE[AUTH_COOKIE_NAME]);
if ($username) {
// 實務:$user = $db->findByUsername($username); 檢查狀態
$_SESSION['user_id'] = 1;
$_SESSION['username'] = $username;
} else {
// Token 無效,清除
setcookie(AUTH_COOKIE_NAME, '', time() - 3600, '/');
}
}
// 登出
function logout(): void {
session_destroy();
setcookie(AUTH_COOKIE_NAME, '', time() - 3600, '/');
}
?>
方案二:MySQL 資料庫帳密驗證登入(PDO + password_hash)
資料表結構
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100),
status TINYINT DEFAULT 1, -- 1=啟用, 0=停用
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
註冊(密碼雜湊)
<?php
// register.php
require 'db.php';
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$email = trim($_POST['email'] ?? '');
if ($username === '' || $password === '') {
die('帳號密碼不可為空');
}
if (strlen($password) < 8) {
die('密碼至少 8 碼');
}
// 檢查帳號是否存在
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
die('帳號已存在');
}
// 雜湊密碼(預設 BCRYPT,cost=10)
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (username, password_hash, email) VALUES (?, ?, ?)');
$stmt->execute([$username, $hash, $email]);
echo '註冊成功,請登入';
?>
登入驗證
<?php
// login.php
session_start();
require 'db.php';
require 'auth.php'; // 上方的 createAuthToken/validateAuthToken
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
$stmt = $pdo->prepare('SELECT id, username, password_hash, status FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user) {
if (!$user['status']) {
$error = '帳號已停用';
} elseif (password_verify($password, $user['password_hash'])) {
// 登入成功
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
// 更新最後登入時間
$pdo->prepare('UPDATE users SET last_login = NOW() WHERE id = ?')->execute([$user['id']]);
if ($remember) {
$token = createAuthToken($user['username']);
setcookie(AUTH_COOKIE_NAME, $token, [
'expires' => time() + AUTH_COOKIE_LIFETIME,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
}
header('Location: dashboard.php');
exit;
} else {
$error = '帳號或密碼錯誤';
}
} else {
// 統一錯誤訊息,防止用戶枚舉
$error = '帳號或密碼錯誤';
}
}
?>
<!DOCTYPE html>
<html><head><title>登入</title></head><body>
<h2>會員登入</h2>
<?php if ($error): ?><p style="color:red"><?= htmlspecialchars($error) ?></p><?php endif; ?>
<form method="post">
<label>帳號:<input type="text" name="username" required autocomplete="username"></label><br><br>
<label>密碼:<input type="password" name="password" required autocomplete="current-password"></label><br><br>
<label><input type="checkbox" name="remember"> 記住我 (30 天)</label><br><br>
<input type="submit" value="登入">
</form>
</body></html>
受保護頁面範例
<?php
// dashboard.php
session_start();
require 'auth.php';
checkAutoLogin(); // 嘗試自動登入
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// 取得用戶資料
require 'db.php';
$stmt = $pdo->prepare('SELECT username, email, created_at FROM users WHERE id = ?');
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
?>
<!DOCTYPE html>
<html><head><title>會員中心</title></head><body>
<h1>歡迎,<?= htmlspecialchars($user['username']) ?></h1>
<p>Email: <?= htmlspecialchars($user['email']) ?></p>
<p>註冊時間: <?= $user['created_at'] ?></p>
<a href="logout.php">登出</a>
</body></html>
登出
<?php
// logout.php
session_start();
require 'auth.php';
logout();
header('Location: login.php');
?>
安全性強化清單
- 密碼雜湊:
password_hash() + password_verify(),定期 password_needs_rehash() 升級演算法。
- HTTPS 全站:Cookie Secure、HSTS、CSP。
- CSRF Token:表單、AJAX 請求雙重驗證。
- 限流:登入失敗計數、IP 封鎖、reCAPTCHA。
- Session 設定:
session.cookie_httponly=1、session.use_strict_mode=1、session.cookie_samesite=Lax。
- 資料庫最小權限:應用帳號僅擁有必要 CRUD 權限。
- 記錄審計日誌:登入成功/失敗、IP、User-Agent、時間。