第三章:PDO预处理语句——prepare与execute、绑定参数、防止SQL注入

说实话,SQL注入这玩意儿,我见过太多新手栽跟头了。记得我刚入行那会儿,公司有个老项目,所有查询都是字符串拼接。有一次线上数据被删了整整一个表,就是因为登录框里被人塞了条 DROP TABLE。嗯,从那以后,我对预处理语句就特别上心。

今天咱们就来聊聊PDO的预处理机制。说白了,它就是你和数据库之间的一道防火墙。

3.1 为什么需要预处理?

先看一个反面教材。很多新手会这么写:

<?php
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";
$stmt = $pdo->query($sql);
?>

你想想看,如果用户传进来的是 1; DROP TABLE users; --,会发生什么?整张表就没了。这就是典型的SQL注入。

我个人习惯,只要涉及用户输入,一律用预处理。这不是小题大做,而是职业习惯。

3.2 prepare与execute的基本用法

PDO的预处理分两步走:先prepare(准备),再execute(执行)。

<?php
// 第一步:准备SQL语句,用占位符代替真实值
$sql = "SELECT * FROM users WHERE email = :email AND status = :status";
$stmt = $pdo->prepare($sql);

// 第二步:绑定参数并执行
$stmt->execute([
    ':email' => 'test@example.com',
    ':status' => 1
]);

// 获取结果
$user = $stmt->fetch(PDO::FETCH_ASSOC);
?>

这里用了命名占位符 :email:status。还有一种问号占位符:

<?php
$sql = "SELECT * FROM users WHERE email = ? AND status = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute(['test@example.com', 1]);
?>

我个人更推荐命名占位符。为什么?因为代码可读性更好,尤其是参数多的时候,你一眼就能看出哪个值对应哪个字段。

3.3 绑定参数的三种方式

绑定参数这事儿,PDO给了你三种选择。我一个个说。

方式一:execute传数组(最常用)

<?php
$stmt->execute([
    ':email' => 'user@example.com',
    ':status'  => 1
]);
?>

这种方式最简洁,我90%的场景都用它。

方式二:bindParam绑定引用

<?php
$stmt = $pdo->prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
$stmt->bindParam(':name', $name);
$stmt->bindParam(':age', $age);

// 批量插入
$users = [
    ['Alice', 25],
    ['Bob', 30],
    ['Charlie', 28]
];

foreach ($users as [$name, $age]) {
    $stmt->execute(); // 变量值改变,执行结果也跟着变
}
?>

bindParam绑定的是变量的引用。变量值变了,下次execute用的就是新值。批量插入时特别好用。

注意:bindParam绑定的变量必须存在,否则会报错。我曾经踩过这个坑,循环里忘了初始化变量,结果插了一堆空值进去。

方式三:bindValue绑定值

<?php
$stmt = $pdo->prepare("UPDATE users SET name = :name WHERE id = :id");
$stmt->bindValue(':name', '张三', PDO::PARAM_STR);
$stmt->bindValue(':id', 100, PDO::PARAM_INT);
$stmt->execute();
?>

bindValue是直接传值,不是引用。第三个参数可以指定类型,比如 PDO::PARAM_INTPDO::PARAM_STR。虽然不指定也能用,但我建议养成指定类型的习惯,尤其是数字类型。

3.4 防止SQL注入的原理

你可能会问:为什么预处理就能防注入?

原因很简单。预处理语句在发送到数据库时,SQL结构和参数是分开传输的。数据库先解析SQL模板,确定执行计划,然后再把参数填进去。参数永远只是数据,不会被当成SQL代码执行。

我打个比方:就像你填表格,先确定表格格式(prepare),再往格子里填内容(execute)。不管你在格子里写什么,它都只是格子里的内容,不会变成表格本身。

核心要点:

  • SQL结构 + 参数 = 安全查询
  • 参数永远不会被当作SQL指令执行
  • 即使用户输入恶意内容,也只是普通字符串

3.5 实战:一个安全的用户登录

咱们写个完整的登录示例,看看预处理怎么用:

<?php
class UserAuth {
    private PDO $pdo;
    
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    
    public function login(string $email, string $password): ?array {
        // 1. 准备SQL
        $sql = "SELECT id, name, email, password_hash 
                FROM users 
                WHERE email = :email 
                AND status = :status 
                LIMIT 1";
        
        $stmt = $this->pdo->prepare($sql);
        
        // 2. 绑定参数并执行
        $stmt->execute([
            ':email' => $email,
            ':status' => 1  // 只查激活用户
        ]);
        
        // 3. 获取用户
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        // 4. 验证密码
        if ($user && password_verify($password, $user['password_hash'])) {
            unset($user['password_hash']); // 不返回密码
            return $user;
        }
        
        return null;
    }
}

// 使用示例
$auth = new UserAuth($pdo);
$user = $auth->login($_POST['email'], $_POST['password']);
?>
我的建议:所有数据库操作都封装成类方法。这样既安全又便于维护。我见过太多人把SQL散落在控制器里,改起来想死的心都有。

3.6 常见陷阱与避坑指南

讲几个我实际踩过的坑:

  1. 表名和字段名不能用占位符——占位符只能替换值,不能替换表名、字段名等SQL结构部分。比如 SELECT * FROM ? 是错的。
  2. IN子句需要动态生成占位符——比如 WHERE id IN (?, ?, ?),占位符数量要和数组长度一致。
  3. LIKE查询要小心——LIKE :keyword 没问题,但通配符 % 要拼在值里,不能拼在SQL里。

我曾经接手过一个项目,开发者在 ORDER BY 后面直接拼接了用户输入,结果被人注入了。记住:任何需要动态拼接SQL结构的地方,都要做白名单校验。

3.7 性能小贴士

预处理还有一个好处:重复执行相同SQL时性能更好。因为数据库只需要解析一次SQL模板,后面直接复用执行计划。

<?php
// 批量插入1000条数据
$sql = "INSERT INTO logs (user_id, action, created_at) VALUES (:uid, :action, :time)";
$stmt = $pdo->prepare($sql);

$start = microtime(true);

for ($i = 0; $i < 1000; $i++) {
    $stmt->execute([
        ':uid' => rand(1, 100),
        ':action' => 'login',
        ':time' => date('Y-m-d H:i:s')
    ]);
}

echo "耗时:" . (microtime(true) - $start) . "秒";
?>

用预处理批量插入,比每次重新拼接SQL快得多。我做过测试,1000条数据能快3-5倍。

3.8 本章小结

好了,咱们把今天的内容捋一捋:

  • 预处理语句 = prepare + execute,两步走
  • 绑定参数有三种方式:execute传数组、bindParam、bindValue
  • 防注入的原理是SQL结构和参数分离传输
  • 表名、字段名不能用占位符
  • 批量操作时预处理性能更好

说实话,预处理是PDO最核心的功能之一。你只要记住一条铁律:所有用户输入,必须经过预处理。做到这一点,SQL注入就基本跟你没关系了。

下一章咱们聊聊PDO的事务处理,那可是保证数据一致性的利器。到时候见。