用戶註冊是 Web 應用最關鍵的入口之一。單靠前端驗證不可靠 (可繞過),單靠後端驗證體驗差 (需往返伺服器)。最佳策略是前端即時提示體驗 + 後端嚴格攔截安全雙層防護。
<form id="regForm" action="/register" method="post" novalidate>
<div class="field">
<label>用戶名</label>
<input type="text" name="username" required minlength="3" maxlength="20" pattern="^[a-zA-Z0-9_]+$">
<span class="error-msg"></span>
</div>
<div class="field">
<label>Email</label>
<input type="email" name="email" required>
<span class="error-msg"></span>
</div>
<div class="field">
<label>密碼</label>
<input type="password" name="password" id="pwd1" required minlength="8">
<span class="error-msg"></span>
</div>
<div class="field">
<label>確認密碼</label>
<input type="password" name="password_confirm" required data-match="#pwd1">
<span class="error-msg"></span>
</div>
<button type="submit">註冊</button>
</form>
<script>
document.getElementById('regForm').addEventListener('submit', function(e) {
const form = this;
let valid = true;
// 移除舊錯誤
form.querySelectorAll('.error-msg').forEach(el => el.textContent = '');
form.querySelectorAll('.field').forEach(el => el.classList.remove('invalid'));
// 原生 HTML5 驗證
if (!form.checkValidity()) {
form.reportValidity(); // 瀏覽器自動顯示氣泡提示
valid = false;
}
// 自訂:密碼一致性
const pwd1 = form.querySelector('#pwd1');
const pwd2 = form.querySelector('[name="password_confirm"]');
if (pwd1.value !== pwd2.value) {
pwd2.nextElementSibling.textContent = '兩次密碼不一致';
pwd2.parentElement.classList.add('invalid');
valid = false;
}
// 自訂:用戶名唯一性 (AJAX 檢查)
if (valid) {
fetch('/api/check-username?username=' + encodeURIComponent(pwd1.value))
.then(r => r.json())
.then(data => {
if (data.exists) {
pwd1.nextElementSibling.textContent = '用戶名已被使用';
pwd1.parentElement.classList.add('invalid');
valid = false;
}
if (valid) form.submit(); // 通過才真正提交
});
e.preventDefault(); // 暫停提交等待 AJAX
} else {
e.preventDefault();
}
});
</script>
<?php
declare(strict_types=1);
class ValidationException extends Exception {
public array $errors;
public function __construct(array $errors) {
$this->errors = $errors;
parent::__construct('驗證失敗');
}
}
function validateRegistration(array $input): array {
$errors = [];
// 用戶名
$username = trim($input['username'] ?? '');
if ($username === '') {
$errors['username'] = '請輸入用戶名';
} elseif (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
$errors['username'] = '用戶名僅限英數下劃線,3-20 字元';
}
// Email
$email = trim($input['email'] ?? '');
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '請輸入有效的 Email';
}
// 密碼
$pwd = $input['password'] ?? '';
$pwd2 = $input['password_confirm'] ?? '';
if (strlen($pwd) < 8) {
$errors['password'] = '密碼至少 8 字元';
}
if ($pwd !== $pwd2) {
$errors['password_confirm'] = '兩次密碼不一致';
}
// 可加入強度檢查:大小寫、數字、特殊符號
// 真實姓名 (選填但若填需驗證)
$realname = trim($input['realname'] ?? '');
if ($realname !== '' && !preg_match('/^[\p{Han}a-zA-Z\s]{2,30}$/u', $realname)) {
$errors['realname'] = '姓名格式不正確';
}
// 性別 (必選)
$sex = $input['sex'] ?? '';
if (!in_array($sex, ['M', 'F', 'O'], true)) {
$errors['sex'] = '請選擇性別';
}
if ($errors) {
throw new ValidationException($errors);
}
return [
'username' => $username,
'email' => strtolower($email),
'password' => $pwd, // 稍後 password_hash()
'realname' => $realname,
'sex' => $sex,
];
}
// 控制器範例
try {
$clean = validateRegistration($_POST);
// 檢查用戶名/Email 唯一性 (資料庫層面)
$stmt = $pdo->prepare("SELECT 1 FROM users WHERE username = ? OR email = ? LIMIT 1");
$stmt->execute([$clean['username'], $clean['email']]);
if ($stmt->fetch()) {
throw new ValidationException(['username' => '用戶名或 Email 已被註冊']);
}
// 寫入資料庫
$hash = password_hash($clean['password'], PASSWORD_ARGON2ID);
$stmt = $pdo->prepare("INSERT INTO users (username, email, password_hash, realname, sex, created_at) VALUES (?, ?, ?, ?, ?, NOW())");
$stmt->execute([$clean['username'], $clean['email'], $hash, $clean['realname'], $clean['sex']]);
// 註冊成功:發送驗證信、建立 Session、重導向
$_SESSION['user_id'] = $pdo->lastInsertId();
header('Location: /welcome');
exit;
} catch (ValidationException $e) {
// AJAX 請求回傳 JSON
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
header('Content-Type: application/json');
http_response_code(422);
echo json_encode(['errors' => $e->errors]);
exit;
}
// 一般表單提交:存入 Session 回顯
$_SESSION['old_input'] = $_POST;
$_SESSION['errors'] = $e->errors;
header('Location: /register');
exit;
} catch (Throwable $e) {
error_log($e);
http_response_code(500);
echo '系統錯誤,請稍後再試';
exit;
}
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
realname VARCHAR(30) DEFAULT '',
sex ENUM('M','F','O') NOT NULL DEFAULT 'O',
email_verified TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
<input type="hidden" name="_token" value="= csrf_token() ?>">,後端驗證。htmlspecialchars() 或框架自動逃逸。前端負責「使用者體驗」,後端負責「資料完整與安全」。兩者驗證邏輯應保持一致,最佳做法是共用驗證規則定義 (如 JSON Schema、PHP 屬性註解、或專用驗證庫如 respect/validation),避免邏輯分歧。