本文提供一套完整的 PHP + MySQL 用戶註冊登入範例,涵蓋資料庫建表、註冊驗證 (含圖形驗證碼)、密碼雜湊加密、Session 登入態管理、登出功能,程式碼結構清晰,適合初學者參考與二次開發。
CREATE DATABASE IF NOT EXISTS `user_demo` DEFAULT CHARSET utf8mb4;
USE `user_demo`;
CREATE TABLE `users` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用戶名',
`email` VARCHAR(100) NOT NULL UNIQUE COMMENT '電子郵件',
`password` VARCHAR(255) NOT NULL COMMENT '密碼雜湊',
`salt` VARCHAR(32) NOT NULL COMMENT '加密鹽值',
`reg_ip` VARCHAR(45) DEFAULT NULL COMMENT '註冊 IP',
`reg_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '註冊時間',
`last_login_time` INT UNSIGNED DEFAULT NULL COMMENT '最後登入時間',
`last_login_ip` VARCHAR(45) DEFAULT NULL COMMENT '最後登入 IP',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常 0=禁用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
config.php<?php
define('DB_HOST', 'localhost');
define('DB_NAME', 'user_demo');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_CHARSET', 'utf8mb4');
define('SITE_KEY', 'your_random_secret_key_here'); // 用於密碼加鹽、Token 簽名
session_start();
// 資料庫連線 (PDO)
try {
$pdo = new PDO(
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET,
DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
} catch (PDOException $e) {
die('資料庫連線失敗: ' . $e->getMessage());
}
?>
captcha.php<?php
require 'config.php';
session_start();
$width = 100; $height = 36;
$image = imagecreatetruecolor($width, $height);
$bg = imagecolorallocate($image, 240, 240, 240);
imagefill($image, 0, 0, $bg);
// 干擾線
for ($i=0; $i<5; $i++) {
$color = imagecolorallocate($image, rand(150,200), rand(150,200), rand(150,200));
imageline($image, 0, rand(0,$height), $width, rand(0,$height), $color);
}
// 干擾點
for ($i=0; $i<50; $i++) {
$color = imagecolorallocate($image, rand(100,200), rand(100,200), rand(100,200));
imagesetpixel($image, rand(0,$width), rand(0,$height), $color);
}
// 產生 4 位隨機字元
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$code = '';
for ($i=0; $i<4; $i++) $code .= $chars[rand(0, strlen($chars)-1)];
$_SESSION['captcha'] = strtoupper($code);
// 繪製文字
$font_size = 18;
$colors = [
imagecolorallocate($image, 0, 0, 0),
imagecolorallocate($image, 100, 0, 0),
imagecolorallocate($image, 0, 100, 0),
];
for ($i=0; $i<4; $i++) {
$x = 15 + $i * 20;
$y = rand(24, 28);
$color = $colors[array_rand($colors)];
imagestring($image, 5, $x, $y, $code[$i], $color);
}
header('Content-Type: image/png');
imagepng($image);
imagedestroy($image);
?>
register.php<?php
require 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$captcha = strtoupper(trim($_POST['captcha'] ?? ''));
// 基礎驗證
$errors = [];
if (!$username) $errors[] = '用戶名不可為空';
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = '信箱格式錯誤';
if (strlen($password) < 6) $errors[] = '密碼至少 6 碼';
if ($captcha !== ($_SESSION['captcha'] ?? '')) $errors[] = '驗證碼錯誤';
// 檢查重複
if (empty($errors)) {
$stmt = $pdo->prepare("SELECT id FROM users WHERE username=? OR email=?");
$stmt->execute([$username, $email]);
if ($stmt->fetch()) $errors[] = '用戶名或信箱已存在';
}
// 寫入資料庫
if (empty($errors)) {
$salt = bin2hex(random_bytes(16));
$hash = password_hash($password . $salt, PASSWORD_DEFAULT);
$ip = $_SERVER['REMOTE_ADDR'];
$time = time();
$stmt = $pdo->prepare(
"INSERT INTO users (username, email, password, salt, reg_ip, reg_time) VALUES (?,?,?,?,?,?)"
);
$stmt->execute([$username, $email, $hash, $salt, $ip, $time]);
header('Location: login.php?reg=ok');
exit;
}
// 顯示錯誤 (簡易示範)
echo '- ' . implode('
- ', $errors) . '
';
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>用戶註冊</title></head><body>
<h2>用戶註冊</h2>
<form method="post">
用戶名:<input name="username" required><br>
信箱:<input name="email" type="email" required><br>
密碼:<input name="password" type="password" required><br>
驗證碼:<input name="captcha" required>
<img src="captcha.php" id="captcha-img" style="vertical-align:middle;cursor:pointer" title="點擊換圖">
<script>document.getElementById('captcha-img').onclick=function(){this.src='captcha.php?'+Date.now();}</script>
<br>
<button type="submit">註冊</button>
</form>
<p><a href="login.php">已有帳號?去登入</a></p>
</body></html>
login.php<?php
require 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $pdo->prepare("SELECT id, username, password, salt, status FROM users WHERE username=? OR email=?");
$stmt->execute([$username, $username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && $user['status'] == 1 && password_verify($password . $user['salt'], $user['password'])) {
// 登入成功
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
// 更新最後登入資訊
$stmt = $pdo->prepare("UPDATE users SET last_login_time=?, last_login_ip=? WHERE id=?");
$stmt->execute([time(), $_SERVER['REMOTE_ADDR'], $user['id']]);
header('Location: member.php');
exit;
} else {
$error = '帳號不存在、密碼錯誤或帳號已停用';
}
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>用戶登入</title></head><body>
<h2>用戶登入</h2>
<?php if (!empty($error)) echo ''.$error.'
'; ?>
<form method="post">
帳號:<input name="username" required autofocus><br>
密碼:<input name="password" type="password" required><br>
<button type="submit">登入</button>
</form>
<p><a href="register.php">註冊新帳號</a></p>
</body></html>
member.php / 登出 logout.php<?php
// member.php
require 'config.php';
if (empty($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
echo '會員中心
';
echo '歡迎,' . htmlspecialchars($_SESSION['username']) . ' (ID: ' . $_SESSION['user_id'] . ')';
echo '
登出';
?>
<?php
// logout.php
require 'config.php';
session_destroy();
header('Location: login.php');
?>
| 項目 | 做法 | 風險若不做 |
|---|---|---|
| 密碼加密 | password_hash() + 隨機鹽值 | 資料庫洩露導致明文密碼外洩 |
| SQL 注入防護 | PDO 預處理語句 (prepare/execute) | 任意 SQL 執行、資料竄改/刪除 |
| XSS 防護 | 輸出時 htmlspecialchars() | 惡意腳本竊取 Cookie、劫持會話 |
| CSRF 防護 | 表單加入 Token 驗證 | 偽造請求修改密碼/轉帳等敏感操作 |
| 驗證碼 | 圖形驗證碼 + Session 儲存 | 暴力破解、自動化註冊機器人 |
| Session 安全 | session.cookie_httponly=1, session.use_strict_mode=1 | Session 劫持、固定攻擊 |
vlucas/phpdotenv 管理環境變數,配置與程式碼分離此範例展示了現代 PHP 開發的核心安全實踐,建議在實際專案中進一步模組化、加入單元測試與 CI/CD 流程。