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

PHP 表單處理入門:GET/POST 請求、安全防護與最佳實踐完整指南

     編輯:PHP 綜合知識

前言

表單處理是 Web 開發最基礎也最關鍵的環節。本文從 HTTP 協議層面解析 GET/POST 差異,構建防 XSS/CSRF/注入的安全處理流程,並給出現代框架風格的驗證、過濾、響應最佳實踐。

一、GET vs POST:協議語義與工程選型

維度GETPOST
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
適用場景搜索、篩選、分頁、排序、幂等查詢登錄、註冊、下單、修改、刪除、上傳、任何狀態變更

二、基礎實現:原生 PHP 處理流程

1. HTML 表單模板 (含 CSRF Token)

<!-- 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>

2. PHP 後端處理 (register.php)

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

三、安全防護核心清單

  1. CSRF 保護:每個會話生成 Token,表單隱藏域攜帶,提交時 `hash_equals` 驗證。現代框架 (Laravel/Symfony) 內建中間件自動處理。
  2. XSS 防護:輸出時 根據上下文編碼:HTML → `htmlspecialchars($str, ENT_QUOTES, 'UTF-8')`,JS → `json_encode($str)`,URL → `urlencode()`,CSS → `cssesc()`。絕不在入庫時過濾,保存原始數據。
  3. SQL 注入:全程參數化查詢 (Prepared Statements),PDO/MySQLi 占位符 `?` / `:name`。嚴禁拼接 SQL 字符串。
  4. 文件上傳安全:
    • MIME 類型用 `finfo` 驗證,不信任 `$_FILES['type']`。
    • 重命名存儲 (UUID + 正確副檔名),禁止保留原文件名。
    • 存儲目錄禁用腳本執行權限 (Nginx `location ~ \.php$ { deny all; }`)。
    • 病毒掃描 (ClamAV) / 圖片重繪 (Imagick `stripImage` 去除 EXIF/腳本)。
  5. 速率限制:登錄/註冊/發送驗證碼接口,IP + 用戶雙維度限流 (Redis 滑動窗口/令牌桶)。
  6. 安全 Header:
    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=()');

四、現代框架風格:表單請求對象 (Form Request / DTO)

<?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', '註冊成功');
}

五、PRG 模式 (Post-Redirect-Get) 詳解

用戶提交 POST
    │
    ▼
服務端處理 (驗證、入庫、發郵件...)
    │
    ├─ 成功 ──► HTTP 302/303 Redirect → GET /success-page
    │                (瀏覽器地址欄變為 GET URL)
    │                (刷新頁面不再重複提交)
    │
    └─ 失敗 ──► HTTP 200 直接渲染表單頁 (含錯誤信息、舊輸入)
                 (或 302 回表單頁 + Session Flash 消息)

核心價值:消除「重複提交」、「瀏覽器刷新警告」、「歷史記錄污染」。

結語

表單處理看似簡單,實則是 **HTTP 語義、安全防線、用戶體驗、代碼架構** 的交匯點。掌握 **GET/POST 語義邊界、CSRF/XSS/SQLi/上傳四大防線、PRG 模式、驗證器抽象**,並遵循「輸入過濾寬鬆、輸出編碼嚴格、參數化查詢、最小權限原則」,即能構建出既安全又優雅的表單處理體系。現代框架已將這些最佳實踐內建為組件,**理解原理才能用好框架,脫離框架也能寫出正確代碼**。

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