一、回顾与本篇目标
上一篇你学会了用 PDO 连接 MySQL 数据库,实现增删改查和事务。现在你的 PHP 程序能把数据持久化到数据库中了。
但有一个关键问题还没解决:数据从哪来?之前的示例中,数据要么是写死在代码里的,要么是手动改 URL 传的。一个真正的 Web 应用需要让用户通过表单提交数据——注册账号、发表评论、上传文件、搜索商品——这些都是通过表单完成的。
表单处理是 Web 开发中最核心的交互模式。但同时,它也是最危险的环节——用户输入的数据可能包含恶意代码(XSS 攻击)、精心构造的 SQL 语句(SQL 注入)、伪造的请求(CSRF 攻击)。处理好用户输入,是后端开发的基本功和底线。
本篇的目标:
- 理解 GET 和 POST 两种请求方式的区别和适用场景
- 学会用
$_GET、$_POST、$_REQUEST接收用户数据 - 掌握表单验证的完整流程:必填检查、格式校验、长度限制
- 学会防御 XSS 攻击——永远不要信任用户输入
- 理解 CSRF 攻击的基本原理和防御策略
- 学会文件上传的处理
二、HTTP 请求方法:GET 和 POST
在深入表单之前,先理解浏览器向服务器发送数据的两种基本方式。如果你之前跟过《后端零基础入门》的 HTTP 篇,这部分可以快速回顾。
2.1 GET 请求——从服务器获取数据
当你直接在浏览器地址栏输入 URL 并回车,或者点击一个普通的链接,浏览器发送的就是 GET 请求。GET 请求的特点:
- 参数在 URL 中可见:
http://example.com/search.php?keyword=手机&page=2 - 数据量有限:URL 长度有限制(通常 2048 字符以内),不适合传输大量数据。
- 可被缓存和书签:带参数的 GET URL 可以被收藏、分享、被搜索引擎索引。
- 是幂等的:多次相同的 GET 请求应该返回相同的结果,不应该修改服务器上的数据。
- 典型用途:搜索、查看详情、分页浏览——只读操作。
2.2 POST 请求——向服务器提交数据
当你提交一个表单(注册、登录、发表评论),浏览器发送的就是 POST 请求。POST 请求的特点:
- 参数在请求体中,不在 URL 中:用户看不到提交的数据,但它们仍然是明文传输的(除非使用 HTTPS)。
- 数据量没有限制:可以提交大量数据,包括文件。
- 不可缓存、不可书签:刷新 POST 请求的页面时,浏览器会提示”是否重新提交表单”。
- 不是幂等的:多次相同的 POST 请求可能创建多条重复数据。
- 典型用途:注册、登录、发表内容、上传文件——任何会改变服务器状态的操作。
2.3 一句话规则
读取数据用 GET,写入数据用 POST。 不要在 GET 请求中修改数据(比如 GET /delete.php?id=1 直接删除一条记录),因为浏览器可能会预加载链接、搜索引擎会爬取它们,导致数据被意外删除。
三、PHP 接收用户输入
PHP 提供了三个超全局变量来接收用户提交的数据。超全局变量在 PHP 的任何位置(函数内部、类内部、文件最外层)都可以直接访问,不需要 global 关键字。
3.1 $_GET——接收 GET 请求的参数
在 URL 中通过 ? 传递的参数,可以通过 $_GET 数组获取:
<?php
// 访问 URL:http://localhost:8080/search.php?keyword=手机&page=2
$keyword = $_GET['keyword'] ?? ''; // 使用 ?? 防止未定义错误
$page = (int)($_GET['page'] ?? 1); // 转成整数,提供默认值
echo "搜索关键词:{$keyword},页码:{$page}";
?>
关键点:
- 始终用
??提供默认值:如果用户没有传某个参数,直接用$_GET['keyword']会抛出警告。 $_GET中的值永远是字符串或数组。如果是数字,需要手动转换类型(int)或(float)。
3.2 $_POST——接收 POST 请求的数据
表单以 POST 方式提交的数据,通过 $_POST 数组获取:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$message = $_POST['message'] ?? '';
echo "收到留言:{$name}({$email})说:{$message}";
}
?>
<form method="POST" action="">
<input type="text" name="name" placeholder="姓名" required><br>
<input type="email" name="email" placeholder="邮箱" required><br>
<textarea name="message" placeholder="留言内容" required></textarea><br>
<button type="submit">提交</button>
</form>
关键点:
action="":表单数据提交到当前页面(空字符串表示当前 URL)。method="POST":指定以 POST 方式提交。如果改成 GET,数据会出现在 URL 中,而且有长度限制。name属性:这是表单控件和$_POST数组之间的桥梁。<input name="email">的值在 PHP 中通过$_POST['email']获取。
3.3 $_REQUEST——同时接收 GET 和 POST
$_REQUEST 包含了 $_GET、$_POST 和 $_COOKIE 的内容。但不推荐使用,因为:
- 你不知道数据从哪来的(GET 还是 POST),不利于调试。
- 如果 GET 和 POST 中有同名的键,
$_REQUEST的值取决于 PHP 配置,不确定。
最佳实践:明确使用 $_GET 或 $_POST,不要用 $_REQUEST。
3.4 $_SERVER——获取请求信息
$_SERVER 包含了服务器和执行环境的信息,最常用的几个键:
| 键名 | 含义 | 示例值 |
|---|---|---|
REQUEST_METHOD |
请求方法 | "GET"、"POST" |
REQUEST_URI |
请求的路径和查询参数 | "/search.php?keyword=手机" |
HTTP_REFERER |
从哪个页面跳转过来的 | "https://www.google.com/" |
REMOTE_ADDR |
客户端的 IP 地址 | "192.168.1.1" |
HTTP_USER_AGENT |
客户端的浏览器信息 | "Mozilla/5.0 ..." |
<?php
echo "请求方法:" . $_SERVER['REQUEST_METHOD'] . "<br>";
echo "请求路径:" . $_SERVER['REQUEST_URI'] . "<br>";
echo "客户端 IP:" . $_SERVER['REMOTE_ADDR'] . "<br>";
?>
注意:HTTP_REFERER 和 HTTP_USER_AGENT 来自客户端,可以被伪造。不要依赖它们做安全相关的判断。
四、表单数据验证
用户输入永远不可信任。浏览器端的表单验证(required、type="email" 等)只能提高用户体验,不能作为安全措施——攻击者可以直接用脚本发送 HTTP 请求,完全绕过浏览器的验证。
所有验证必须在服务端再做一遍。
4.1 必填检查
<?php
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$age = $_POST['age'] ?? '';
// 必填检查
if ($name === '') {
$errors['name'] = '姓名不能为空';
}
if ($email === '') {
$errors['email'] = '邮箱不能为空';
}
if ($age === '') {
$errors['age'] = '年龄不能为空';
}
// 如果没有错误,继续处理
if (empty($errors)) {
echo "验证通过!姓名:{$name},邮箱:{$email},年龄:{$age}";
}
}
?>
trim() 函数去掉字符串首尾的空白字符。用户可能在输入时多打了空格,trim() 让验证更准确。
4.2 格式校验
<?php
// 邮箱格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '邮箱格式不正确';
}
// URL 格式
if (!filter_var($url, FILTER_VALIDATE_URL)) {
$errors['url'] = '网址格式不正确';
}
// 整数范围
$age = (int)$age; // 先转成整数
if ($age < 1 || $age > 150) {
$errors['age'] = '年龄必须在 1-150 之间';
}
// 字符串长度
if (mb_strlen($name) < 2) {
$errors['name'] = '姓名至少需要 2 个字符';
}
if (mb_strlen($name) > 50) {
$errors['name'] = '姓名不能超过 50 个字符';
}
// 正则表达式验证
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors['username'] = '用户名只能包含字母、数字和下划线';
}
?>
常用验证函数:
| 验证类型 | 函数 / 方法 | 示例 |
|---|---|---|
| 邮箱 | filter_var($email, FILTER_VALIDATE_EMAIL) |
返回邮箱字符串或 false |
| URL | filter_var($url, FILTER_VALIDATE_URL) |
返回 URL 或 false |
| 整数 | filter_var($int, FILTER_VALIDATE_INT) |
返回整数或 false |
| 字符串长度 | mb_strlen($str) |
返回字符数(支持中文) |
| 正则匹配 | preg_match($pattern, $str) |
返回 1(匹配)或 0(不匹配) |
使用 filter_var() 进行验证和过滤:
<?php
// 验证:返回验证后的值或 false
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if ($email === false) {
echo "邮箱格式不正确";
}
// 过滤:去除不合法的字符
$clean_int = filter_var($_POST['age'], FILTER_SANITIZE_NUMBER_INT);
// 输入 "abc123def456" → 输出 "123456"
$clean_string = filter_var($_POST['name'], FILTER_SANITIZE_STRING);
// 去除 HTML 标签和特殊字符
?>
4.3 完整验证示例
<?php
function validateUserInput(array $data): array {
$errors = [];
// 姓名验证
$name = trim($data['name'] ?? '');
if ($name === '') {
$errors['name'] = '姓名不能为空';
} elseif (mb_strlen($name) < 2) {
$errors['name'] = '姓名至少需要 2 个字符';
} elseif (mb_strlen($name) > 50) {
$errors['name'] = '姓名不能超过 50 个字符';
}
// 邮箱验证
$email = trim($data['email'] ?? '');
if ($email === '') {
$errors['email'] = '邮箱不能为空';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '邮箱格式不正确';
}
// 密码验证
$password = $data['password'] ?? '';
if ($password === '') {
$errors['password'] = '密码不能为空';
} elseif (mb_strlen($password) < 6) {
$errors['password'] = '密码至少需要 6 个字符';
}
// 确认密码
$password_confirm = $data['password_confirm'] ?? '';
if ($password !== $password_confirm) {
$errors['password_confirm'] = '两次输入的密码不一致';
}
return $errors;
}
?>
五、XSS 攻击与防御
XSS(Cross-Site Scripting,跨站脚本攻击)是最常见的 Web 安全漏洞之一。攻击者在输入框中插入 JavaScript 代码,如果服务端不处理直接输出到页面上,这段代码就会在其他用户的浏览器中执行。
5.1 XSS 攻击示例
假设你有一个留言板,用户提交留言后直接显示:
<?php
// 危险的写法!
echo "<p>" . $_POST['message'] . "</p>";
?>
如果攻击者在留言中输入:
<script>alert('你的账号被黑了!')</script>
这段代码会被直接嵌入 HTML,每个浏览留言板的用户都会弹出一个对话框。更危险的攻击可以窃取 Cookie、重定向到钓鱼网站、或者以用户身份执行操作。
5.2 XSS 防御:永远转义输出
核心原则:任何可能包含用户输入的数据,在输出到 HTML 之前,必须转义 HTML 特殊字符。
PHP 提供了 htmlspecialchars() 函数来做这件事:
<?php
$user_input = '<script>alert("XSS")</script>';
// 安全的输出
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
// 输出:<script>alert("XSS")</script>
// 浏览器会显示这段文字,但不会执行它
?>
htmlspecialchars() 的四个参数:
- 第一个参数:要转义的字符串。
- 第二个参数
ENT_QUOTES:同时转义单引号和双引号。这是最安全的选项。 - 第三个参数
'UTF-8':指定字符编码。中文网站必须用 UTF-8。
哪些字符会被转义:
| 字符 | 转义后 |
|---|---|
< |
< |
> |
> |
" |
" |
' |
' |
& |
& |
最佳实践:在输出时转义,而不是在存储时转义。数据库里存原始数据,显示的时候再转义——这样数据可以灵活地用于不同场景(HTML 页面、JSON API、纯文本导出等)。
<?php
// 存储:保持原始数据
$message = $_POST['message']; // 可能包含 HTML
// 存入数据库...(用参数化查询防止 SQL 注入)
// 显示:转义后输出
echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
?>
5.3 不要过滤用户输入
有些开发者为了防止 XSS,在用户提交时就过滤掉 <script> 等标签。这种做法不推荐——它破坏了用户原本想表达的内容。用户输入的内容应该被原样存储,只在输出时转义。这样数据不会丢失,灵活性最高。
六、CSRF 攻击与防御
CSRF(Cross-Site Request Forgery,跨站请求伪造)是另一种常见的 Web 攻击。攻击者诱导用户在不知情的情况下,向一个已登录的网站发送恶意请求。
6.1 CSRF 攻击原理
假设你登录了银行网站 bank.com,登录状态通过 Cookie 保持。然后你点击了某封钓鱼邮件里的链接,跳转到了一个恶意网站。这个恶意网站里有一个隐藏的表单:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="攻击者的账户">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
由于你仍然处于 bank.com 的登录状态(Cookie 还在),浏览器会自动带上 Cookie 发送转账请求。银行服务器看到请求带着有效的 Cookie,以为是你本人发起的,就执行了转账。
6.2 CSRF 防御:生成和验证 Token
防御 CSRF 的核心思路是:在表单中嵌入一个随机的、不可预测的 Token,服务器验证这个 Token 是否匹配。攻击者无法获取或预测这个 Token,所以无法构造有效的恶意请求。
<?php
session_start();
// 生成 CSRF Token
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证 CSRF Token
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die("CSRF 验证失败!");
}
// Token 验证通过,处理表单数据
echo "表单提交成功!";
}
?>
<form method="POST">
<!-- 隐藏字段:存放 CSRF Token -->
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="text" name="name" placeholder="姓名"><br>
<button type="submit">提交</button>
</form>
关键点:
- Token 存储在 Session 中,攻击者无法读取。
- 每次请求都验证 Token 是否匹配。
- Token 应该是随机的、不可预测的——
random_bytes(32)生成 32 字节的加密安全随机数。
七、文件上传
文件上传是表单处理中的一个特殊场景,需要额外关注安全和存储。
7.1 基本的文件上传表单
<!-- 注意 enctype="multipart/form-data" 是必须的 -->
<form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">上传</button>
</form>
enctype="multipart/form-data" 是必须的。如果不加这个属性,文件数据不会被传输。
7.2 处理上传的文件
上传的文件信息存储在 $_FILES 超全局变量中:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$file = $_FILES['avatar'];
// 文件信息
echo "文件名:{$file['name']}<br>";
echo "文件类型:{$file['type']}<br>";
echo "文件大小:{$file['size']} 字节<br>";
echo "临时路径:{$file['tmp_name']}<br>";
echo "错误码:{$file['error']}<br>";
}
?>
$_FILES 数组的结构:
| 键名 | 含义 |
|---|---|
name |
用户电脑上的原始文件名 |
type |
文件的 MIME 类型(由浏览器提供,不可信任) |
size |
文件大小(字节) |
tmp_name |
文件在服务器上的临时存储路径 |
error |
错误码:0 表示成功,4 表示未选择文件,1/2 表示文件太大 |
7.3 安全的文件上传流程
<?php
function uploadFile(array $file, string $uploadDir = 'uploads/'): array {
$errors = [];
// 1. 检查上传是否成功
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = '文件上传失败,错误码:' . $file['error'];
return $errors;
}
// 2. 检查文件大小(限制为 5MB)
$maxSize = 5 * 1024 * 1024; // 5MB = 5 * 1024 * 1024 字节
if ($file['size'] > $maxSize) {
$errors[] = '文件太大,最大允许 5MB';
return $errors;
}
// 3. 检查文件类型(通过 MIME 类型检测,不是浏览器给的 type 字段)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimeType, $allowedTypes)) {
$errors[] = '不允许的文件类型:' . $mimeType;
return $errors;
}
// 4. 生成唯一的文件名(防止覆盖和路径遍历攻击)
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$newFilename = bin2hex(random_bytes(16)) . '.' . strtolower($extension);
$destination = rtrim($uploadDir, '/') . '/' . $newFilename;
// 5. 创建上传目录(如果不存在)
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 6. 移动文件到目标位置
if (!move_uploaded_file($file['tmp_name'], $destination)) {
$errors[] = '文件保存失败';
return $errors;
}
return ['success' => true, 'filename' => $newFilename, 'path' => $destination];
}
// 使用
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$result = uploadFile($_FILES['avatar']);
if (isset($result['success'])) {
echo "上传成功!文件保存在:{$result['path']}";
} else {
echo "上传失败:" . implode('; ', $result);
}
}
?>
文件上传安全要点:
- 不要信任
$_FILES['file']['type']——它来自浏览器,可以被伪造。用finfo扩展检测真实的 MIME 类型。 - 不要使用用户提供的文件名——可能包含
../等路径遍历字符。用random_bytes()生成随机文件名。 - 限制文件大小——防止用户上传超大文件耗尽磁盘。
- 限制文件类型——只允许必要的文件类型(如图片)。
- 把上传目录放在 Web 根目录之外(或者配置
.htaccess禁止直接执行上传的文件),防止攻击者上传 PHP 脚本并通过浏览器访问执行。 - 使用
move_uploaded_file()——而不是rename()或copy()。这个函数会检查文件是否真的是通过 HTTP 上传的。
八、综合演示:一个完整的注册页面
下面这个完整示例综合了表单验证、CSRF 防护、XSS 防御、数据库操作:
<?php
require_once 'db.php';
session_start();
// 生成 CSRF Token
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$errors = [];
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 验证 CSRF Token
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die("CSRF 验证失败!");
}
// 获取并清理输入
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
$age = trim($_POST['age'] ?? '');
// 验证
if ($name === '') {
$errors['name'] = '姓名不能为空';
} elseif (mb_strlen($name) < 2 || mb_strlen($name) > 50) {
$errors['name'] = '姓名必须在 2-50 个字符之间';
}
if ($email === '') {
$errors['email'] = '邮箱不能为空';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = '邮箱格式不正确';
}
if ($password === '') {
$errors['password'] = '密码不能为空';
} elseif (mb_strlen($password) < 6) {
$errors['password'] = '密码至少需要 6 个字符';
}
if ($password !== $password_confirm) {
$errors['password_confirm'] = '两次输入的密码不一致';
}
if ($age === '') {
$errors['age'] = '年龄不能为空';
} else {
$age = (int)$age;
if ($age < 1 || $age > 150) {
$errors['age'] = '年龄必须在 1-150 之间';
}
}
// 如果没有验证错误,检查邮箱是否已存在并插入数据库
if (empty($errors)) {
try {
$pdo = getDBConnection();
// 检查邮箱是否已注册
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = :email");
$stmt->execute(['email' => $email]);
if ($stmt->fetch()) {
$errors['email'] = '该邮箱已被注册';
} else {
// 密码哈希
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 插入用户
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, age, password) VALUES (:name, :email, :age, :password)"
);
$stmt->execute([
'name' => $name,
'email' => $email,
'age' => $age,
'password' => $hashedPassword
]);
$success = '注册成功!欢迎,' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
}
} catch (PDOException $e) {
$errors['database'] = '系统错误,请稍后重试';
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
<style>
body { font-family: sans-serif; padding: 40px; background: #f0f4f8; display: flex; justify-content: center; }
.container { background: white; padding: 40px; border-radius: 12px; width: 400px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h2 { margin-top: 0; color: #333; text-align: center; }
.form-group { margin-bottom: 16px; }
label { display: block; margin-bottom: 4px; color: #555; font-size: 14px; }
input { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
input:focus { outline: none; border-color: #4a90d9; }
.error { color: #e74c3c; font-size: 13px; margin-top: 4px; }
.success { background: #e8f5e9; color: #2e7d32; padding: 12px; border-radius: 6px; margin-bottom: 20px; text-align: center; }
button { width: 100%; padding: 12px; background: #4a90d9; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
button:hover { background: #3a7bc8; }
</style>
</head>
<body>
<div class="container">
<h2>📝 用户注册</h2>
<?php if ($success): ?>
<div class="success"><?= $success ?></div>
<?php endif; ?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="form-group">
<label>姓名</label>
<input type="text" name="name" value="<?= htmlspecialchars($name ?? '', ENT_QUOTES, 'UTF-8') ?>">
<?php if (isset($errors['name'])): ?>
<div class="error"><?= $errors['name'] ?></div>
<?php endif; ?>
</div>
<div class="form-group">
<label>邮箱</label>
<input type="email" name="email" value="<?= htmlspecialchars($email ?? '', ENT_QUOTES, 'UTF-8') ?>">
<?php if (isset($errors['email'])): ?>
<div class="error"><?= $errors['email'] ?></div>
<?php endif; ?>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password">
<?php if (isset($errors['password'])): ?>
<div class="error"><?= $errors['password'] ?></div>
<?php endif; ?>
</div>
<div class="form-group">
<label>确认密码</label>
<input type="password" name="password_confirm">
<?php if (isset($errors['password_confirm'])): ?>
<div class="error"><?= $errors['password_confirm'] ?></div>
<?php endif; ?>
</div>
<div class="form-group">
<label>年龄</label>
<input type="number" name="age" value="<?= htmlspecialchars($age ?? '', ENT_QUOTES, 'UTF-8') ?>" min="1" max="150">
<?php if (isset($errors['age'])): ?>
<div class="error"><?= $errors['age'] ?></div>
<?php endif; ?>
</div>
<button type="submit">注册</button>
</form>
</div>
</body>
</html>
代码要点总结:
- CSRF Token:每个表单都嵌入一个随机 Token,提交时验证。
- 输入验证:所有字段都做服务端验证——必填、长度、格式、范围。
- 输出转义:所有可能包含用户输入的值,输出时都用
htmlspecialchars()处理。 - 密码哈希:使用
password_hash()加密存储密码(和bcrypt相同的机制)。永远不要明文存储密码。 - 数据库安全:所有 SQL 操作使用参数化查询。
- 用户友好的错误提示:每个字段旁边显示对应的错误信息。
- 保留用户输入:表单验证失败时,把用户已输入的值回填到表单中(
value="<?= htmlspecialchars($name) ?>"),用户不需要重新填写所有内容。
九、本篇动手练习
练习 1:登录表单
新建 practice7-1.php,实现一个登录页面。包含邮箱和密码两个字段。验证邮箱格式和密码非空,从数据库查询用户并验证密码(使用 password_verify())。加上 CSRF Token 防护。
练习 2:搜索功能
新建 practice7-2.php,实现一个搜索页面。用 GET 方式提交搜索关键词,用参数化查询从数据库模糊搜索(LIKE),搜索结果用 htmlspecialchars() 安全输出,并高亮显示匹配的关键词。
练习 3:文件上传头像
新建 practice7-3.php,实现一个头像上传功能。限制文件类型为图片(JPG/PNG/GIF),大小不超过 2MB,生成随机文件名保存。上传成功后显示头像预览。
练习 4:留言板(综合练习)
新建 practice7-4.php,做一个简易留言板。包含:发表留言的表单(昵称、邮箱、内容)、留言列表的显示、CSRF Token 防护、XSS 输出转义、所有数据存入 MySQL 数据库。
十、本篇小结
这一篇你学会了 PHP 表单处理的完整技能:
- GET 和 POST 的区别:GET 用于读取数据(参数在 URL 中),POST 用于提交数据(参数在请求体中)。读取用 GET,写入用 POST。
- 接收用户输入:
$_GET接收 URL 参数,$_POST接收表单数据,$_FILES接收上传文件。$_REQUEST不推荐使用。始终用??提供默认值。 - 表单验证:服务端必须重新验证所有用户输入。必填检查(
trim()+ 空字符串判断)、格式校验(filter_var()、preg_match())、长度限制(mb_strlen())。 - XSS 防御:输出时转义,存储时保留原始数据。使用
htmlspecialchars($str, ENT_QUOTES, 'UTF-8')。 - CSRF 防御:表单中嵌入随机 Token(存在 Session 中),提交时验证。
- 文件上传安全:验证 MIME 类型(用
finfo而非浏览器提供的 type)、限制大小、生成随机文件名、使用move_uploaded_file()。
表单处理是 Web 开发中最频繁的工作之一,也是安全风险最集中的环节。把这一篇的验证、转义、Token 防护练成肌肉记忆,你的 PHP 应用就能抵御绝大多数常见的 Web 攻击。
下一篇预告
下一篇——《PHP Session 和 Cookie——记住用户状态》:HTTP 是无状态协议,服务器默认不知道两次请求是否来自同一个用户。Session 和 Cookie 解决了这个问题——Cookie 在浏览器端存储数据,Session 在服务器端存储数据。学会这两者,你就能实现登录状态保持、购物车、用户偏好设置等功能。
PHP 零基础入门,每周更新。













暂无评论内容