表单处理
表单是 Web 应用中用户与服务器交互的核心方式。PHP 作为服务端脚本语言,提供了丰富的功能来接收、验证和处理表单数据。本节将全面介绍如何使用 PHP 处理 HTML 表单,包括 GET 和 POST 请求的区别、表单验证、数据安全(防 XSS 攻击)、表单持久化(Sticky Forms)以及多值表单(复选框、多选框)的处理。
前置知识
阅读本节前,你需要:
- 已阅读过 基础交互脚本 一节,了解 PHP 基本语法
- 了解 HTML 表单的基础知识(
<form>、<input>、<select>等标签) - 知道 HTTP GET 和 POST 请求的基本区别
基础概念
HTML 表单基础
HTML 表单通过 <form> 标签定义,用户在表单中输入的数据会被发送到服务器指定的 URL 进行处理。以下是一个基本的 HTML 表单结构:
<?php
declare(strict_types=1);
// basic-form.php — 最简单的表单示例
// 将 PHP 处理逻辑和 HTML 表单放在同一个文件中
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>基本表单示例</title>
</head>
<body>
<h1>用户注册</h1>
<?php
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
echo "<p>提交成功!</p>";
echo "<p>用户名:" . htmlspecialchars($username, ENT_QUOTES, 'UTF-8') . "</p>";
echo "<p>邮箱:" . htmlspecialchars($email, ENT_QUOTES, 'UTF-8') . "</p>";
}
?>
<form method="POST" action="">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br><br>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required><br><br>
<button type="submit">提交</button>
</form>
</body>
</html>GET 与 POST 的区别
PHP 提供了两个超全局数组来接收表单数据:$_GET 和 $_POST,分别对应 HTTP 的 GET 和 POST 请求方法:
| 特性 | GET ($_GET) | POST ($_POST) |
|---|---|---|
| 数据位置 | URL 查询字符串中 | HTTP 请求体中 |
| 数据长度 | 有 URL 长度限制(约 2000 字符) | 理论上无限制(受 post_max_size 配置) |
| 可见性 | 数据在 URL 中可见(不安全) | 数据不在 URL 中(相对安全) |
| 浏览器历史 | 参数保存在历史记录中 | 不保存在历史记录中 |
| 缓存 | 可被缓存 | 不可缓存 |
| 安全性 | 不适合敏感数据 | 适合敏感数据 |
| 用途 | 搜索、筛选、分页 | 注册、登录、数据修改 |
| 幂等性 | 幂等(重复请求安全) | 非幂等 |
安全原则
- 永远不要用 GET 请求提交密码等敏感信息。
- POST 并不意味着绝对安全,仍需配合 HTTPS 使用。
- 所有的表单数据,无论来自 GET 还是 POST,都必须进行验证和转义。
PHP 接收表单数据
使用 $_GET 接收数据
$_GET 用于接收通过 URL 查询字符串传递的数据:
<?php
declare(strict_types=1);
// search.php — 使用 GET 方法的搜索页面
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品搜索</title>
</head>
<body>
<h1>商品搜索</h1>
<form method="GET" action="">
<label for="keyword">关键词:</label>
<input type="text" id="keyword" name="q"
value="<?= htmlspecialchars($_GET['q'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="category">分类:</label>
<select id="category" name="category">
<option value="">全部分类</option>
<option value="electronics" <?= ($_GET['category'] ?? '') === 'electronics' ? 'selected' : '' ?>>电子产品</option>
<option value="books" <?= ($_GET['category'] ?? '') === 'books' ? 'selected' : '' ?>>图书</option>
<option value="clothing" <?= ($_GET['category'] ?? '') === 'clothing' ? 'selected' : '' ?>>服装</option>
</select>
<button type="submit">搜索</button>
</form>
<hr>
<?php
$keyword = trim($_GET['q'] ?? '');
$category = $_GET['category'] ?? '';
if ($keyword !== '') {
echo "<p>搜索关键词:<strong>" . htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8') . "</strong></p>";
if ($category !== '') {
$categoryNames = [
'electronics' => '电子产品',
'books' => '图书',
'clothing' => '服装',
];
echo "<p>分类:" . ($categoryNames[$category] ?? $category) . "</p>";
}
echo "<p>(这里应该是实际的搜索结果展示区域)</p>";
} else {
echo "<p>请输入关键词进行搜索。</p>";
}
?>
</body>
</html>使用 $_POST 接收数据
$_POST 用于接收通过 HTTP POST 请求体传递的数据,适合提交敏感信息或大量数据:
<?php
declare(strict_types=1);
// login.php — 使用 POST 方法的登录页面
$errors = [];
$formData = [
'email' => '',
'password' => '',
'remember' => false,
];
// 处理 POST 请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$formData['email'] = trim($_POST['email'] ?? '');
$formData['password'] = $_POST['password'] ?? '';
$formData['remember'] = isset($_POST['remember']);
if ($formData['email'] === '') {
$errors['email'] = '请输入邮箱地址';
} elseif (!filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '邮箱格式不正确';
}
if ($formData['password'] === '') {
$errors['password'] = '请输入密码';
} elseif (strlen($formData['password']) < 6) {
$errors['password'] = '密码长度不能少于 6 个字符';
}
if (empty($errors)) {
echo "<p>登录验证通过!(示例演示)</p>";
echo "<p>邮箱:" . htmlspecialchars($formData['email'], ENT_QUOTES, 'UTF-8') . "</p>";
echo "<p>记住我:" . ($formData['remember'] ? '是' : '否') . "</p>";
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<?php if (!empty($errors)): ?>
<div style="color: red;">
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form method="POST" action="">
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email"
value="<?= htmlspecialchars($formData['email'], ENT_QUOTES, 'UTF-8') ?>"
required>
<?php if (isset($errors['email'])): ?>
<span style="color: red;"><?= htmlspecialchars($errors['email'], ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
</div>
<br>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
<?php if (isset($errors['password'])): ?>
<span style="color: red;"><?= htmlspecialchars($errors['password'], ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
</div>
<br>
<div>
<label>
<input type="checkbox" name="remember"
<?= $formData['remember'] ? 'checked' : '' ?>>
记住我
</label>
</div>
<br>
<button type="submit">登录</button>
</form>
</body>
</html>表单验证
服务端验证
无论客户端做了多少验证,服务端验证都是必须的。以下是一个完整的表单验证器类:
<?php
declare(strict_types=1);
/**
* 表单验证器类
* 提供常用的表单验证方法
*/
class FormValidator
{
private array $errors = [];
private array $data = [];
public function validate(array $data, array $rules): bool
{
$this->data = $data;
$this->errors = [];
foreach ($rules as $field => $fieldRules) {
$value = $data[$field] ?? null;
foreach ($fieldRules as $rule => $param) {
$this->applyRule($field, $value, $rule, $param);
if (isset($this->errors[$field])) {
break;
}
}
}
return empty($this->errors);
}
private function applyRule(string $field, mixed $value, string $rule, mixed $param): void
{
$fieldLabel = is_array($param) ? ($param['label'] ?? $field) : $field;
match ($rule) {
'required' => $this->validateRequired($field, $value, $fieldLabel),
'email' => $this->validateEmail($field, $value, $fieldLabel),
'min_length' => $this->validateMinLength($field, $value, (int) $param, $fieldLabel),
'max_length' => $this->validateMaxLength($field, $value, (int) $param, $fieldLabel),
'pattern' => $this->validatePattern($field, $value, $param, $fieldLabel),
'confirmed' => $this->validateConfirmed($field, $value, $param, $fieldLabel),
default => null,
};
}
private function validateRequired(string $field, mixed $value, string $label): void
{
if ($value === null || $value === '') {
$this->errors[$field] = "{$label} 不能为空";
}
}
private function validateEmail(string $field, mixed $value, string $label): void
{
if ($value !== '' && $value !== null && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->errors[$field] = "{$label} 格式不正确";
}
}
private function validateMinLength(string $field, mixed $value, int $min, string $label): void
{
if ($value !== null && mb_strlen((string) $value, 'UTF-8') < $min) {
$this->errors[$field] = "{$label} 长度不能少于 {$min} 个字符";
}
}
private function validateMaxLength(string $field, mixed $value, int $max, string $label): void
{
if ($value !== null && mb_strlen((string) $value, 'UTF-8') > $max) {
$this->errors[$field] = "{$label} 长度不能超过 {$max} 个字符";
}
}
private function validatePattern(string $field, mixed $value, string $pattern, string $label): void
{
if ($value !== '' && $value !== null && !preg_match($pattern, (string) $value)) {
$this->errors[$field] = "{$label} 格式不正确";
}
}
private function validateConfirmed(string $field, mixed $value, string $otherField, string $label): void
{
$otherValue = $this->data[$otherField] ?? null;
if ($value !== $otherValue) {
$this->errors[$field] = "{$label} 两次输入不一致";
}
}
public function getErrors(): array
{
return $this->errors;
}
public function getFirstError(): ?string
{
return empty($this->errors) ? null : reset($this->errors);
}
public function hasError(string $field): bool
{
return isset($this->errors[$field]);
}
public function getError(string $field): string
{
return $this->errors[$field] ?? '';
}
}
// 使用示例
$validator = new FormValidator();
$rules = [
'username' => [
'required' => '用户名',
'min_length' => 2,
'max_length' => 20,
'pattern' => '/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u',
],
'email' => [
'required' => '邮箱',
'email' => '邮箱',
],
'password' => [
'required' => '密码',
'min_length' => 8,
],
'password_confirm' => [
'required' => '确认密码',
'confirmed' => 'password',
],
];
// 模拟表单数据
$formData = [
'username' => '张三',
'email' => 'zhangsan@example.com',
'password' => '12345678',
'password_confirm' => '12345678',
];
$isValid = $validator->validate($formData, $rules);
echo "验证结果: " . ($isValid ? '通过' : '失败') . "\n";
if (!$isValid) {
foreach ($validator->getErrors() as $field => $error) {
echo " {$field}: {$error}\n";
}
}数据安全 — 防止 XSS 攻击
htmlspecialchars 的使用
跨站脚本攻击(XSS)是 Web 应用中最常见的安全漏洞之一。PHP 提供了 htmlspecialchars() 函数来对输出进行转义,防止 XSS 攻击:
<?php
declare(strict_types=1);
/**
* 安全输出辅助类
* 封装常用的安全转义函数
*/
class SecurityHelper
{
/**
* 安全地输出 HTML 内容
* 将特殊字符转换为 HTML 实体
*/
public static function escapeHtml(string $string): string
{
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* 安全地输出 HTML 属性
*/
public static function escapeAttr(string $string): string
{
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* 净化 HTML 内容(移除危险的标签和属性)
* 简单实现,生产环境建议使用 HTML Purifier 库
*/
public static function sanitizeHtml(string $html): string
{
// 移除 script 标签
$html = preg_replace('#<script[^>]*>.*?</script>#is', '', $html);
// 移除 onclick 等事件属性
$html = preg_replace('#\s*on\w+\s*=\s*["\'][^"\']*["\']#i', '', $html);
// 移除 javascript: 协议
$html = preg_replace('#javascript:#i', '', $html);
return $html;
}
/**
* 生成 CSRF 令牌
*/
public static function generateCsrfToken(): string
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
return $token;
}
/**
* 验证 CSRF 令牌
*/
public static function verifyCsrfToken(string $token): bool
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
}
// XSS 攻击演示 — 不转义的危险
$maliciousInput = '<script>alert("XSS攻击!")</script><img src="x" onerror="alert(\'hack\')">';
echo "原始输入: {$maliciousInput}\n";
echo "转义输出: " . SecurityHelper::escapeHtml($maliciousInput) . "\n";
echo "净化HTML: " . SecurityHelper::sanitizeHtml($maliciousInput) . "\n";
// 用户输入的安全处理示例
$userInput = $_GET['name'] ?? 'World';
$safeOutput = SecurityHelper::escapeHtml($userInput);
echo "安全输出: {$safeOutput}\n";htmlspecialchars 参数说明
ENT_QUOTES:转义单引号和双引号(推荐)ENT_HTML5:使用 HTML5 实体模式'UTF-8':指定字符编码(必须与页面编码一致)
始终使用这三个参数的组合:htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8')
表单持久化 — Sticky Forms
保持表单数据
当表单验证失败时,应该保留用户已输入的数据(称为 Sticky Forms),避免用户重新填写所有字段:
<?php
declare(strict_types=1);
// contact-form.php — 带有持久化和完整验证的联系表单
session_start();
$errors = [];
$oldInput = [];
// 定义表单字段及其默认值
$fields = [
'name' => '',
'email' => '',
'phone' => '',
'subject' => 'general',
'message' => '',
'priority' => 'normal',
];
// 处理 POST 请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 保存旧输入(用于表单回填)
$oldInput = [
'name' => trim($_POST['name'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'phone' => trim($_POST['phone'] ?? ''),
'subject' => $_POST['subject'] ?? 'general',
'message' => trim($_POST['message'] ?? ''),
'priority' => $_POST['priority'] ?? 'normal',
];
// 验证规则
if ($oldInput['name'] === '') {
$errors['name'] = '请输入您的姓名';
} elseif (mb_strlen($oldInput['name'], 'UTF-8') > 50) {
$errors['name'] = '姓名不能超过 50 个字符';
}
if ($oldInput['email'] === '') {
$errors['email'] = '请输入邮箱地址';
} elseif (!filter_var($oldInput['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '邮箱格式不正确';
}
if ($oldInput['phone'] !== '' && !preg_match('/^1[3-9]\d{9}$/', $oldInput['phone'])) {
$errors['phone'] = '手机号格式不正确';
}
if ($oldInput['message'] === '') {
$errors['message'] = '请输入留言内容';
} elseif (mb_strlen($oldInput['message'], 'UTF-8') < 10) {
$errors['message'] = '留言内容不能少于 10 个字符';
} elseif (mb_strlen($oldInput['message'], 'UTF-8') > 2000) {
$errors['message'] = '留言内容不能超过 2000 个字符';
}
// 验证通过
if (empty($errors)) {
$_SESSION['form_success'] = true;
$_SESSION['form_message'] = '感谢您的留言!我们会尽快回复。';
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
}
// 显示成功消息
if (isset($_SESSION['form_success'])) {
$successMessage = $_SESSION['form_message'];
unset($_SESSION['form_success'], $_SESSION['form_message']);
}
// 辅助函数:获取旧输入值
function old(string $field, string $default = ''): string
{
global $oldInput;
return htmlspecialchars($oldInput[$field] ?? $default, ENT_QUOTES, 'UTF-8');
}
// 辅助函数:检查字段是否有错误
function hasError(string $field): bool
{
global $errors;
return isset($errors[$field]);
}
// 辅助函数:获取错误信息
function getError(string $field): string
{
global $errors;
return htmlspecialchars($errors[$field] ?? '', ENT_QUOTES, 'UTF-8');
}
// 辅助函数:选中选项
function selected(string $field, string $value): string
{
global $oldInput;
return ($oldInput[$field] ?? '') === $value ? 'selected' : '';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>联系我们</title>
</head>
<body>
<h1>联系我们</h1>
<?php if (isset($successMessage)): ?>
<div style="color: green; padding: 10px; background: #e8f5e9; border-radius: 4px;">
<?= htmlspecialchars($successMessage, ENT_QUOTES, 'UTF-8') ?>
</div>
<?php endif; ?>
<form method="POST" action="">
<div>
<label for="name">姓名 *</label>
<input type="text" id="name" name="name" value="<?= old('name') ?>" required>
<?php if (hasError('name')): ?>
<small style="color: red;"><?= getError('name') ?></small>
<?php endif; ?>
</div>
<br>
<div>
<label for="email">邮箱 *</label>
<input type="email" id="email" name="email" value="<?= old('email') ?>" required>
<?php if (hasError('email')): ?>
<small style="color: red;"><?= getError('email') ?></small>
<?php endif; ?>
</div>
<br>
<div>
<label for="phone">手机号</label>
<input type="tel" id="phone" name="phone" value="<?= old('phone') ?>"
placeholder="请输入 11 位手机号">
<?php if (hasError('phone')): ?>
<small style="color: red;"><?= getError('phone') ?></small>
<?php endif; ?>
</div>
<br>
<div>
<label for="subject">主题</label>
<select id="subject" name="subject">
<option value="general" <?= selected('subject', 'general') ?>>一般咨询</option>
<option value="support" <?= selected('subject', 'support') ?>>技术支持</option>
<option value="business" <?= selected('subject', 'business') ?>>商务合作</option>
<option value="other" <?= selected('subject', 'other') ?>>其他</option>
</select>
</div>
<br>
<div>
<label for="message">留言内容 *</label>
<textarea id="message" name="message" rows="5" cols="40"
required><?= old('message') ?></textarea>
<?php if (hasError('message')): ?>
<small style="color: red;"><?= getError('message') ?></small>
<?php endif; ?>
</div>
<br>
<div>
<label>优先级:</label>
<label>
<input type="radio" name="priority" value="low"
<?= selected('priority', 'low') ?>>
低
</label>
<label>
<input type="radio" name="priority" value="normal"
<?= selected('priority', 'normal') ?>>
普通
</label>
<label>
<input type="radio" name="priority" value="high"
<?= selected('priority', 'high') ?>>
高
</label>
</div>
<br>
<button type="submit">提交</button>
</form>
</body>
</html>多值表单处理
复选框和多选框
处理复选框(checkbox)和多选框(select multiple)需要特别注意,因为这类表单元素可以提交多个值:
<?php
declare(strict_types=1);
// multi-value-form.php — 多值表单处理示例
// PHP 中处理多值表单的关键是在 HTML 中使用数组语法
// 例如:name="skills[]" 而不是 name="skills"
// 也可以使用 name="user[address][city]" 创建嵌套数组
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 收集多值表单数据
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
// 复选框 — 兴趣爱好(可能没有选中任何项)
$hobbies = $_POST['hobbies'] ?? [];
if (!is_array($hobbies)) {
$hobbies = [];
}
// 多选框 — 编程语言技能
$skills = $_POST['skills'] ?? [];
if (!is_array($skills)) {
$skills = [];
}
// 单选按钮 — 经验等级
$experience = $_POST['experience'] ?? 'junior';
// 验证
if ($username === '') {
$errors['username'] = '请输入用户名';
}
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '请输入有效的邮箱地址';
}
if (empty($errors)) {
$success = true;
}
}
// 辅助函数:检查复选框/多选框是否被选中
function isChecked(string $name, string $value): string
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
return '';
}
$array = $_POST[$name] ?? [];
return in_array($value, (array) $array, true) ? 'checked' : '';
}
function isSelected(string $name, string $value): string
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
return '';
}
$array = $_POST[$name] ?? [];
return in_array($value, (array) $array, true) ? 'selected' : '';
}
// 允许的兴趣爱好选项
$availableHobbies = [
'reading' => '阅读',
'gaming' => '游戏',
'coding' => '编程',
'music' => '音乐',
'sports' => '运动',
'travel' => '旅行',
];
// 允许的编程语言选项
$availableSkills = [
'php' => 'PHP',
'javascript' => 'JavaScript',
'python' => 'Python',
'java' => 'Java',
'go' => 'Go',
'rust' => 'Rust',
'typescript' => 'TypeScript',
'csharp' => 'C#',
];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>多值表单示例</title>
</head>
<body>
<h1>开发者调查问卷</h1>
<?php if ($success): ?>
<div style="color: green;">
<p>提交成功!以下是您的选择:</p>
<ul>
<li>用户名:<?= htmlspecialchars($username, ENT_QUOTES, 'UTF-8') ?></li>
<li>邮箱:<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?></li>
<li>兴趣:<?= implode(', ', array_map(fn($h) => $availableHobbies[$h] ?? $h, $hobbies)) ?></li>
<li>技能:<?= implode(', ', array_map(fn($s) => $availableSkills[$s] ?? $s, $skills)) ?></li>
<li>经验:<?= htmlspecialchars($experience, ENT_QUOTES, 'UTF-8') ?></li>
</ul>
</div>
<?php endif; ?>
<form method="POST" action="">
<!-- 复选框组 -->
<fieldset>
<legend>兴趣爱好(可多选)</legend>
<?php foreach ($availableHobbies as $value => $label): ?>
<label>
<input type="checkbox" name="hobbies[]" value="<?= $value ?>"
<?= isChecked('hobbies', $value) ?>>
<?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
</label>
<?php endforeach; ?>
</fieldset>
<br>
<!-- 多选下拉框 -->
<fieldset>
<legend>编程语言技能(按住 Ctrl/Cmd 多选)</legend>
<select name="skills[]" multiple size="5">
<?php foreach ($availableSkills as $value => $label): ?>
<option value="<?= $value ?>" <?= isSelected('skills', $value) ?>>
<?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</fieldset>
<br>
<!-- 单选按钮 -->
<fieldset>
<legend>经验等级</legend>
<label>
<input type="radio" name="experience" value="junior"
<?= ($_POST['experience'] ?? 'junior') === 'junior' ? 'checked' : '' ?>>
初级(1-3 年)
</label>
<label>
<input type="radio" name="experience" value="mid"
<?= ($_POST['experience'] ?? '') === 'mid' ? 'checked' : '' ?>>
中级(3-5 年)
</label>
<label>
<input type="radio" name="experience" value="senior"
<?= ($_POST['experience'] ?? '') === 'senior' ? 'checked' : '' ?>>
高级(5 年以上)
</label>
</fieldset>
<br>
<button type="submit">提交</button>
</form>
</body>
</html>实战示例
带文件上传的完整注册表单
<?php
declare(strict_types=1);
/**
* 完整的用户注册表单处理
* 展示了表单验证、CSRF 防护、文件上传、数据持久化
*/
class RegistrationHandler
{
private array $errors = [];
private array $data = [];
private string $csrfToken = '';
private ?string $uploadedAvatar = null;
public function __construct()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
public function generateCsrfToken(): string
{
$this->csrfToken = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $this->csrfToken;
$_SESSION['csrf_token_time'] = time();
return $this->csrfToken;
}
private function verifyCsrf(): bool
{
$token = $_POST['_token'] ?? '';
$storedToken = $_SESSION['csrf_token'] ?? '';
$tokenTime = $_SESSION['csrf_token_time'] ?? 0;
// 令牌过期检查(30 分钟)
if (time() - $tokenTime > 1800) {
return false;
}
return hash_equals($storedToken, $token);
}
public function handleSubmission(): bool
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
return false;
}
// CSRF 验证
if (!$this->verifyCsrf()) {
$this->errors['_token'] = '安全验证失败,请重新提交';
return false;
}
// 收集数据
$this->data = [
'username' => trim($_POST['username'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'password' => $_POST['password'] ?? '',
'passwordConfirm' => $_POST['password_confirm'] ?? '',
'bio' => trim($_POST['bio'] ?? ''),
'website' => trim($_POST['website'] ?? ''),
'gender' => $_POST['gender'] ?? '',
];
// 表单验证
$this->validateForm();
// 处理头像上传
if (!isset($this->errors['avatar'])) {
$this->handleAvatarUpload();
}
return empty($this->errors);
}
private function validateForm(): void
{
// 用户名验证
if ($this->data['username'] === '') {
$this->errors['username'] = '用户名不能为空';
} elseif (mb_strlen($this->data['username'], 'UTF-8') < 3) {
$this->errors['username'] = '用户名至少 3 个字符';
} elseif (mb_strlen($this->data['username'], 'UTF-8') > 20) {
$this->errors['username'] = '用户名不能超过 20 个字符';
} elseif (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $this->data['username'])) {
$this->errors['username'] = '用户名只能包含字母、数字、下划线和中文';
}
// 邮箱验证
if ($this->data['email'] === '') {
$this->errors['email'] = '邮箱不能为空';
} elseif (!filter_var($this->data['email'], FILTER_VALIDATE_EMAIL)) {
$this->errors['email'] = '邮箱格式不正确';
}
// 密码验证
if ($this->data['password'] === '') {
$this->errors['password'] = '密码不能为空';
} elseif (strlen($this->data['password']) < 8) {
$this->errors['password'] = '密码至少 8 个字符';
} elseif (!preg_match('/[a-z]/', $this->data['password']) || !preg_match('/[A-Z]/', $this->data['password'])) {
$this->errors['password'] = '密码必须包含大小写字母';
} elseif (!preg_match('/\d/', $this->data['password'])) {
$this->errors['password'] = '密码必须包含数字';
}
// 确认密码
if ($this->data['password'] !== $this->data['passwordConfirm']) {
$this->errors['password_confirm'] = '两次密码输入不一致';
}
// 个人简介
if (mb_strlen($this->data['bio'], 'UTF-8') > 500) {
$this->errors['bio'] = '个人简介不能超过 500 个字符';
}
// 网站验证
if ($this->data['website'] !== '' && !filter_var($this->data['website'], FILTER_VALIDATE_URL)) {
$this->errors['website'] = '网站地址格式不正确';
}
}
private function handleAvatarUpload(): void
{
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] === UPLOAD_ERR_NO_FILE) {
return;
}
$file = $_FILES['avatar'];
// 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->errors['avatar'] = '文件上传失败';
return;
}
// 检查文件大小(最大 2MB)
if ($file['size'] > 2 * 1024 * 1024) {
$this->errors['avatar'] = '头像文件大小不能超过 2MB';
return;
}
// 检查文件类型
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!in_array($mimeType, $allowedTypes, true)) {
$this->errors['avatar'] = '只支持 JPG、PNG、GIF、WebP 格式的图片';
return;
}
// 生成安全的文件名
$extension = match ($mimeType) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
default => 'bin',
};
$safeName = bin2hex(random_bytes(16)) . '.' . $extension;
$this->uploadedAvatar = '/uploads/avatars/' . $safeName;
}
public function getErrors(): array
{
return $this->errors;
}
public function getData(): array
{
return $this->data;
}
public function getCsrfToken(): string
{
return $this->csrfToken;
}
}
// 使用示例(CLI 模式下的演示)
if (PHP_SAPI === 'cli') {
$handler = new RegistrationHandler();
$token = $handler->generateCsrfToken();
echo "CSRF Token 已生成: " . substr($token, 0, 16) . "...\n";
echo "注册处理器已就绪,在 Web 模式下可处理完整的表单提交。\n";
}注意事项
- 永远不要信任用户输入:无论前端做了多少验证,服务端必须进行完整的验证。
- 始终转义输出:使用
htmlspecialchars()转义所有输出到 HTML 的用户数据,防止 XSS 攻击。 - 使用 CSRF 令牌:所有修改数据的表单都应该包含 CSRF 令牌,防止跨站请求伪造攻击。
- 文件上传安全:处理文件上传时,始终检查文件类型(使用
finfo而不是扩展名)、文件大小、上传路径。不要使用原始文件名。 - 密码安全:永远不要以明文存储密码。使用
password_hash()和password_verify()处理密码。 - POST 重定向模式:表单处理成功后,使用 PRG(Post/Redirect/Get)模式,避免用户刷新页面导致重复提交。
$_POST的限制:$_POST只能接收application/x-www-form-urlencoded和multipart/form-data类型的数据。如果需要接收 JSON 数据,使用file_get_contents('php://input')。
最佳实践
- 使用 PRG 模式(Post/Redirect/Get)防止表单重复提交。
- 使用 CSRF 令牌保护所有修改数据的表单。
- 使用
filter_var()配合预定义过滤器进行数据验证,避免手写正则表达式。 - 为每个表单字段提供清晰的错误提示信息。
- 使用
<label>标签关联表单控件,提高可访问性。 - 文件名使用数组语法(
name="files[]")处理多文件上传。 - 使用
readonly、disabled属性防止用户修改不应修改的字段。 - 在生产环境中使用框架的表单组件(如 Laravel 的 Form Request Validation、Symfony 的 Form Component)。
下一节
掌握了表单处理后,你已经能够构建交互式的 Web 页面。接下来让我们学习 数据库交互,了解如何将数据持久化到数据库中。