从零开始学 PHP 系列(五):Web 表单处理与文件上传——让网站“活”起来

摘要 :前几篇我们掌握了 PHP 的语法、流程控制、函数和数组,但一直是在"自言自语"。真正的 Web 应用需要与用户互动:用户填写注册信息、提交评论、上传头像,服务器接收并处理这些数据,再返回个性化页面。这一切的核心就是 表单请求数据。本篇将带你系统地学习 HTML 表单与 PHP 的交互方式,深入理解 GET 与 POST 的区别,手把手教你如何安全地接收、验证和过滤用户输入,抵御 XSS 和 CSRF 攻击。接着,你会学到文件上传的完整流程------从前端表单到后端校验,再到安全存储。最后,我们揭开 Cookie 和 Session 的面纱,实现用户登录状态的保持。学完本篇,你将能够独立开发带有用户交互功能的动态网站,安全意识和技能都会大幅提升。


一、引言:从"我写你看"到"你来我往"

回顾之前的例子,我们的 PHP 脚本只是被动地输出内容,像一块固定的电子公告板。但真实的网站------比如微博、淘宝、知乎------核心是 交互:你输入内容,点击按钮,服务器处理并反馈结果。

这个交互的起点就是 HTML 表单。当你在搜索框里输入关键词点击"搜索",或者在登录框填写用户名密码点击"登录",背后都有一套数据传递的机制。而 PHP 作为服务器端的语言,正是负责接收、处理这些数据的主角。


二、HTTP 请求方法:GET 与 POST 的本质区别

2.1 HTTP 协议简览

浏览器与服务器通信使用 HTTP 协议。一个 HTTP 请求包含:请求方法 (如 GET、POST)、URL头信息 (Headers),以及可选的 请求体(Body)。服务器返回状态码、头和响应体(HTML 等)。

表单提交数据的核心方法是 GETPOST

2.2 GET 方法:数据附在 URL 上

GET 请求将表单数据经过 URL 编码后附加在 URL 后面,以 ? 开头,多个参数用 & 分隔。

示例 URL:

复制代码
http://localhost/search.php?keyword=php&page=2

特点

  • 数据可见于 URL,可被收藏、分享、缓存。

  • 长度受限(通常 2048 字符以内)。

  • 用于获取数据、查询、导航等不改变服务器状态的操作

  • 不要用于提交敏感信息(密码、身份证等)。

2.3 POST 方法:数据放在请求体中

POST 请求将数据放在请求体中发送,URL 上不可见。

特点

  • 数据不可见于 URL(但未加密,抓包仍可见,HTTPS 才是加密关键)。

  • 长度无限制(服务器可配置限制)。

  • 用于创建、修改数据,提交表单(注册、登录、订单等)

  • 不会像 GET 那样被缓存或保存到历史记录。

2.4 在 PHP 中接收 GET 和 POST 数据

php 复制代码
<?php
// 对于 GET 请求,数据存放在 $_GET 超全局数组
// 对于 POST 请求,数据存放在 $_POST 超全局数组
​
$keyword = $_GET['keyword'] ?? '';    // 获取 GET 参数 keyword
$username = $_POST['username'] ?? ''; // 获取 POST 参数 username
?>

$_REQUEST 包含了 $_GET$_POST$_COOKIE 的数据,不推荐使用,因为来源不明易导致安全问题。


三、表单数据处理实战

3.1 创建第一个表单(GET 方式)

search.html

html 复制代码
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>搜索</title></head>
<body>
    <form action="search.php" method="get">
        <input type="text" name="keyword" placeholder="请输入关键词">
        <button type="submit">搜索</button>
    </form>
</body>
</html>

search.php

php 复制代码
<?php
// 获取GET传参,无值则为空字符串
$keyword = $_GET['keyword'] ?? '';

// 去除首尾空白
$keyword = trim($keyword);

if ($keyword !== '') {
    // htmlspecialchars 防止XSS输出转义
    echo "你搜索了:" . htmlspecialchars($keyword, ENT_QUOTES);
} else {
    echo "请输入关键词";
}
?>

访问 search.html,输入内容提交,URL 会变成类似 search.php?keyword=hello,页面显示搜索内容。

3.2 POST 方式表单(更常用)

login.php(表单和处理合在一起,推荐方式):

php 复制代码
<?php
$error = '';
$username = '';

// 判断是否POST提交表单
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 获取并清理输入
    $username = trim($_POST['username'] ?? '');
    $password = trim($_POST['password'] ?? '');

    // 简单非空校验
    if (empty($username)) {
        $error = '用户名不能为空';
    } elseif (empty($password)) {
        $error = '密码不能为空';
    } elseif ($username === 'admin' && $password === '123456') {
        // 登录成功
        echo '<div style="color:#009944;font-size:18px;">登录成功!欢迎回来,' . htmlspecialchars($username, ENT_QUOTES) . '</div>';
        exit;
    } else {
        $error = '用户名或密码错误';
    }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            background: #f5f7fa;
            display: flex;
            justify-content: center;
            padding-top: 80px;
            margin: 0;
        }
        .login-box {
            background: #fff;
            padding: 30px 40px;
            border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.1);
            width: 320px;
        }
        h2 {
            text-align: center;
            color: #333;
            margin-top: 0;
        }
        .err-tip {
            color: #e53e3e;
            text-align: center;
            margin: 8px 0 16px;
        }
        label {
            display: block;
            margin: 12px 0 4px;
            color: #444;
        }
        input {
            width: 100%;
            box-sizing: border-box;
            padding: 9px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 15px;
        }
        button {
            width: 100%;
            margin-top: 20px;
            padding: 10px;
            background: #3490dc;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
        }
        button:hover {
            background: #2779bd;
        }
    </style>
</head>
<body>
<div class="login-box">
    <h2>账号登录</h2>
    <?php if (!empty($error)): ?>
        <p class="err-tip"><?= htmlspecialchars($error, ENT_QUOTES) ?></p>
    <?php endif; ?>

    <form method="post">
        <label>用户名</label>
        <input type="text" name="username" value="<?= htmlspecialchars($username, ENT_QUOTES) ?>" placeholder="请输入用户名">

        <label>密码</label>
        <input type="password" name="password" placeholder="请输入密码">

        <button type="submit">立即登录</button>
    </form>
</div>
</body>
</html>

说明

  • $_SERVER['REQUEST_METHOD'] 判断请求方法,区分"首次展示页面"和"提交后处理"。

  • 表单 action 留空则提交到当前页面,处理逻辑写在顶部。

  • 使用 htmlspecialchars 输出防止 XSS。

3.3 多值字段的处理(复选框、下拉多选)

复选框和多个选择框的名字以 [] 结尾,PHP 会将其组织为数组。

php 复制代码
<!-- hobby.html -->
<form action="hobby.php" method="post">
    <label><input type="checkbox" name="hobby[]" value="reading"> 阅读</label>
    <label><input type="checkbox" name="hobby[]" value="sports"> 运动</label>
    <label><input type="checkbox" name="hobby[]" value="music"> 音乐</label>
    <button type="submit">提交</button>
</form>
// hobby.php
$hobbies = $_POST['hobby'] ?? [];
if ($hobbies) {
    echo "你的爱好:" . implode(", ", array_map('htmlspecialchars', $hobbies));
} else {
    echo "你没有选择任何爱好。";
}

同样适用于 <select multiple>

3.4 隐藏域与按钮值

php 复制代码
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="123">
<button type="submit" name="submit" value="save">保存</button>

隐藏域用于传递页面状态而不让用户看到。按钮可以有 name 和 value,点击后也会出现在 $_POST 中,用于判断哪个按钮被点击。


四、数据验证与过滤:永远不要信任用户输入

4.1 为什么要验证?

黑客可以通过构造恶意输入攻击你的网站(SQL 注入、XSS、文件包含等)。前端验证仅仅是用户体验,安全验证必须在后端

4.2 常用验证方法

php 复制代码
<?php
// 1. 必填项检查
if (empty($_POST['username'])) {
    die("用户名不能为空");
}
​
// 2. 长度限制
if (strlen($username) < 3 || strlen($username) > 20) {
    die("用户名长度应在 3-20 字符之间");
}
​
// 3. 格式验证(正则)
$email = $_POST['email'];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    die("邮箱格式不正确");
}
​
// 4. 数字范围
$age = (int)$_POST['age'];
if ($age < 1 || $age > 150) {
    die("年龄不合法");
}
​
// 5. 白名单验证(枚举值)
$allowed_roles = ['admin', 'user', 'guest'];
if (!in_array($_POST['role'], $allowed_roles)) {
    die("角色非法");
}
?>

4.3 使用 filter_var 过滤与验证

PHP 内置的 filter_* 函数非常强大,既能验证也能清理。

常用过滤器

  • FILTER_VALIDATE_EMAIL:验证邮箱

  • FILTER_VALIDATE_URL:验证 URL

  • FILTER_VALIDATE_INT:验证整数(可带范围选项)

  • FILTER_VALIDATE_FLOAT:浮点数

  • FILTER_VALIDATE_BOOLEAN:布尔

  • FILTER_SANITIZE_EMAIL:清理邮箱(移除非法字符)

  • FILTER_SANITIZE_STRING(已弃用,但可用 htmlspecialcharsstrip_tags

  • FILTER_SANITIZE_NUMBER_INT:只保留数字和加减号

php 复制代码
<?php
$email = "bad@email<script>";
$clean = filter_var($email, FILTER_SANITIZE_EMAIL);
echo $clean; // bad@email (移除了非法字符)
?>

4.4 综合验证示例

php 复制代码
<?php
/**
 * 注册表单数据校验函数
 * @param array $data 表单提交数据数组
 * @return array 错误信息数组,键为字段名,值为错误提示
 */
function validateRegistration(array $data): array
{
    $errors = [];

    // 统一提取并清洗字段,不存在则设为空字符串
    $username = trim($data['username'] ?? '');
    $password = $data['password'] ?? '';
    $password2 = $data['password2'] ?? '';
    $email = trim($data['email'] ?? '');

    // 用户名校验:非空且长度≥3(兼容中文)
    if (empty($username) || mb_strlen($username) < 3) {
        $errors['username'] = '用户名至少3个字符';
    }

    // 密码校验
    if (empty($password) || strlen($password) < 6) {
        $errors['password'] = '密码至少6位';
    } else {
        // 仅当密码合法时,校验两次密码是否一致
        if ($password !== $password2) {
            $errors['password2'] = '两次密码不一致';
        }
    }

    // 邮箱校验
    if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = '邮箱格式不正确';
    }

    return $errors;
}
?>

若有错误,则返回给用户并提示。


五、安全防线:XSS 与 CSRF 攻防

5.1 XSS(跨站脚本攻击)

攻击者提交包含恶意 JavaScript 的输入,当该内容被展示给其他用户时,脚本执行,可窃取 Cookie、会话、重定向等。

示例 :用户在评论框输入 <script>alert('XSS')</script>,若不处理直接输出,其他用户打开评论页即弹窗。

防护

  • 输出时使用 htmlspecialchars($str, ENT_QUOTES, 'UTF-8') 编码特殊字符。

  • 永远不要直接 echo $_GET['...']

  • 设置 Content-Security-Policy 头。

5.2 CSRF(跨站请求伪造)

攻击者诱导用户点击一个链接或加载一个图片,该请求携带用户已登录的 Cookie,冒充用户执行操作(如删除文章、转账)。

防护

  • 关键操作使用 POST 而非 GET。

  • 添加 CSRF Token:生成随机令牌存储在 Session,表单中放入隐藏域,提交时比对。

  • 使用 SameSite Cookie 属性。

  • 检查 Referer/Origin 头(不绝对可靠)。

CSRF Token 简单实现

php 复制代码
<?php
session_start();
​
// 生成 Token
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
​
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $token)) {
        die('CSRF 验证失败');
    }
    // 处理表单...
}
?>
<!-- 表单中 -->
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">

hash_equals 防止时序攻击,比较字符串用。


六、文件上传:让用户可以上传头像

6.1 文件上传的前提条件

  • 表单 enctype 必须为 multipart/form-data

  • 上传方法用 POST。

  • PHP 配置 php.inifile_uploads = On,调整 upload_max_filesizepost_max_size

6.2 前端表单

php 复制代码
<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <button type="submit">上传头像</button>
</form>

6.3 后端接收与处理

上传的文件信息保存在 $_FILES['avatar'] 中,是一个数组:

  • name:原始文件名

  • type:MIME 类型(浏览器提供,不可信)

  • size:文件大小(字节)

  • tmp_name:临时文件路径

  • error:错误代码(0 表示成功)

php 复制代码
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
    $file = $_FILES['avatar'];
    
    // 检查错误
    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("上传失败,错误代码: " . $file['error']);
    }
    
    // 验证文件大小(例如最大 2MB)
    $maxSize = 2 * 1024 * 1024;
    if ($file['size'] > $maxSize) {
        die("文件太大,不能超过 2MB");
    }
    
    // 验证 MIME 类型(实际检查文件内容更安全,这里用简易方式)
    $allowedMime = ['image/jpeg', 'image/png', 'image/gif'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);
    if (!in_array($mime, $allowedMime)) {
        die("只允许上传 JPG/PNG/GIF 图片");
    }
    
    // 生成唯一文件名,防止冲突和路径穿越
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $newName = md5(uniqid() . time()) . '.' . $ext;
    $destination = __DIR__ . '/uploads/' . $newName;
    
    // 移动文件
    if (move_uploaded_file($file['tmp_name'], $destination)) {
        echo "上传成功!文件路径:uploads/$newName";
    } else {
        die("移动文件失败,请检查目录权限");
    }
}
?>

关键安全要点

  • 永远不信任 $_FILES['file']['type'],用 Fileinfo 库检查真实 MIME。

  • 检查文件扩展名,但不要仅依赖扩展名。

  • 上传目录不要有执行权限,最好放在文档根目录之外。

  • 用随机文件名存储,避免覆盖和路径遍历攻击(如 ../../etc/passwd)。

  • 检查文件大小,服务器配置限制外再加应用层限制。

6.4 多文件上传

在表单中使用 name="photos[]" 并添加 multiple 属性,$_FILES['photos'] 结构会变化(nametype 等均为数组),需要循环处理。使用 array() 方式组织文件表单数据更易遍历。

php 复制代码
// 遍历
foreach ($_FILES['photos']['error'] as $key => $error) {
    if ($error === UPLOAD_ERR_OK) {
        // 使用对应 key 的 tmp_name, name, size 等
    }
}

七、Cookie 与 Session:记住用户的状态

HTTP 是无状态协议,每次请求都是独立的。如何让网站"记住"你已经登录?这就需要 CookieSession

7.1 Cookie:客户端的小纸条

Cookie 是服务器让浏览器保存在用户电脑上的小文本数据(通常 4KB 以内),下次请求同域名时浏览器自动发送。

设置 Cookie

php 复制代码
<?php
setcookie('username', '张三', time() + 3600, '/'); // 有效期 1 小时
// 参数:名称, 值, 过期时间, 路径, 域, 安全, HttpOnly
?>

读取 Cookie

php 复制代码
<?php
echo $_COOKIE['username'] ?? '未设置';
?>

删除 Cookie:将过期时间设为过去。

php 复制代码
<?php
setcookie('username', '', time() - 3600);
?>

安全实践

  • 通过 HttpOnly 标志禁止 JavaScript 读取,防止 XSS 窃取。

  • 通过 Secure 标志仅允许 HTTPS 传输。

  • 设置 SameSite 属性防御 CSRF(Lax/Strict)。

7.2 Session:服务器端的状态保存

Session 将数据保存在服务器端,仅给浏览器一个唯一的 Session ID(存在 Cookie 中)。用户每次请求带上这个 ID,服务器查找对应的数据,实现状态保持。

启动 Session

php 复制代码
<?php
session_start(); // 必须放在输出任何内容之前
?>

存取数据

php 复制代码
<?php
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'admin';
echo "欢迎 " . $_SESSION['username'];
?>

销毁 Session(退出登录):

php 复制代码
<?php
session_start();
$_SESSION = []; // 清空数组
// 清除 Cookie 中的 Session ID
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]);
}
session_destroy();
?>

配置 Session(php.ini):

  • session.gc_maxlifetime:垃圾回收最大生命周期(默认 1440 秒)。

  • session.save_path:存放位置。

  • session.cookie_httponly = On

  • session.cookie_secure = On(HTTPS 时)

  • session.use_strict_mode = 1(防止 Session 固定攻击)

7.3 实战:简易登录与页面保护

login_session.php 登录页

php 复制代码
<?php
// 必须放在最顶部,前面不能有任何输出
session_start();

$error = '';
$username = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['user'] ?? '');
    $pass = trim($_POST['pass'] ?? '');

    // 简单非空校验
    if ($username === '' || $pass === '') {
        $error = '用户名和密码不能为空';
    } elseif ($username === 'admin' && $pass === '123') {
        // 登录成功,写入会话
        $_SESSION['logged_in'] = true;
        $_SESSION['username'] = $username;
        $_SESSION['login_time'] = time();

        // 跳转后台,302重定向
        header('Location: dashboard.php', true, 302);
        exit;
    } else {
        $error = '用户名或密码错误';
    }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>登录验证</title>
    <style>
        body {
            font-family: system-ui;
            max-width: 320px;
            margin: 60px auto;
        }
        .err {
            color: #dc3545;
            background: #ffebee;
            padding: 8px;
            border-radius: 4px;
        }
        div {
            margin: 12px 0;
        }
        input {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
        }
        button {
            width: 100%;
            padding: 10px;
            background: #0d6efd;
            color: #fff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h2>系统登录</h2>
    <?php if ($error !== ''): ?>
        <p class="err"><?= htmlspecialchars($error, ENT_QUOTES) ?></p>
    <?php endif; ?>

    <form method="post">
        <div>
            <label>用户名</label>
            <input type="text" name="user" value="<?= htmlspecialchars($username, ENT_QUOTES) ?>" placeholder="admin">
        </div>
        <div>
            <label>密码</label>
            <input type="password" name="pass" placeholder="123">
        </div>
        <div>
            <button type="submit">登录</button>
        </div>
    </form>
</body>
</html>

dashboard.php 受保护后台页面

php 复制代码
<?php
session_start();

// 校验登录状态,未登录强制跳转登录页
if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
    header('Location: login_session.php', true, 302);
    exit;
}

// 安全取出用户名
$user = htmlspecialchars($_SESSION['username'] ?? '', ENT_QUOTES);
$loginTime = date('Y-m-d H:i:s', $_SESSION['login_time']);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>管理后台</title>
    <style>
        body { font-family: system-ui; padding: 40px; }
        .box {
            border: 1px solid #eee;
            padding: 30px;
            border-radius: 8px;
            max-width: 400px;
        }
        a {
            display: inline-block;
            margin-top: 20px;
            padding: 8px 16px;
            background: #6c757d;
            color: white;
            text-decoration: none;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div class="box">
        <h2>欢迎,<?= $user ?>!</h2>
        <p>登录时间:<?= $loginTime ?></p>
        <p>当前页面受会话保护,未登录无法访问</p>
        <a href="logout.php">安全退出登录</a>
    </div>
</body>
</html>

logout.php 完整安全登出

php 复制代码
<?php
session_start();

// 1. 清空当前会话数据
$_SESSION = [];

// 2. 清除客户端session cookie
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(
        session_name(),
        '',
        time() - 3600,
        $params["path"],
        $params["domain"],
        $params["secure"],
        $params["httponly"]
    );
}

// 3. 销毁服务器端会话文件
session_destroy();

// 跳转登录页
header('Location: login_session.php', true, 302);
exit;
?>

八、综合实战:带验证的注册页

让我们将本篇所有知识整合,创建一个较为完整的注册系统:包含表单验证、头像上传、密码哈希(安全性)、Session 登录持久化、CSRF 保护。

register.php

php 复制代码
<?php
// 页面最顶部,无任何前置输出
session_start();
// 安全响应头,防止会话劫持、XSS
header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");

// 自动生成CSRF令牌
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$errors = [];
$input = [
    'username' => '',
    'email' => '',
];

// 上传目录自动创建
$uploadDir = __DIR__ . '/uploads/';
if (!file_exists($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}
// 用户存储文件不存在则初始化空数组
$userFile = __DIR__ . '/users.json';
if (!file_exists($userFile)) {
    file_put_contents($userFile, json_encode([], JSON_PRETTY_PRINT));
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 1. CSRF 防护校验
    $postCsrf = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $postCsrf)) {
        $errors['global'] = '非法提交请求,禁止重复提交或跨站攻击';
    }

    // 2. 获取并清洗所有输入
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    $password2 = $_POST['password2'] ?? '';
    $email = trim($_POST['email'] ?? '');
    // 回填输入框
    $input['username'] = $username;
    $input['email'] = $email;

    // 3. 表单基础校验
    if (empty($username)) {
        $errors['username'] = '用户名不能为空';
    } elseif (mb_strlen($username) < 3) {
        $errors['username'] = '用户名至少3个字符';
    }

    if (empty($password)) {
        $errors['password'] = '密码不能为空';
    } elseif (strlen($password) < 6) {
        $errors['password'] = '密码长度至少6位';
    }

    if ($password !== $password2) {
        $errors['password2'] = '两次输入的密码不一致';
    }

    if (empty($email)) {
        $errors['email'] = '邮箱不能为空';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = '邮箱格式不合法';
    }

    // 4. 校验用户名/邮箱是否已被注册
    $userList = json_decode(file_get_contents($userFile), true) ?? [];
    foreach ($userList as $user) {
        if ($user['username'] === $username) {
            $errors['username'] = '该用户名已被占用';
        }
        if ($user['email'] === $email) {
            $errors['email'] = '该邮箱已注册账号';
        }
    }

    // 5. 头像上传处理
    $avatarPath = '';
    if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {
        $file = $_FILES['avatar'];
        // 上传错误判断
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors['avatar'] = '文件上传失败,请重新选择';
        } else {
            // MIME类型校验
            $finfo = new finfo(FILEINFO_MIME_TYPE);
            $mime = $finfo->file($file['tmp_name']);
            $allowMime = ['image/jpeg', 'image/png'];
            if (!in_array($mime, $allowMime)) {
                $errors['avatar'] = '仅支持 JPG / PNG 图片头像';
            } elseif ($file['size'] > 1048576) {
                $errors['avatar'] = '头像文件不能超过 1MB';
            } else {
                // 生成唯一文件名,防止覆盖
                $ext = $mime === 'image/png' ? 'png' : 'jpg';
                $fileName = md5(uniqid(microtime(true), true) . $username) . '.' . $ext;
                $destPath = $uploadDir . $fileName;
                if (move_uploaded_file($file['tmp_name'], $destPath)) {
                    $avatarPath = 'uploads/' . $fileName;
                } else {
                    $errors['avatar'] = '文件写入失败,目录权限不足';
                }
            }
        }
    }

    // 6. 无错误则写入用户数据并自动登录
    if (empty($errors)) {
        $newUser = [
            'username' => $username,
            'password' => password_hash($password, PASSWORD_DEFAULT),
            'email'    => $email,
            'avatar'   => $avatarPath,
            'reg_time' => time()
        ];
        $userList[] = $newUser;
        file_put_contents($userFile, json_encode($userList, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

        // 写入会话,自动登录
        $_SESSION['logged_in'] = true;
        $_SESSION['username']  = $username;
        $_SESSION['avatar']    = $avatarPath;

        // 跳转后台,终止脚本
        header('Location: dashboard.php', true, 302);
        exit;
    }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>用户注册</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: "Microsoft YaHei", system-ui;
        }
        body {
            background: #f5f7fa;
            padding: 60px 20px;
        }
        .register-card {
            max-width: 420px;
            margin: 0 auto;
            background: #fff;
            padding: 32px;
            border-radius: 10px;
            box-shadow: 0 2px 14px rgba(0,0,0,0.08);
        }
        h1 {
            text-align: center;
            color: #2d3748;
            margin-bottom: 24px;
            font-size: 22px;
        }
        .global-err {
            background: #fee;
            color: #dc2626;
            padding: 10px;
            border-radius: 6px;
            margin-bottom: 16px;
            text-align: center;
        }
        .form-item {
            margin-bottom: 16px;
        }
        label {
            display: block;
            margin-bottom: 6px;
            color: #4a5568;
        }
        input[type="text"],
        input[type="password"],
        input[type="email"],
        input[type="file"] {
            width: 100%;
            padding: 10px 12px;
            border: 1px solid #cbd5e0;
            border-radius: 6px;
            font-size: 15px;
        }
        .err-text {
            color: #dc2626;
            font-size: 13px;
            margin-top: 4px;
            display: block;
        }
        button {
            width: 100%;
            padding: 11px;
            background: #2563eb;
            color: #fff;
            border: none;
            border-radius: 6px;
            font-size: 16px;
            cursor: pointer;
            transition: background 0.2s;
        }
        button:hover {
            background: #1d4ed8;
        }
    </style>
</head>
<body>
    <div class="register-card">
        <h1>新用户注册</h1>

        <?php if (!empty($errors['global'])): ?>
            <div class="global-err">
                <?= htmlspecialchars($errors['global'], ENT_QUOTES) ?>
            </div>
        <?php endif; ?>

        <form method="POST" enctype="multipart/form-data">
            <!-- CSRF 隐藏令牌 -->
            <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES) ?>">

            <div class="form-item">
                <label>用户名</label>
                <input type="text" name="username" value="<?= htmlspecialchars($input['username'], ENT_QUOTES) ?>" placeholder="至少3个字符">
                <?php if (!empty($errors['username'])): ?>
                    <span class="err-text"><?= htmlspecialchars($errors['username'], ENT_QUOTES) ?></span>
                <?php endif; ?>
            </div>

            <div class="form-item">
                <label>登录密码</label>
                <input type="password" name="password" placeholder="最少6位">
                <?php if (!empty($errors['password'])): ?>
                    <span class="err-text"><?= htmlspecialchars($errors['password'], ENT_QUOTES) ?></span>
                <?php endif; ?>
            </div>

            <div class="form-item">
                <label>确认密码</label>
                <input type="password" name="password2" placeholder="再次输入密码">
                <?php if (!empty($errors['password2'])): ?>
                    <span class="err-text"><?= htmlspecialchars($errors['password2'], ENT_QUOTES) ?></span>
                <?php endif; ?>
            </div>

            <div class="form-item">
                <label>邮箱地址</label>
                <input type="email" name="email" value="<?= htmlspecialchars($input['email'], ENT_QUOTES) ?>" placeholder="example@shturl.">
                <?php if (!empty($errors['email'])): ?>
                    <span class="err-text"><?= htmlspecialchars($errors['email'], ENT_QUOTES) ?></span>
                <?php endif; ?>
            </div>

            <div class="form-item">
                <label>上传头像(选填,1MB内 JPG/PNG)</label>
                <input type="file" name="avatar" accept="image/jpeg,image/png">
                <?php if (!empty($errors['avatar'])): ?>
                    <span class="err-text"><?= htmlspecialchars($errors['avatar'], ENT_QUOTES) ?></span>
                <?php endif; ?>
            </div>

            <button type="submit">立即注册</button>
        </form>
    </div>
</body>
</html>

核心亮点

  • 使用了 password_hash 安全存储密码。

  • CSRF Token 验证。

  • 文件真实 MIME 检测。

  • htmlspecialchars 输出防止 XSS。

  • 自动登录并跳转到 dashboard(需自己创建 dashboard.php 展示欢迎信息和头像)。

dashboard.php

php 复制代码
<?php
session_start();
// 未登录跳转注册页
if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
    header('Location: register.php', true, 302);
    exit;
}
$userName = htmlspecialchars($_SESSION['username'], ENT_QUOTES);
$avatar = $_SESSION['avatar'] ?? '';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>个人中心</title>
    <style>
        body { padding: 50px; text-align: center; }
        .avatar { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 2px solid #2563eb; }
        .no-avatar { width: 120px; height: 120px; background: #eee; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size:14px; color:#666; }
        a { display: inline-block; margin-top:20px; padding:8px 16px; background:#666; color:#fff; text-decoration:none; border-radius:4px; }
    </style>
</head>
<body>
    <h2>欢迎你,<?= $userName ?></h2>
    <div style="margin:20px 0;">
        <?php if (!empty($avatar) && file_exists($avatar)): ?>
            <img class="avatar" src="<?= htmlspecialchars($avatar, ENT_QUOTES) ?>">
        <?php else: ?>
            <div class="no-avatar">暂无头像</div>
        <?php endif; ?>
    </div>
    <a href="logout.php">退出登录</a>
</body>
</html>

logout.php

php 复制代码
<?php
session_start();
// 清空会话
$_SESSION = [];
// 销毁Cookie
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 86400,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}
session_destroy();
header('Location: register.php', true, 302);
exit;
?>

九、总结

  • GET 与 POST :掌握了它们的区别和适用场景,通过 $_GET$_POST 接收数据。

  • 表单处理:学会了创建表单、多值字段、隐藏域,以及如何在同一页面完成展示与处理。

  • 数据验证与过滤 :掌握了 filter_var 和自定义验证函数,知道后端验证才是安全的根基。

  • 安全攻防 :理解了 XSS 的原理与 htmlspecialchars 防御,了解了 CSRF 及 Token 防御机制。

  • 文件上传:完整实现了从前端到后端的文件上传,包括错误检查、类型校验、唯一命名和安全存储。

  • Cookie 与 Session:理解了 HTTP 无状态以及如何通过它们维持用户状态,实现了登录、退出及页面保护。

  • 综合案例:注册系统融合了以上全部知识,让你看到一个真实开发流程的缩影。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

相关推荐
凡人叶枫2 小时前
Effective C++ 条款41:了解隐式接口和编译期多态
java·开发语言·c++·effective c++
2601_954706492 小时前
云手机技术详解+Python实战调用|2026高稳云手机平台推荐
开发语言·python·智能手机
chushiyunen2 小时前
java中的路径处理、左右斜杠
java·开发语言·python
重生之后端学习3 小时前
Java入门
java·开发语言·职场和发展
碧海蓝天20223 小时前
C++法则24:在标准 C++ 中,没有任何可移植的方式判断指针 T* pt 指向的内存位置是否已经 构造了对象,程序员必须手动跟踪哪些元素已构造。
java·开发语言·c++
代码不加糖3 小时前
Proxy能够监听到对象中的对象的引用吗?
开发语言·前端·javascript
charlie1145141913 小时前
现代C++指南:Lambda,让我们用另一种方式持有函数
开发语言·c++
qq3621967054 小时前
阿里裁员新消息(2026最新动态汇总)
java·开发语言·前端
酉鬼女又兒4 小时前
零基础入门计算机网络运输层:端到端通信核心作用、端口号分类规则、复用分用工作机制及UDP与TCP协议全方位对比详解
网络·网络协议·tcp/ip·计算机网络·考研·udp·php
.千余4 小时前
【C++】模板进阶全解:非类型参数|全特化|偏特化|分离编译完全指南
开发语言·c++·笔记·学习·其他