PHP 表单验证
Client-side validation cannot be trusted; the server must re-validate.
Why server-side validation
Browser required and type="email" are trivially bypassed: edit the HTML, disable JS, or just curl the endpoint. All client data is untrusted.
# 完全绕过页面,直接提交
curl -X POST http://localhost/welcome.php \
-d "fname=<script>alert(1)</script>" \
-d "age=abc"The server receives exactly that string. Without validation and escaping it lands in the page verbatim — reflected XSS.
Validate and escape
Two separate jobs: validation decides whether data is usable, escaping decides how it is safely displayed. Do both.
<?php
$name = trim($_POST["fname"] ?? "");
$age = trim($_POST["age"] ?? "");
$errors = [];
if ($name === "") {
$errors[] = "名字不能为空";
} elseif (mb_strlen($name) > 20) {
$errors[] = "名字不能超过 20 个字";
}
if ($age === "") {
$errors[] = "年龄不能为空";
} elseif (!ctype_digit($age)) {
$errors[] = "年龄必须是整数";
} elseif ((int)$age < 1 || (int)$age > 120) {
$errors[] = "年龄必须在 1 到 120 之间";
}
if ($errors) {
foreach ($errors as $e) {
echo "错误: $e\n";
}
} else {
echo "通过: " . htmlspecialchars($name) . ", " . (int)$age . " 岁\n";
}通过: Tom, 18 岁Feeding abc as the age instead yields:
错误: 年龄必须是整数Validation helpers
| Check | Function | Note |
|---|---|---|
| 非空 | $v !== '' | 别用 empty(),"0" 会被判空 |
| 纯数字 | ctype_digit($v) | 不接受负号和小数点 |
| 整数 | filter_var($v, FILTER_VALIDATE_INT) | 支持范围选项 |
| 邮箱 | filter_var($v, FILTER_VALIDATE_EMAIL) | 标准做法 |
| URL | filter_var($v, FILTER_VALIDATE_URL) | 要限定协议就再查前缀 |
| IP | filter_var($v, FILTER_VALIDATE_IP) | 可加 FILTER_FLAG_IPV4 |
| 手机号(中国) | preg_match('/^1[3-9]\d{9}$/', $v) | 按业务调整号段 |
empty() is true for "0", 0 and []. To test whether a field was filled use !== '' or isset().Collecting errors
Collect all errors in an array and show them together instead of exiting on the first — far better UX.
<?php
function validate(array $in): array {
$errors = [];
$name = trim($in["name"] ?? "");
if ($name === "") {
$errors["name"] = "姓名必填";
}
$email = trim($in["email"] ?? "");
if ($email === "") {
$errors["email"] = "邮箱必填";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "邮箱格式不正确";
}
return [$errors, compact("name", "email")];
}
[$errors, $clean] = validate(["name" => "", "email" => "bad@"]);
print_r($errors);Array
(
[name] => 姓名必填
[email] => 邮箱格式不正确
)Keying errors by field lets the form render each message under its input — the same shape Laravel's validator uses.
Length and encoding
中文的「长度」要用 mb_strlen。用 strlen 会把「张三」算成 6,限制 20 字时实际只能输入 6 个汉字。
<?php
$v = "张三丰";
echo strlen($v), "\n";
echo mb_strlen($v), "\n";
echo mb_strlen($v) <= 20 ? "长度合法" : "超长", "\n";9
3
长度合法"UTF-8" to mb_strlen or ensure default_charset is UTF-8. Mixing GBK and UTF-8 corrupts lengths and truncation.为什么必须服务端校验
浏览器的 required、type="email" 都能被绕过:改 HTML、关 JS、直接用 curl 发请求。凡是来自客户端的数据一律不可信。
# 完全绕过页面,直接提交
curl -X POST http://localhost/welcome.php \
-d "fname=<script>alert(1)</script>" \
-d "age=abc"服务端收到的就是这段字符串。不校验、不转义,它就会原样出现在页面上——这就是反射型 XSS。
校验 + 转义
两件事不能混:校验决定数据能不能用,转义决定数据怎么安全地显示。两者都要做。
<?php
$name = trim($_POST["fname"] ?? "");
$age = trim($_POST["age"] ?? "");
$errors = [];
if ($name === "") {
$errors[] = "名字不能为空";
} elseif (mb_strlen($name) > 20) {
$errors[] = "名字不能超过 20 个字";
}
if ($age === "") {
$errors[] = "年龄不能为空";
} elseif (!ctype_digit($age)) {
$errors[] = "年龄必须是整数";
} elseif ((int)$age < 1 || (int)$age > 120) {
$errors[] = "年龄必须在 1 到 120 之间";
}
if ($errors) {
foreach ($errors as $e) {
echo "错误: $e\n";
}
} else {
echo "通过: " . htmlspecialchars($name) . ", " . (int)$age . " 岁\n";
}通过: Tom, 18 岁把上面的 $age 换成 abc,输出就变成:
错误: 年龄必须是整数常用校验函数
| 检查 | 函数 | 说明 |
|---|---|---|
| 非空 | $v !== '' | 别用 empty(),"0" 会被判空 |
| 纯数字 | ctype_digit($v) | 不接受负号和小数点 |
| 整数 | filter_var($v, FILTER_VALIDATE_INT) | 支持范围选项 |
| 邮箱 | filter_var($v, FILTER_VALIDATE_EMAIL) | 标准做法 |
| URL | filter_var($v, FILTER_VALIDATE_URL) | 要限定协议就再查前缀 |
| IP | filter_var($v, FILTER_VALIDATE_IP) | 可加 FILTER_FLAG_IPV4 |
| 手机号(中国) | preg_match('/^1[3-9]\d{9}$/', $v) | 按业务调整号段 |
empty() 对 "0"、0、[] 都返回 true。判断「用户填了没有」只能用 !== '' 或 isset()——这是 PHP 里最容易埋下的逻辑 bug。错误信息怎么收集
用数组收集全部错误再一起显示,比遇到第一个就 exit 体验好得多——用户不用来回试。
<?php
function validate(array $in): array {
$errors = [];
$name = trim($in["name"] ?? "");
if ($name === "") {
$errors["name"] = "姓名必填";
}
$email = trim($in["email"] ?? "");
if ($email === "") {
$errors["email"] = "邮箱必填";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "邮箱格式不正确";
}
return [$errors, compact("name", "email")];
}
[$errors, $clean] = validate(["name" => "", "email" => "bad@"]);
print_r($errors);Array
(
[name] => 姓名必填
[email] => 邮箱格式不正确
)键名用字段名,回显表单时就能精确地把错误挂在对应输入框下面。这也是 Laravel 校验器的做法。
长度与字符集
Count Chinese length with mb_strlen. strlen reports 6 for 张三, so a 20-char limit allows only 6 Chinese characters.
<?php
$v = "张三丰";
echo strlen($v), "\n";
echo mb_strlen($v), "\n";
echo mb_strlen($v) <= 20 ? "长度合法" : "超长", "\n";9
3
长度合法mb_strlen 的第二个参数要传 "UTF-8" 或确保 default_charset 是 UTF-8。混用 GBK 和 UTF-8 会导致长度算错、截断乱码。