採用「檢查腳本 + 受保護頁面」模式:
login_chk.php:驗證 Session 合法性,失敗則阻擋require() 此檔,自動獲得保護login.php 處理認證,成功寫入 Session<?php
// login_chk.php
session_start();
// 檢查是否已登入 (假設登入成功設定 $_SESSION['authenticated'] = true)
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
// 未登入:導向登入頁,記錄原本想去的頁面
$redirect = urlencode($_SERVER['REQUEST_URI'] ?? '/');
header("Location: login.php?redirect=$redirect");
exit;
}
// 可選:檢查權限等級
// if ($_SESSION['role'] !== 'admin') { header('Location: no_permission.php'); exit; }
// 可選:Session 閒置逾時檢查 (30 分鐘)
$timeout = 1800;
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $timeout)) {
session_unset();
session_destroy();
header("Location: login.php?timeout=1");
exit;
}
$_SESSION['last_activity'] = time();
// 可選:防 Session 劫持 (驗證 User-Agent + IP 前段)
$uaHash = md5($_SERVER['HTTP_USER_AGENT'] ?? '');
$ipPrefix = implode('.', array_slice(explode('.', $_SERVER['REMOTE_ADDR'] ?? ''), 0, 2));
if (($_SESSION['ua_hash'] ?? '') !== $uaHash || ($_SESSION['ip_prefix'] ?? '') !== $ipPrefix) {
session_destroy();
header("Location: login.php?hijack=1");
exit;
}
?>
<?php
// login.php
session_start();
require_once 'db.php'; // PDO 連線
$errors = [];
$redirect = $_GET['redirect'] ?? '/dashboard.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if ($username === '' || $password === '') {
$errors[] = '帳號密碼不可為空';
} else {
// 查詢使用者 (預備語句防注入)
$stmt = $pdo->prepare("SELECT id, username, password_hash, role FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// 登入成功
session_regenerate_id(true); // 防 Session Fixation
$_SESSION['authenticated'] = true;
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['last_activity'] = time();
$_SESSION['ua_hash'] = md5($_SERVER['HTTP_USER_AGENT'] ?? '');
$_SESSION['ip_prefix'] = implode('.', array_slice(explode('.', $_SERVER['REMOTE_ADDR'] ?? ''), 0, 2));
// 導向原頁面
header("Location: " . urldecode($redirect));
exit;
} else {
$errors[] = '帳號或密碼錯誤';
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>登入系統</title>
<style>.error { color: red; margin: 10px 0; }</style>
</head>
<body>
<h1>會員登入</h1>
<?php if ($errors): ?>
<div class="error">
<?php foreach ($errors as $e) echo "<p>" . htmlspecialchars($e) . "</p>"; ?>
</div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="redirect" value="<?= htmlspecialchars($redirect) ?>">
<p><label>帳號:<input type="text" name="username" required></label></p>
<p><label>密碼:<input type="password" name="password" required></label></p>
<p><button type="submit">登入</button></p>
</form>
</body>
</html>
<?php
require_once 'login_chk.php'; // 一行即可獲得保護
?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>會員專區</title>
</head>
<body>
<h1>歡迎, <?= htmlspecialchars($_SESSION['username']) ?>!</h1>
<p>您已通過驗證,可以存取此頁面。</p>
<p>角色: <?= htmlspecialchars($_SESSION['role']) ?></p>
<p><a href="logout.php">登出</a></p>
</body>
</html>
<?php
session_start();
// 完全銷毀 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();
header("Location: login.php?logout=1");
exit;?>
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL, -- password_hash() 產生
role ENUM('user', 'admin', 'moderator') DEFAULT 'user',
status ENUM('active', 'inactive', 'banned') DEFAULT 'active',
email_verified_at TIMESTAMP NULL,
last_login_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
// 註冊/修改密碼時
$passwordHash = password_hash($plainPassword, PASSWORD_DEFAULT); // 目前為 bcrypt
// 儲存 $passwordHash 到資料庫
// 驗證時
if (password_verify($plainPassword, $storedHash)) {
// 成功
// 檢查是否需重新雜湊 (演算法更新、cost 變更)
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
$newHash = password_hash($plainPassword, PASSWORD_DEFAULT);
// 更新資料庫
}
}
?>
| 措施 | 實作方式 | 防護目標 |
|---|---|---|
| HTTPS 強制 | Nginx/Apache 導向、HSTS 標頭 | 傳輸加密、防竊聽 |
| 密碼強度要求 | 前後端雙重驗證 (長度、複雜度) | 防暴力破解 |
| 登入失敗限流 | Redis 計數 IP+帳號、封鎖 15 分鐘 | 防暴力破解、撞庫 |
| 雙因子認證 (2FA) | TOTP (Google Authenticator) / SMS | 帳號被盜仍安全 |
| CSRF Token | 表單隱藏欄位、驗證 | 防跨站請求偽造 |
| SameSite Cookie | setcookie(..., ['samesite' => 'Lax']) | 防 CSRF |
| 安全標頭 | CSP、X-Frame-Options、Referrer-Policy | 防 XSS、點擊劫持 |
| 審計日誌 | 記錄登入/登出/失敗/權限變更 | 事後追蹤、合規 |
<?php
// routes/web.php
Route::middleware('guest')->group(function () {
Route::get('/login', [AuthController::class, 'show'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
});
Route::middleware('auth')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
});
// app/Http/Controllers/AuthController.php
public function login(Request $request) {
$credentials = $request->validate([
'username' => 'required|string',
'password' => 'required|string',
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
return back()->withErrors(['username' => '帳號或密碼錯誤']);
}
?>
登入驗證是 Web 應用安全的第一道防線。從手寫
login_chk.php到框架內建Auth,核心原則不變:嚴格驗證身分、安全管理 Session、最小權限存取、完整審計記錄。現代開發強烈建議使用框架成熟的認證系統,避免重複造輪子導致安全漏洞。