首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 用戶註冊驗證最佳實踐:前端 JS 與後端雙重防護完整指南

     編輯:PHP 相關知識

前言

用戶註冊是 Web 應用最關鍵的入口之一。單靠前端驗證不可靠 (可繞過),單靠後端驗證體驗差 (需往返伺服器)。最佳策略是前端即時提示體驗 + 後端嚴格攔截安全雙層防護。

前端驗證:原生 JavaScript (ES6+) 實作

<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 8.1+ 嚴格模式 + 異常處理

<?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;
}

資料表結構 (users)

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;

安全加固清單

  1. CSRF 保護:表單隱藏欄位 <input type="hidden" name="_token" value="">,後端驗證。
  2. 速率限制:同 IP 分鐘內註冊請求不超過 N 次 (Redis 計數器)。
  3. 密碼策略:Argon2id 雜湊、禁止常見弱密碼 (HaveIBeenPwned API)、強制定期更新。
  4. Email 驗證:註冊後發送含簽名 Token 的驗證連結,未驗證前限制權限。
  5. 輸出編碼:模板輸出用戶輸入時務必 htmlspecialchars() 或框架自動逃逸。

總結

前端負責「使用者體驗」,後端負責「資料完整與安全」。兩者驗證邏輯應保持一致,最佳做法是共用驗證規則定義 (如 JSON Schema、PHP 屬性註解、或專用驗證庫如 respect/validation),避免邏輯分歧。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved