<?php
session_start(); // 必須在任何輸出前呼叫
// 設定:僅 HTTPS 傳輸 Cookie,防 XSS
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.use_strict_mode', '1');
// 資料庫連線 (PDO)
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$errors = [];
$redirectUrl = '/dashboard.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. CSRF 保護
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '')) {
$errors[] = '無效的請求,請重新整理頁面';
}
// 2. 輸入驗證
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
if ($username === '') $errors[] = '請輸入帳號';
if ($password === '') $errors[] = '請輸入密碼';
if (!$errors) {
// 3. 查詢使用者
$stmt = $pdo->prepare('SELECT id, username, password_hash, status, failed_login_count, locked_until FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user) {
$errors[] = '帳號或密碼錯誤'; // 統一訊息防止用戶枚舉
} elseif ($user['status'] !== 1) {
$errors[] = '帳號已停用,請聯繫管理員';
} elseif ($user['locked_until'] && $user['locked_until'] > time()) {
$errors[] = '帳號暫時鎖定,請稍後再試';
} elseif (!password_verify($password, $user['password_hash'])) {
// 4. 密碼錯誤:記錄失敗次數、鎖定機制
$pdo->prepare('UPDATE users SET failed_login_count = failed_login_count + 1, locked_until = CASE WHEN failed_login_count >= 4 THEN ? ELSE NULL END WHERE id = ?')
->execute([time() + 900, $user['id']]); // 鎖定 15 分鐘
$errors[] = '帳號或密碼錯誤';
} else {
// 5. 登入成功
$pdo->prepare('UPDATE users SET failed_login_count = 0, locked_until = NULL, last_login_at = NOW() WHERE id = ?')
->execute([$user['id']]);
// 重新產生 Session ID 防 Session Fixation
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['login_time'] = time();
// 6. 記住我:長效 Token (不直接存密碼)
if ($remember) {
$selector = bin2hex(random_bytes(16));
$validator = bin2hex(random_bytes(32));
$hashedValidator = hash('sha256', $validator);
$expires = time() + 86400 * 30; // 30 天
$pdo->prepare('INSERT INTO remember_tokens (selector, validator_hash, user_id, expires) VALUES (?, ?, ?, ?)')
->execute([$selector, $hashedValidator, $user['id'], $expires]);
setcookie('remember_token', $selector . ':' . $validator, [
'expires' => $expires,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
}
header('Location: ' . $redirectUrl);
exit;
}
}
}
// 產生 CSRF Token
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
?>
登入
$e"; ?>
| 機制 | 實作方式 | 防禦目標 |
|---|---|---|
| 密碼雜湊 | password_hash() + PASSWORD_ARGON2ID | 資料庫洩露時密碼不可逆 |
| 參數化查詢 | PDO Prepared Statements | SQL Injection |
| CSRF Token | Session + 表單隱藏欄位 + hash_equals() | 跨站請求偽造 |
| Session 安全 | session_regenerate_id(true)、Secure/HttpOnly/SameSite | Session Fixation、XSS 竊取 |
| 暴力破解防護 | 失敗計數 + 暫時鎖定 (15分鐘) | 密碼噴灑、暴力破解 |
| 用戶枚舉防護 | 統一錯誤訊息「帳號或密碼錯誤」 | 帳號存在性探測 |
| 記住我 Token | Selector:Validator 分離、Validator 雜湊儲存、HttpOnly Cookie | 長期 Token 洩露風險 |
selector:validator 存於 Cookie。hash_equals(hash('sha256', validator), db_hash)。<?php
session_start();
// 刪除記住我 Token
if (isset($_COOKIE['remember_token'])) {
[$selector] = explode(':', $_COOKIE['remember_token'], 2);
$pdo->prepare('DELETE FROM remember_tokens WHERE selector = ?')->execute([$selector]);
setcookie('remember_token', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true]);
}
// 銷毀 Session
$_SESSION = [];
session_destroy();
header('Location: /login.php');
exit;
?>
這套登入系統涵蓋現代 Web 應用的核心安全需求,可作為專案起始模板直接擴充。