展示表單登入、密碼雜湊驗證、Session+Cookie 雙重標記、記住我功能的完整流程。
<?php
session_start();
$host = 'localhost';
$user = 'root';
$pass = 'password';
$db = 'auth_demo';
$mysqli = new mysqli($host, $user, $pass, $db);
if ($mysqli->connect_errno) die('DB連線失敗');
$mysqli->set_charset('utf8mb4');
$message = '';
// 處理登入
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
if ($username && $password) {
// 預備語句查詢用戶
$stmt = $mysqli->prepare('SELECT id, username, password_hash FROM users WHERE username = ?');
$stmt->bind_param('s', $username);
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
$stmt->close();
if ($user && password_verify($password, $user['password_hash'])) {
// 登入成功
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['authenticated'] = true;
// 記住我:設定長效 Cookie
if ($remember) {
$selector = bin2hex(random_bytes(16));
$validator = bin2hex(random_bytes(32));
$hash = hash('sha256', $validator);
$expires = time() + 30 * 86400; // 30天
$stmt = $mysqli->prepare('INSERT INTO remember_tokens (user_id, selector, validator_hash, expires) VALUES (?, ?, ?, FROM_UNIXTIME(?))');
$stmt->bind_param('issi', $user['id'], $selector, $hash, $expires);
$stmt->execute();
$stmt->close();
setcookie('remember', $selector . ':' . $validator, $expires, '/', '', true, true);
}
header('Location: dashboard.php');
exit;
} else {
$message = '帳號或密碼錯誤';
}
} else {
$message = '請輸入帳號密碼';
}
}
// 自動登入檢查(每次頁面載入)
if (!isset($_SESSION['authenticated']) && isset($_COOKIE['remember'])) {
list($selector, $validator) = explode(':', $_COOKIE['remember'], 2);
$hash = hash('sha256', $validator);
$stmt = $mysqli->prepare('SELECT user_id FROM remember_tokens WHERE selector = ? AND validator_hash = ? AND expires > NOW()');
$stmt->bind_param('ss', $selector, $hash);
$stmt->execute();
$token = $stmt->get_result()->fetch_assoc();
$stmt->close();
if ($token) {
// 取得用戶資料
$stmt = $mysqli->prepare('SELECT id, username FROM users WHERE id = ?');
$stmt->bind_param('i', $token['user_id']);
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
$stmt->close();
if ($user) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['authenticated'] = true;
// 更新 token 到期時間(滑動過期)
$newExpires = time() + 30 * 86400;
$stmt = $mysqli->prepare('UPDATE remember_tokens SET expires = FROM_UNIXTIME(?) WHERE selector = ?');
$stmt->bind_param('is', $newExpires, $selector);
$stmt->execute();
$stmt->close();
}
}
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>登入</title></head>
<body>
<h1>會員登入</h1>
<?php if ($message): ?><div style="color:red"><?= $message ?></div><?php endif; ?>
<form method="post">
<p>帳號:<input type="text" name="username" value="<?= htmlspecialchars($_COOKIE['remember'] ? '' : ($_POST['username'] ?? '')) ?>" required></p>
<p>密碼:<input type="password" name="password" required></p>
<p><label><input type="checkbox" name="remember"> 記住我 (30天)</label></p>
<button type="submit">登入</button>
</form>
</body></html>
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE remember_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
selector VARCHAR(32) NOT NULL,
validator_hash VARCHAR(64) NOT NULL,
expires DATETIME NOT NULL,
INDEX idx_selector (selector),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
| 措施 | 說明 |
|---|---|
| 密碼雜湊 | password_hash() + password_verify(),預設 bcrypt,成本可調 |
| 預備語句 | 所有查詢參數化,防 SQL 注入 |
| 記住我機制 | Selector:Validator 雙重 Token,Validator 僅存雜湊,Selector 索引查詢 |
| Cookie 安全屬性 | HttpOnly、Secure、SameSite=Lax、path=/ |
| Session 固定保護 | 登入後 session_regenerate_id(true) |
| Token 滑動過期 | 每次自動登入延長有效期,閒置 30 天失效 |
| 登出清理 | 刪除 DB token、清除 Cookie、銷毀 Session |
<?php
session_start();
if (isset($_COOKIE['remember'])) {
list($selector) = explode(':', $_COOKIE['remember'], 2);
// 刪除 DB token
$mysqli->prepare('DELETE FROM remember_tokens WHERE selector = ?')->bind_param('s', $selector)->execute();
// 刪除 Cookie
setcookie('remember', '', time() - 3600, '/', '', true, true);
}
$_SESSION = [];
session_destroy();
header('Location: login.php');