七:PHP 表单处理——接收用户输入与安全防护

一、回顾与本篇目标

上一篇你学会了用 PDO 连接 MySQL 数据库,实现增删改查和事务。现在你的 PHP 程序能把数据持久化到数据库中了。

但有一个关键问题还没解决:数据从哪来?之前的示例中,数据要么是写死在代码里的,要么是手动改 URL 传的。一个真正的 Web 应用需要让用户通过表单提交数据——注册账号、发表评论、上传文件、搜索商品——这些都是通过表单完成的。

表单处理是 Web 开发中最核心的交互模式。但同时,它也是最危险的环节——用户输入的数据可能包含恶意代码(XSS 攻击)、精心构造的 SQL 语句(SQL 注入)、伪造的请求(CSRF 攻击)。处理好用户输入,是后端开发的基本功和底线。

本篇的目标:

  1. 理解 GET 和 POST 两种请求方式的区别和适用场景
  2. 学会用 $_GET$_POST$_REQUEST 接收用户数据
  3. 掌握表单验证的完整流程:必填检查、格式校验、长度限制
  4. 学会防御 XSS 攻击——永远不要信任用户输入
  5. 理解 CSRF 攻击的基本原理和防御策略
  6. 学会文件上传的处理

二、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_REFERERHTTP_USER_AGENT 来自客户端,可以被伪造。不要依赖它们做安全相关的判断。

四、表单数据验证

用户输入永远不可信任。浏览器端的表单验证(requiredtype="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。

哪些字符会被转义:

字符 转义后
< &lt;
> &gt;
" &quot;
' &#039;
& &amp;

最佳实践:在输出时转义,而不是在存储时转义。数据库里存原始数据,显示的时候再转义——这样数据可以灵活地用于不同场景(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 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容