系統需求與設計
- 用戶註冊、登入、登出
- Session 管理登入狀態
- 密碼安全儲存(bcrypt 雜湊)
- 防止 SQL 注入、XSS、CSRF、Session Fixation
- 記錄最後登入時間
資料庫設計
CREATE DATABASE IF NOT EXISTS `user_auth` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `user_auth`;
CREATE TABLE `users` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(190) NOT NULL UNIQUE,
`password_hash` VARCHAR(255) NOT NULL,
`last_login` TIMESTAMP NULL DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`is_active` TINYINT(1) DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心類別:Auth.php
<?php
// Auth.php
class Auth {
private $pdo;
private const SESSION_KEY = 'auth_user_id';
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 防止 Session Fixation:權限變更時重新生成 ID
// session_regenerate_id(true); // 登入成功時呼叫
}
// 註冊
public function register(string $username, string $email, string $password): array {
$username = trim($username);
$email = trim($email);
$errors = [];
if ($username === '') $errors[] = '請輸入用戶名';
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) $errors[] = '用戶名 3-20 字元,僅限英數底線';
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Email 格式不正確';
if (strlen($password) < 8) $errors[] = '密碼至少 8 字元';
if (!empty($errors)) return ['success' => false, 'errors' => $errors];
// 檢查重複
$stmt = $this->pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
if ($stmt->fetch()) {
return ['success' => false, 'errors' => ['用戶名或 Email 已被註冊']];
}
// 建立用戶
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $this->pdo->prepare("INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)");
$stmt->execute([$username, $email, $hash]);
return ['success' => true, 'user_id' => $this->pdo->lastInsertId()];
}
// 登入
public function login(string $username, string $password): array {
$stmt = $this->pdo->prepare("SELECT id, username, password_hash FROM users WHERE username = ? AND is_active = 1");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
return ['success' => false, 'error' => '帳號或密碼錯誤'];
}
// 更新最後登入時間
$this->pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = ?")->execute([$user['id']]);
// 重新生成 Session ID 防 Fixation
session_regenerate_id(true);
$_SESSION[self::SESSION_KEY] = $user['id'];
$_SESSION['auth_username'] = $user['username'];
return ['success' => true, 'user' => $user];
}
// 登出
public function logout(): void {
$_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();
}
// 取得當前用戶
public function user(): ?array {
if (empty($_SESSION[self::SESSION_KEY])) return null;
$stmt = $this->pdo->prepare("SELECT id, username, email, last_login, created_at FROM users WHERE id = ? AND is_active = 1");
$stmt->execute([$_SESSION[self::SESSION_KEY]]);
return $stmt->fetch() ?: null;
}
// 檢查是否登入
public function check(): bool {
return $this->user() !== null;
}
// 要求登入(中介層用法)
public function requireLogin(): void {
if (!$this->check()) {
header('Location: login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
}
}
?>
使用範例
register.php
<?php
require_once 'config.php';
require_once 'Auth.php';
$auth = new Auth($pdo);
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $auth->register($_POST['username'] ?? '', $_POST['email'] ?? '', $_POST['password'] ?? '');
if ($result['success']) {
header('Location: login.php?registered=1');
exit;
}
$errors = $result['errors'];
}
?>
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>註冊</title></head><body>
<h2>用戶註冊</h2>
<?php if ($errors): ?><ul style="color:red"><?php foreach($errors as $e) echo "<li>".htmlspecialchars($e)."</li>"; ?></ul><?php endif; ?>
<form method="POST">
<input type="text" name="username" placeholder="用戶名" required><br>
<input type="email" name="email" placeholder="Email" required><br>
<input type="password" name="password" placeholder="密碼 (最少 8 碼)" required><br>
<button type="submit">註冊</button>
</form>
<p><a href="login.php">已有帳號?去登入</a></p>
</body></html>
login.php
<?php
require_once 'config.php';
require_once 'Auth.php';
$auth = new Auth($pdo);
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $auth->login($_POST['username'] ?? '', $_POST['password'] ?? '');
if ($result['success']) {
$redirect = $_GET['redirect'] ?? 'dashboard.php';
header('Location: ' . $redirect);
exit;
}
$error = $result['error'];
}
?>
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>登入</title></head><body>
<h2>用戶登入</h2>
<?php if ($error): ?><p style="color:red"><?= htmlspecialchars($error) ?></p><?php endif; ?>
<form method="POST">
<input type="text" name="username" placeholder="用戶名" required><br>
<input type="password" name="password" placeholder="密碼" required><br>
<button type="submit">登入</button>
</form>
<p><a href="register.php">還沒帳號?立即註冊</a></p>
</body></html>
dashboard.php (受保護頁面)
<?php
require_once 'config.php';
require_once 'Auth.php';
$auth = new Auth($pdo);
$auth->requireLogin(); // 未登入自動導向登入頁
$user = $auth->user();
?>
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>會員中心</title></head><body>
<h2>歡迎,<?= htmlspecialchars($user['username']) ?></h2>
<p>Email: <?= htmlspecialchars($user['email']) ?></p>
<p>最後登入: <?= $user['last_login'] ? htmlspecialchars($user['last_login']) : '首次登入' ?></p>
<p><a href="logout.php">登出</a></p>
</body></html>
logout.php
<?php
require_once 'config.php';
require_once 'Auth.php';
$auth = new Auth($pdo);
$auth->logout();
header('Location: login.php?logged_out=1');
exit;
安全檢查清單
- ✅ 密碼使用
password_hash() (bcrypt) 儲存
- ✅ PDO 預處理語句防 SQL 注入
- ✅
htmlspecialchars() 輸出轉義防 XSS
- ✅
session_regenerate_id(true) 登入時重新生成 Session ID 防 Session Fixation
- ✅ Session Cookie 設定
HttpOnly、Secure (HTTPS)、SameSite=Strict/Lax
- ✅ 登出銷毀 Session 並清除 Cookie
- ✅ CSRF Token 保護狀態變更操作(登入/登出/註冊建議加上)