
资料在文章末尾
PHP 入门第五篇。前几篇都在写脚本输出内容,这篇开始真正和用户打交道——表单怎么提交、数据怎么收、怎么验证、怎么防攻击、文件怎么上传、登录状态怎么保持。一篇讲透 Web 交互的核心,附完整可运行代码。
前几篇的 PHP 脚本,本质都是"自己说给自己听"——脚本写死什么,用户就看到什么。但真实网站不是这样:你在搜索框敲关键词点搜索,在登录页填账号密码点登录,背后都是浏览器把数据发给服务器,PHP 接收、处理、再返回结果。
这套数据传递机制的入口,就是 HTML 表单。PHP 作为服务端语言,负责接收这些数据并做处理。这篇会从最基础的 GET/POST 讲起,一直到一个带验证、防攻击、文件上传、Session 持久化的完整注册系统。
浏览器和服务器之间用 HTTP 协议通信。一个 HTTP 请求包含:请求方法(GET、POST 等)、URL、头信息,以及可选的请求体。服务器返回状态码、头和响应内容(通常是 HTML)。
表单提交数据主要用两种方法:GET 和 POST。两者的区别不是"一个安全一个不安全"那么简单,本质区别在于数据放在哪里传输。
GET 请求把表单数据经过 URL 编码后附加在 URL 后面,以 ? 开头,多个参数用 & 分隔。比如:
http://localhost/search.php?keyword=php&page=2GET 的特点:
• 数据出现在 URL 上,可以被收藏、分享、缓存• URL 长度有限制(通常 2048 字符以内)• 适合"获取数据"的操作——查询、搜索、分页导航• 不要用来提交敏感信息(密码、身份证号这些绝对不能放 URL 里)
POST 请求把数据放在 HTTP 请求体中发送,URL 上看不到数据内容。
POST 的特点:
• 数据不在 URL 上(但没加密,抓包照样能看到,要加密得靠 HTTPS)• 长度基本没限制(服务器可以配置上限)• 适合"修改数据"的操作——注册、登录、提交订单、发布文章• 不会被浏览器缓存或存进历史记录
实战提醒:很多人以为 POST 比 GET 安全,其实只是"不在 URL 上显示"。要真正安全传输必须上 HTTPS。GET 和 POST 的本质区别是语义不同——GET 用于读取,POST 用于写入。
PHP 提供了两个超全局数组来接收表单数据:
<?php// GET 请求的数据存放在 $_GET 超全局数组// POST 请求的数据存放在 $_POST 超全局数组$keyword = $_GET['keyword'] ?? ''; // 获取 GET 参数 keyword$username = $_POST['username'] ?? ''; // 获取 POST 参数 username?>这里用 ?? 空合并运算符兜底,避免参数不存在时报警告。还有一个 $_REQUEST 数组包含了 GET、POST、Cookie 的所有数据,但不推荐用——分不清数据来源容易出安全问题。
先写一个最简单的搜索表单。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>PHP 处理部分(search.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,页面显示搜索内容。


必踩的坑:输出用户输入的内容时,一定要用 htmlspecialchars 转义。否则用户输入 <script>alert(1)</script> 会被当成 HTML 执行,这就是 XSS 漏洞。
实际开发中更常用 POST。把表单和处理逻辑写在同一个文件里是推荐做法——方便维护,逻辑集中。下面是一个完整的登录页示例:
<?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>登录成功!欢迎回来,' . htmlspecialchars($username, ENT_QUOTES) . '</div>'; exit; } else { $error = '用户名或密码错误'; }}?><form method="post"> <input type="text" name="username" value="<?= htmlspecialchars($username, ENT_QUOTES) ?>"> <input type="password" name="password"> <button type="submit">登录</button></form>
关键点:用 $_SERVER['REQUEST_METHOD'] 区分"首次打开页面"和"提交表单后处理"。表单 action 留空表示提交到当前页面,处理逻辑写在文件顶部。
复选框和下拉多选这类多值字段,name 属性要以 [] 结尾,PHP 才会把它们组织成数组:
<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><?php// hobby.php$hobbies = $_POST['hobby'] ?? [];if ($hobbies) { echo "你的爱好:" . implode(", ", array_map('htmlspecialchars', $hobbies));} else { echo "你没有选择任何爱好。";}?><select multiple> 多选下拉也是同样的处理方式。
<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 里,可以用来判断"用户点了哪个按钮"。
攻击者可以绕过前端直接构造 HTTP 请求——用 curl、Postman、甚至改浏览器请求。所以前端验证只是为了用户体验,真正的安全验证必须在后端做。
<?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("角色非法");}?>PHP 内置的 filter_* 系列函数很强大,既能验证也能清理数据:
• FILTER_VALIDATE_EMAIL:验证邮箱格式• FILTER_VALIDATE_URL:验证 URL 格式• FILTER_VALIDATE_INT:验证整数(可带范围)• FILTER_VALIDATE_FLOAT:浮点数验证• FILTER_SANITIZE_EMAIL:清理邮箱(移除非法字符)• FILTER_SANITIZE_NUMBER_INT:只保留数字和加减号
<?php$email = "bad@email<script>";$clean = filter_var($email, FILTER_SANITIZE_EMAIL);echo $clean; // 输出 bad@email(移除了 <script>)?>实际项目里建议把校验逻辑封装成函数,返回错误信息数组,前端逐字段展示:
<?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(兼容中文用 mb_strlen) if (empty($username) || mb_strlen($username) < 3) { $errors['username'] = '用户名至少3个字符'; } // 密码:长度 + 两次一致 if (empty($password) || strlen($password) < 6) { $errors['password'] = '密码至少6位'; } elseif ($password !== $password2) { $errors['password2'] = '两次密码不一致'; } // 邮箱格式 if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors['email'] = '邮箱格式不正确'; } return $errors;}?>攻击者在输入框里塞一段恶意 JavaScript,你不处理直接输出,其他用户打开这个页面时脚本就会执行——可以偷 Cookie、伪造操作、跳转到钓鱼网站。
比如有人在评论框输入 <script>alert('XSS')</script>,如果你的页面直接 echo 出来,所有人打开评论页都会弹窗。
防护方案:
• 输出时用 htmlspecialchars($str, ENT_QUOTES, 'UTF-8') 转义特殊字符• 永远不要直接 echo $_GET['...']• 配置 Content-Security-Policy 响应头作为额外防线
攻击者诱导已登录用户访问一个恶意页面,这个页面里藏着一个请求(比如一个自动提交的表单,或者一个 img 标签的 src 指向转账接口)。因为用户已经登录,浏览器会自动带上 Cookie,服务器以为是用户本人操作就执行了——结果钱被转走、文章被删。
防护方案:
• 关键操作用 POST,别用 GET(GET 容易被 img/a 标签触发)• 加 CSRF Token:生成随机令牌存 Session,表单里放隐藏域,提交时比对• Cookie 设置 SameSite 属性(Lax 或 Strict)• 检查 Referer/Origin 头(不绝对可靠,作为辅助手段)
<?phpsession_start();// 生成 Token(每个会话生成一次即可)if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32));}if ($_SERVER['REQUEST_METHOD'] === 'POST') { $token = $_POST['csrf_token'] ?? ''; // hash_equals 防止时序攻击,不要用 == 比较 if (!hash_equals($_SESSION['csrf_token'], $token)) { die('CSRF 验证失败'); } // 处理表单...}?><form method="post"> <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>"> <!-- 其他表单字段 --></form>必踩的坑:比较 Token 一定要用 hash_equals 而不是 ==。普通字符串比较存在时序攻击风险——攻击者可以通过比对耗时推断字符。
• 表单 enctype 必须设为 multipart/form-data• 提交方法用 POST• php.ini 中 file_uploads = On,调整 upload_max_filesize 和 post_max_size
<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="file" name="avatar"> <button type="submit">上传头像</button></form>上传文件信息存在 $_FILES['avatar'] 数组里,包含这些字段:
• name:原始文件名• type:MIME 类型(浏览器提供,不可信)• size:文件大小(字节)• tmp_name:临时文件路径• error:错误代码(0 表示成功)
<?phpif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) { $file = $_FILES['avatar']; // 1. 检查上传错误 if ($file['error'] !== UPLOAD_ERR_OK) { die("上传失败,错误代码: " . $file['error']); } // 2. 文件大小限制(2MB) $maxSize = 2 * 1024 * 1024; if ($file['size'] > $maxSize) { die("文件太大,不能超过 2MB"); } // 3. 用 Fileinfo 检测真实 MIME(别信 $_FILES['type']) $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 图片"); } // 4. 生成随机文件名,防冲突和路径穿越 $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $newName = md5(uniqid() . time()) . '.' . $ext; $destination = __DIR__ . '/uploads/' . $newName; // 5. 移动文件到正式目录 if (move_uploaded_file($file['tmp_name'], $destination)) { echo "上传成功!文件路径:uploads/$newName"; } else { die("移动文件失败,请检查目录权限"); }}?>文件上传安全要点:永远不要信任 $_FILES['file']['type'](浏览器伪造的),用 Fileinfo 库检查文件真实内容;上传目录不要给执行权限,最好放在 Web 根目录之外;用随机文件名存储,防止覆盖和路径遍历攻击(比如 ../../etc/passwd 这种)。
HTTP 协议是无状态的——每次请求都是独立的,服务器不知道上一次请求是谁。要让网站"记住"用户的登录状态,得靠 Cookie 和 Session。
Cookie 是服务器让浏览器保存在用户电脑上的小文本(通常 4KB 以内),下次请求同域名时浏览器会自动带上。
设置 Cookie:
<?php// 参数:名称, 值, 过期时间, 路径, 域, 是否仅HTTPS, 是否HttpOnlysetcookie('username', '张三', time() + 3600, '/'); // 有效期 1 小时?>读取 Cookie:
<?phpecho $_COOKIE['username'] ?? '未设置';?>删除 Cookie:把过期时间设为过去即可。
<?phpsetcookie('username', '', time() - 3600);?>安全建议:设置 HttpOnly 标志禁止 JavaScript 读取 Cookie(防 XSS 偷 Cookie);设置 Secure 标志仅允许 HTTPS 传输;设置 SameSite 属性防御 CSRF。
Session 把数据存在服务器端,只给浏览器一个唯一的 Session ID(通过 Cookie 传递)。用户每次请求带上这个 ID,服务器找到对应数据,实现状态保持。相比 Cookie,Session 更安全——数据不在客户端,用户看不到也改不了。
启动 Session 并存取数据:
<?phpsession_start(); // 必须在任何输出之前调用$_SESSION['user_id'] = 123;$_SESSION['username'] = 'admin';echo "欢迎 " . $_SESSION['username'];?>销毁 Session(退出登录):
<?phpsession_start();$_SESSION = []; // 清空数组// 清除客户端的 Session ID Cookieif (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();?>login_session.php(登录页核心逻辑):
<?phpsession_start(); // 必须放在最顶部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(); header('Location: dashboard.php', true, 302); exit; } else { $error = '用户名或密码错误'; }}?><form method="post"> <input type="text" name="user"> <input type="password" name="pass"> <button type="submit">登录</button></form>dashboard.php(受保护后台页面):
<?phpsession_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']);?><h2>欢迎,<?= $user ?>!</h2><p>登录时间:<?= $loginTime ?></p><a href="logout.php">安全退出</a>logout.php(安全登出):
<?phpsession_start();$_SESSION = []; // 清空会话数据if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 3600, $params["path"], $params["domain"], $params["secure"], $params["httponly"]);}session_destroy();header('Location: login_session.php', true, 302);exit;?>

把前面所有知识整合起来——注册系统包含:表单验证、头像上传、密码哈希、Session 自动登录、CSRF 保护。这个示例代码较长,但每一部分都对应前面的知识点。
实战提醒:实际项目里密码存储一定要用 password_hash 哈希,绝不能明文存。这个示例用 JSON 文件存用户是为了演示方便,真实项目请用数据库。
register.php(核心代码):
<?phpsession_start();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/';$userFile = __DIR__ . '/users.json';if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true);if (!file_exists($userFile)) file_put_contents($userFile, json_encode([]));if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 1. CSRF 校验 if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) { $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) || mb_strlen($username) < 3) { $errors['username'] = '用户名至少3个字符'; } if (empty($password) || strlen($password) < 6) { $errors['password'] = '密码长度至少6位'; } if ($password !== $password2) { $errors['password2'] = '两次输入的密码不一致'; } if (empty($email) || !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 { $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; if (move_uploaded_file($file['tmp_name'], $uploadDir . $fileName)) { $avatarPath = 'uploads/' . $fileName; } } } } // 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; }}?><form method="POST" enctype="multipart/form-data"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES) ?>"> <input type="text" name="username" value="<?= htmlspecialchars($input['username'], ENT_QUOTES) ?>"> <input type="password" name="password"> <input type="password" name="password2"> <input type="email" name="email" value="<?= htmlspecialchars($input['email'], ENT_QUOTES) ?>"> <input type="file" name="avatar" accept="image/jpeg,image/png"> <button type="submit">立即注册</button></form>dashboard.php(注册后跳转的个人中心页,带头像显示):
<?phpsession_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(安全登出,清 Session + 清 Cookie + 销毁):
<?phpsession_start();// 清空会话$_SESSION = [];// 销毁Cookieif (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;?>核心亮点:password_hash 安全存储密码、CSRF Token 验证、文件真实 MIME 检测、htmlspecialchars 输出防 XSS、注册成功自动登录跳转。

• GET 与 POST:GET 数据拼 URL 适合查询,POST 数据放请求体适合写入• 表单处理:复选框 name 加 [] 变数组,表单和处理逻辑同文件最方便• 数据验证:后端验证才是安全的根基,filter_var 是 PHP 内置利器• 安全攻防:XSS 用 htmlspecialchars 防御,CSRF 用 Token 防御• 文件上传:检查真实 MIME、随机文件名、上传目录无执行权限• Cookie 与 Session:Cookie 存客户端,Session 存服务器端,登录状态靠 Session 维持• 综合案例:注册系统融合以上全部知识,是真实开发流程的缩影
本文基于作者实际学习与项目经验整理,代码示例均已本地测试可运行。文字整理过程中借助了 AI 工具辅助排版。

本文配套资料包
资料下载

-END-
