2、输入验证与过滤:为什么输入验证如此重要?PHP中的过滤函数(filter_var, filter_input),正则表达式验证实战

说实话,我见过太多被攻破的网站,问题都出在同一个地方——对用户输入太信任了。

你想想看,用户提交的数据,说白了就是一段字符串。这段字符串里可能藏着什么?可能是正常的姓名,也可能是精心构造的 SQL 注入语句,或者是一段 XSS 攻击脚本。我当年刚入行时,就吃过这个亏。一个简单的搜索框,因为没有做任何过滤,被人注入了恶意代码,整站被挂马。嗯,从那以后,我再也不敢轻视输入验证了。

为什么输入验证如此重要?

核心原因就一个:用户输入是不可信的

不管是来自表单、URL 参数、Cookie 还是 API 接口,所有外部数据都必须经过验证。这不是小题大做,而是安全编程的底线。

我总结了几条血的教训:

  • SQL 注入:用户输入直接拼接到 SQL 语句中,数据库就被脱裤了。
  • XSS 攻击:用户输入直接输出到页面,别人的浏览器就被控制了。
  • 文件上传漏洞:用户上传了一个伪装成图片的 PHP 文件,服务器就被拿下了。
  • 命令注入:用户输入传入了 shell 执行函数,服务器就被远程控制了。

你看,每一个问题都是致命的。所以我的原则很简单:永远不要相信用户输入,哪怕它看起来人畜无害

核心原则:过滤输入,转义输出。输入验证是防御的第一道关卡,也是最重要的一道。

PHP 中的过滤函数

PHP 内置了一套非常实用的过滤函数,我个人习惯用它们来处理大部分常规验证。省时省力,而且比自己写正则要安全得多。

filter_var() — 过滤单个变量

这个函数是我用得最多的。它可以根据指定的过滤器,验证或清理一个变量。

<?php
// 验证邮箱
$email = "user@example.com";
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "邮箱格式正确";
} else {
    echo "邮箱格式错误";
}

// 清理 URL(去掉非法字符)
$url = "http://example.com/<script>alert('xss')</script>";
$cleanUrl = filter_var($url, FILTER_SANITIZE_URL);
echo $cleanUrl; // 输出: http://example.com/<script>alert('xss')</script>

// 验证 IP 地址
$ip = "192.168.1.1";
if (filter_var($ip, FILTER_VALIDATE_IP)) {
    echo "IP 地址合法";
}

// 验证整数范围
$age = 25;
$options = array("options" => array("min_range" => 1, "max_range" => 120));
if (filter_var($age, FILTER_VALIDATE_INT, $options)) {
    echo "年龄在合法范围内";
}
?>

我在项目中遇到过一种情况:用户提交的邮箱里带了个换行符,直接入库后导致邮件发送失败。用 FILTER_SANITIZE_EMAIL 一清理,问题就解决了。你看,有时候问题就这么简单。

小技巧filter_var() 的第三个参数可以传选项数组,用来设置验证范围、默认值等。比如验证整数时指定最小值和最大值,非常实用。

filter_input() — 直接过滤输入变量

这个函数更直接,它直接从输入源(GET、POST、COOKIE 等)获取数据并过滤。省去了先取变量再过滤的步骤。

<?php
// 直接从 POST 获取并验证邮箱
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email) {
    echo "收到的邮箱是:$email";
} else {
    echo "邮箱格式不对";
}

// 从 GET 获取并清理字符串
$name = filter_input(INPUT_GET, 'name', FILTER_SANITIZE_STRING);
echo "清理后的名字:$name";

// 验证整数
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, 
    array("options" => array("min_range" => 1, "max_range" => 150)));
if ($age === false) {
    echo "年龄不合法";
}
?>

我个人习惯用 filter_input() 来处理表单提交。为什么呢?因为它直接返回过滤后的结果,如果验证失败就返回 false。这样代码更简洁,逻辑也更清晰。

注意filter_input() 在变量不存在时返回 null,验证失败时返回 false。判断时一定要用 === 严格比较,否则容易踩坑。我曾经就因为用了 == 判断,把空字符串和 false 搞混了,排查了半天。

常用过滤器一览

过滤器 用途 示例
FILTER_VALIDATE_EMAIL 验证邮箱格式 user@example.com
FILTER_VALIDATE_URL 验证 URL 格式 http://example.com
FILTER_VALIDATE_IP 验证 IP 地址 192.168.1.1
FILTER_VALIDATE_INT 验证整数 123
FILTER_VALIDATE_FLOAT 验证浮点数 3.14
FILTER_SANITIZE_STRING 清理字符串(去掉标签) <b>hello</b> → hello
FILTER_SANITIZE_EMAIL 清理邮箱(去掉非法字符) user@exa mple.com → user@example.com
FILTER_SANITIZE_URL 清理 URL http://exa mple.com → http://example.com

正则表达式验证实战

内置过滤器虽然方便,但有些场景它搞不定。比如验证手机号、身份证号、自定义格式的订单号等。这时候就得靠正则表达式了。

正则表达式,说白了就是一套匹配规则。用好了,它能精确控制用户输入的格式。用不好,那就是给自己挖坑。

手机号验证

<?php
function validatePhone($phone) {
    // 中国大陆手机号:1开头的11位数字
    $pattern = '/^1[3-9]\d{9}$/';
    return preg_match($pattern, $phone) === 1;
}

$phone = "13800138000";
if (validatePhone($phone)) {
    echo "手机号合法";
} else {
    echo "手机号不合法";
}
?>

密码强度验证

<?php
function validatePassword($password) {
    // 至少8位,包含大小写字母和数字
    $pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/';
    return preg_match($pattern, $password) === 1;
}

$password = "Abc12345";
if (validatePassword($password)) {
    echo "密码强度合格";
} else {
    echo "密码需要至少8位,包含大小写字母和数字";
}
?>

自定义订单号验证

<?php
function validateOrderId($orderId) {
    // 格式:ORD-2024-XXXXX(ORD-年份-5位数字)
    $pattern = '/^ORD-20\d{2}-\d{5}$/';
    return preg_match($pattern, $orderId) === 1;
}

$orderId = "ORD-2024-12345";
if (validateOrderId($orderId)) {
    echo "订单号格式正确";
} else {
    echo "订单号格式错误,正确格式:ORD-2024-XXXXX";
}
?>

避坑指南:写正则时,一定要考虑边界情况。比如手机号验证,/^1\d{10}$/ 虽然也能匹配,但会放过 10000000000 这种不存在的号段。加上 [3-9] 限制后,就严谨多了。我曾经就因为正则写得太宽松,导致用户提交了无效手机号,后续的短信验证码发送全失败了。

综合实战:一个安全的用户注册验证

好了,理论说完了,咱们来点实战。下面是一个用户注册的验证函数,综合了内置过滤器和正则表达式。

<?php
function validateRegistration($data) {
    $errors = [];
    
    // 验证用户名:2-20位字母数字下划线
    if (!preg_match('/^[a-zA-Z0-9_]{2,20}$/', $data['username'])) {
        $errors[] = "用户名必须是2-20位字母、数字或下划线";
    }
    
    // 验证邮箱
    if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
        $errors[] = "邮箱格式不正确";
    }
    
    // 验证密码强度
    if (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/', $data['password'])) {
        $errors[] = "密码至少8位,包含大小写字母和数字";
    }
    
    // 验证年龄:1-120之间的整数
    $ageOptions = array("options" => array("min_range" => 1, "max_range" => 120));
    if (!filter_var($data['age'], FILTER_VALIDATE_INT, $ageOptions)) {
        $errors[] = "年龄必须在1-120之间";
    }
    
    // 验证手机号
    if (!preg_match('/^1[3-9]\d{9}$/', $data['phone'])) {
        $errors[] = "手机号格式不正确";
    }
    
    // 清理可能包含 XSS 的字段
    $data['bio'] = filter_var($data['bio'], FILTER_SANITIZE_STRING);
    
    return $errors;
}

// 模拟用户提交数据
$userInput = [
    'username' => 'test_user',
    'email' => 'test@example.com',
    'password' => 'Abc12345',
    'age' => '25',
    'phone' => '13800138000',
    'bio' => '<script>alert("xss")</script>Hello world'
];

$errors = validateRegistration($userInput);
if (empty($errors)) {
    echo "所有验证通过!可以注册了。";
    // 注意:这里只是验证通过,入库时还要做 SQL 转义
} else {
    foreach ($errors as $error) {
        echo "<p>错误:$error</p>";
    }
}
?>

你看,这个函数把内置过滤器和正则表达式结合起来了。每个字段都用最合适的方式验证。用户名和密码用正则,邮箱和年龄用内置过滤器,简介字段做清理。这样既高效又安全。

总结一下:输入验证不是可选项,而是必选项。能用内置过滤器就用内置过滤器,搞不定的再上正则。记住,验证要严格,输出要转义。这两步做好了,80% 的安全问题就跟你没关系了。

嗯,今天就聊到这儿。下一章咱们聊聊 SQL 注入的防御,那又是一个大坑。到时候我会分享一些我当年踩过的雷,保证让你少走弯路。