傳統 mysql_* 函數已移除(PHP 7.0),本文提供基於 PDO、密碼雜湊、預處理語句、Session 安全配置、CSRF 防護、記住我功能的現代化登入系統完整實作,可直接作為專案起始模板。
CREATE TABLE `users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(100) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL, -- bcrypt 雜湊
`remember_token` VARCHAR(64) DEFAULT NULL, -- 記住我選擇器
`token_expires` DATETIME DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
declare(strict_types=1);
class AuthService {
private PDO $pdo;
private int $sessionLifetime = 7200; // 2小時
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->configureSession();
}
private function configureSession(): void {
// 僅 HTTPS 傳輸、HttpOnly 防 XSS、SameSite=Lax 防 CSRF
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', '1'); // 防 Session Fixation
ini_set('session.gc_maxlifetime', (string)$this->sessionLifetime);
session_set_cookie_params($this->sessionLifetime, '/', '', true, true);
session_start();
}
/** 註冊 */
public function register(string $username, string $email, string $password): bool {
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = $this->pdo->prepare('INSERT INTO `users` (`username`, `email`, `password`) VALUES (?, ?, ?)');
try {
return $stmt->execute([$username, $email, $hash]);
} catch (PDOException $e) {
if ($e->getCode() === '23000') {
throw new DomainException('帳號或信箱已存在');
}
throw $e;
}
}
/** 登入 */
public function login(string $username, string $password, bool $remember = false): bool {
$stmt = $this->pdo->prepare('SELECT `id`, `password`, `username` FROM `users` WHERE `username` = ? OR `email` = ?');
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password'])) {
return false;
}
// 重新生成 Session ID(防 Session Fixation)
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
// 記住我:產生 selector:validator,資料庫存 selector + hash(validator)
if ($remember) {
$selector = bin2hex(random_bytes(16));
$validator = bin2hex(random_bytes(32));
$hash = hash('sha256', $validator);
$expires = (new DateTime())->modify('+30 days')->format('Y-m-d H:i:s');
$stmt = $this->pdo->prepare('UPDATE `users` SET `remember_token` = ?, `token_expires` = ? WHERE `id` = ?');
$stmt->execute([$selector, $expires, $user['id']]);
// Cookie 存 selector:validator(Base64 編碼)
$cookieValue = base64_encode($selector . ':' . $validator);
setcookie('remember_me', $cookieValue, [
'expires' => time() + 30 * 86400,
'path' => '/',
'httponly' => true,
'secure' => true,
'samesite' => 'Lax',
]);
}
return true;
}
/** 自動登入(每請求中介層呼叫) */
public function autoLogin(): bool {
if (isset($_SESSION['user_id'])) return true; // 已有 Session
$cookie = $_COOKIE['remember_me'] ?? '';
if (!$cookie) return false;
$decoded = base64_decode($cookie, true);
if (!$decoded || !str_contains($decoded, ':')) return false;
[$selector, $validator] = explode(':', $decoded, 2);
$hash = hash('sha256', $validator);
$stmt = $this->pdo->prepare('SELECT `id`, `username` FROM `users` WHERE `remember_token` = ? AND `token_expires` > NOW()');
$stmt->execute([$selector]);
$user = $stmt->fetch();
if (!$user || !hash_equals($hash, $user['remember_token_hash'] ?? '')) {
// Token 無效或過期,清除 Cookie
$this->logout();
return false;
}
// 登入成功,建立新 Session
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
// 滾動更新 Token(選擇性)
return true;
}
/** 登出 */
public function logout(): void {
// 清除資料庫 Token
if (isset($_SESSION['user_id'])) {
$stmt = $this->pdo->prepare('UPDATE `users` SET `remember_token` = NULL, `token_expires` = NULL WHERE `id` = ?');
$stmt->execute([$_SESSION['user_id']]);
}
// 銷毀 Session
$_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();
// 清除記住我 Cookie
setcookie('remember_me', '', time() - 3600, '/', '', true, true);
}
/** 取得當前用戶 */
public function user(): ?array {
if (empty($_SESSION['user_id'])) return null;
$stmt = $this->pdo->prepare('SELECT `id`, `username`, `email` FROM `users` WHERE `id` = ?');
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch() ?: null;
}
/** 檢查是否登入 */
public function check(): bool {
return !empty($_SESSION['logged_in']);
}
}
<?php
// login.php
require 'vendor/autoload.php';
require 'config.php'; // $pdo
$auth = new AuthService($pdo);
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = !empty($_POST['remember']);
if ($auth->login($username, $password, $remember)) {
header('Location: dashboard.php');
exit;
}
$error = '帳號或密碼錯誤';
}
?>
<!DOCTYPE html>
<html>
<head><title>登入</title></head>
<body>
<h1>登入</h1>
<?php if ($error): ?><p style="color:red"><?= htmlspecialchars($error) ?></p><?php endif; ?>
<form method="post">
帳號/信箱: <input type="text" name="username" required><br>
密碼: <input type="password" name="password" required><br>
<label><input type="checkbox" name="remember"> 記住我 30 天</label><br>
<button type="submit">登入</button>
</form>
</body>
</html>
<?php
// middleware/auth.php
require 'config.php';
require 'AuthService.php';
$auth = new AuthService($pdo);
$auth->autoLogin(); // 嘗試恢復 Session 或記住我
// 受保護頁面
if (!$auth->check()) {
header('Location: login.php');
exit;
}
$user = $auth->user(); // 當前用戶資料
| 項目 | 實作 |
|---|---|
| 密碼雜湊 | password_hash(PASSWORD_BCRYPT, cost=12) |
| SQL 注入防護 | PDO 預處理語句 (prepare/execute) |
| Session Fixation | session_regenerate_id(true) 登入後重新生成 ID |
| Session 劫持 | cookie_httponly=1, cookie_secure=1, cookie_samesite=Lax |
| 暴力破解 | 失敗計數 + 延遲/鎖定(Redis 計數器)、驗證碼 |
| 記住我 Token | Selector:Validator 模式,資料庫存 hash(validator) |
| CSRF | 表單隱藏 csrf_token,Session 驗證 hash_equals |
| XSS | 輸出 htmlspecialchars()、CSP 標頭 |
現代 PHP 登入系統核心在於 PDO + bcrypt + Session 安全配置 + Selector:Validator 記住我。本文提供的 AuthService 類別封裝了註冊、登入、自動登入、登出、用戶查詢完整流程,配合中介層模式即可保護全站受限頁面。直接複製、調整資料庫連線、佈署 HTTPS,即可上線。