第三章: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用的就是新值。批量插入时特别好用。
方式三: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_INT、PDO::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']);
?>
3.6 常见陷阱与避坑指南
讲几个我实际踩过的坑:
- 表名和字段名不能用占位符——占位符只能替换值,不能替换表名、字段名等SQL结构部分。比如
SELECT * FROM ?是错的。 - IN子句需要动态生成占位符——比如
WHERE id IN (?, ?, ?),占位符数量要和数组长度一致。 - 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的事务处理,那可是保证数据一致性的利器。到时候见。