表單處理是 Web 開發最基礎也最關鍵的環節。本文從 HTTP 協議層面解析 GET/POST 差異,構建防 XSS/CSRF/注入的安全處理流程,並給出現代框架風格的驗證、過濾、響應最佳實踐。
| 維度 | GET | POST |
|---|---|---|
| HTTP 語義 | 安全、冪等、可緩存、可收藏、歷史記錄可見 | 非安全、非冪等、不可緩存、修改服務端狀態 |
| 數據傳遞 | URL Query String (`?k=v&k2=v2`) | Request Body (`application/x-www-form-urlencoded` / `multipart/form-data`) |
| 長度限制 | 瀏覽器/服務器限制 (~2KB~8KB) | 理論無限制 (受 `post_max_size`、`upload_max_filesize`、內存限制) |
| 編碼方式 | URL Encode (ASCII only) | URL Encode / Multipart (支援二進制、大文件) |
| 安全性 | 參數暴露於 URL、日誌、Referer、歷史記錄 | Body 不易洩露,但同樣需防 CSRF/XSS |
| 適用場景 | 搜索、篩選、分頁、排序、幂等查詢 | 登錄、註冊、下單、修改、刪除、上傳、任何狀態變更 |
<!-- form.html -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>用戶註冊</title>
<style>
.error { color: #dc3545; font-size: .875rem; margin-top: .25rem; }
.success { color: #198754; }
label { display: block; margin: 1rem 0 .25rem; font-weight: 500; }
input, textarea { width: 100%; max-width: 400px; padding: .5rem; border: 1px solid #ccc; border-radius: 4px; }
button { margin-top: 1rem; padding: .5rem 1.5rem; background: #0d6efd; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<h1>用戶註冊</h1>
<form action="register.php" method="POST" enctype="multipart/form-data">
<!-- CSRF 保護 -->
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
<label>用戶名<input type="text" name="username" value="<?= htmlspecialchars($old['username'] ?? '') ?>" required></label>
<?php if (!empty($errors['username'])): ?><div class="error"><?= $errors['username'] ?></div><?php endif; ?>
<label>郵箱<input type="email" name="email" value="<?= htmlspecialchars($old['email'] ?? '') ?>" required></label>
<?php if (!empty($errors['email'])): ?><div class="error"><?= $errors['email'] ?></div><?php endif; ?>
<label>密碼<input type="password" name="password" required minlength="8"></label>
<?php if (!empty($errors['password'])): ?><div class="error"><?= $errors['password'] ?></div><?php endif; ?>
<label>頭像<input type="file" name="avatar" accept="image/*"></label>
<?php if (!empty($errors['avatar'])): ?><div class="error"><?= $errors['avatar'] ?></div><?php endif; ?>
<button type="submit">註冊</button>
</form>
</body>
</html>
<?php
// register.php
declare(strict_types=1);
session_start();
require_once __DIR__ . '/vendor/autoload.php'; // Composer 自動加載
use function PHPUnit\Framework\assertNull; // 僅示例,實際用自定義驗證器
// --- 1. CSRF Token 生成與驗證 ---
function generateCsrfToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function validateCsrfToken(string $token): bool {
return isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token); // 時序安全比較
}
// --- 2. 輸入獲取與基礎過濾 ---
$input = [
'username' => trim($_POST['username'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'password' => $_POST['password'] ?? '', // 密碼不 trim,保留前後空格 (若業務允許)
'csrf_token' => $_POST['csrf_token'] ?? '',
];
$files = $_FILES['avatar'] ?? null;
// --- 3. CSRF 驗證 (最先執行) ---
$errors = [];
if (!validateCsrfToken($input['csrf_token'])) {
$errors['_form'] = '請求無效,請重新提交表單。';
}
// --- 4. 業務驗證規則 ---
$validator = new class($input, $files) {
private array $data;
private ?array $file;
private array $errors = [];
public function __construct(array $data, ?array $file) {
$this->data = $data;
$this->file = $file;
}
public function validate(): array {
$this->checkUsername();
$this->checkEmail();
$this->checkPassword();
$this->checkAvatar();
return $this->errors;
}
private function addError(string $field, string $msg): void {
$this->errors[$field] = $msg;
}
private function checkUsername(): void {
$v = $this->data['username'];
if ($v === '') $this->addError('username', '用戶名不能為空');
elseif (mb_strlen($v) < 3) $this->addError('username', '用戶名至少 3 字符');
elseif (mb_strlen($v) > 20) $this->addError('username', '用戶名不超過 20 字符');
elseif (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $v))
$this->addError('username', '僅支持中英文、數字、下劃線');
// TODO: 檢查唯一性 (查數據庫)
}
private function checkEmail(): void {
$v = $this->data['email'];
if ($v === '') $this->addError('email', '郵箱不能為空');
elseif (!filter_var($v, FILTER_VALIDATE_EMAIL)) $this->addError('email', '郵箱格式不正確');
// TODO: 檢查唯一性
}
private function checkPassword(): void {
$v = $this->data['password'];
if ($v === '') $this->addError('password', '密碼不能為空');
elseif (mb_strlen($v) < 8) $this->addError('password', '密碼至少 8 字符');
elseif (!preg_match('/[A-Z]/', $v)) $this->addError('password', '需包含大寫字母');
elseif (!preg_match('/[a-z]/', $v)) $this->addError('password', '需包含小寫字母');
elseif (!preg_match('/\d/', $v)) $this->addError('password', '需包含數字');
elseif (!preg_match('/[\W_]/', $v)) $this->addError('password', '需包含特殊字符');
}
private function checkAvatar(): void {
if (!$this->file || $this->file['error'] === UPLOAD_ERR_NO_FILE) return; // 可選
if ($this->file['error'] !== UPLOAD_ERR_OK) {
$this->addError('avatar', '上傳失敗,錯誤碼: ' . $this->file['error']);
return;
}
// MIME 類型驗證 (不信任瀏覽器提供的 type)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($this->file['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!in_array($mime, $allowed, true)) {
$this->addError('avatar', '僅支持 JPG/PNG/WebP/GIF 格式');
return;
}
// 大小限制 (2MB)
if ($this->file['size'] > 2 * 1024 * 1024) {
$this->addError('avatar', '文件大小不能超過 2MB');
}
// 圖片尺寸驗證 (可選)
$info = getimagesize($this->file['tmp_name']);
if ($info && ($info[0] > 2000 || $info[1] > 2000)) {
$this->addError('avatar', '圖片尺寸不能超過 2000×2000');
}
}
};
$errors = array_merge($errors, $validator->validate());
// --- 5. 處理結果 ---
$csrfToken = generateCsrfToken(); // 重新生成供下次渲染
if (empty($errors)) {
// 成功:業務邏輯 (哈希密碼、存庫、保存頭像、發送歡迎郵件...)
$passwordHash = password_hash($input['password'], PASSWORD_ARGON2ID);
// 模擬保存
$userId = 12345; // DB insert 返回 ID
// 保存頭像
$avatarPath = '';
if ($files && $files['error'] === UPLOAD_ERR_OK) {
$ext = match ($mime) {
'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif',
};
$avatarPath = "/uploads/avatars/{$userId}.{$ext}";
move_uploaded_file($files['tmp_name'], $_SERVER['DOCUMENT_ROOT'] . $avatarPath);
}
// 註冊成功:重定向防止重複提交 (PRG 模式)
$_SESSION['flash_success'] = '註冊成功!歡迎加入。';
header('Location: /login.php');
exit;
}
// --- 6. 失敗:回顯表單 (保留用戶輸入) ---
$old = $input;
unset($old['csrf_token'], $old['password']); // 密碼不回顯
include __DIR__ . '/form.html';
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' https://cdn.example.com; style-src \'self\' \'unsafe-inline\'; img-src \'self\' data: https:; font-src \'self\' https://fonts.gstatic.com; frame-ancestors \'none\'; base-uri \'self\'; form-action \'self\'');
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
<?php
// 使用 Symfony Validator / Laravel Form Request / 自定義 DTO
class RegisterRequest {
// PHP 8.1+ readonly + constructor promotion
public function __construct(
public readonly string $username,
public readonly string $email,
public readonly string $password,
public readonly ?\Symfony\Component\HttpFoundation\File\UploadedFile $avatar = null,
) {}
public static function fromGlobals(): self {
// 這裡可接入 Validator 組件自動驗證並拋出 ValidationException
return new self(
username: trim($_POST['username'] ?? ''),
email: trim($_POST['email'] ?? ''),
password: $_POST['password'] ?? '',
avatar: $_FILES['avatar']['error'] === UPLOAD_ERR_OK
? new \Symfony\Component\HttpFoundation\File\UploadedFile(
$_FILES['avatar']['tmp_name'],
$_FILES['avatar']['name'],
$_FILES['avatar']['type'],
$_FILES['avatar']['size']
) : null,
);
}
}
// 控制器
public function register(RegisterRequest $request): Response {
// $request 已通過驗證,直接使用
$user = $this->userService->register($request);
return $this->redirectToRoute('login')->with('success', '註冊成功');
}
用戶提交 POST
│
▼
服務端處理 (驗證、入庫、發郵件...)
│
├─ 成功 ──► HTTP 302/303 Redirect → GET /success-page
│ (瀏覽器地址欄變為 GET URL)
│ (刷新頁面不再重複提交)
│
└─ 失敗 ──► HTTP 200 直接渲染表單頁 (含錯誤信息、舊輸入)
(或 302 回表單頁 + Session Flash 消息)
核心價值:消除「重複提交」、「瀏覽器刷新警告」、「歷史記錄污染」。
表單處理看似簡單,實則是 **HTTP 語義、安全防線、用戶體驗、代碼架構** 的交匯點。掌握 **GET/POST 語義邊界、CSRF/XSS/SQLi/上傳四大防線、PRG 模式、驗證器抽象**,並遵循「輸入過濾寬鬆、輸出編碼嚴格、參數化查詢、最小權限原則」,即能構建出既安全又優雅的表單處理體系。現代框架已將這些最佳實踐內建為組件,**理解原理才能用好框架,脫離框架也能寫出正確代碼**。