SQL注入是PHP应用开发中需要重点防范的安全漏洞,攻击者通过在用户输入中插入恶意SQL片段,可能绕过验证逻辑、窃取敏感数据甚至篡改数据库内容。预处理语句配合参数绑定是PHP官方推荐的防注入方案,能有效隔离SQL逻辑与用户输入数据。

SQL注入的产生原因
当开发者直接将用户输入拼接到SQL语句中执行时,就容易产生注入风险。比如下面的代码就存在明显的安全问题:
<?php $username = $_POST['username']; $password = $_POST['password']; // 直接拼接用户输入到SQL语句,存在注入风险 $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = mysqli_query($conn, $sql); ?>
如果攻击者在username输入框中填入admin' --,拼接后的SQL会变成SELECT * FROM users WHERE username = 'admin' --' AND password = 'xxx',--是SQL的注释符,后面的密码验证逻辑会被直接忽略,攻击者无需密码就能登录admin账户。
预处理语句的防注入原理
预处理语句的核心是将SQL语句的结构和数据分开处理:首先向数据库发送带占位符的SQL模板,数据库会对模板进行编译解析,生成执行计划;之后再将用户输入的数据单独发送给数据库,数据库只会把数据当作纯参数处理,不会将其解析为SQL指令的一部分,从根源上避免了注入。
使用mysqli扩展实现预处理与参数绑定
1. 准备预处理语句
使用mysqli_prepare函数创建预处理对象,SQL中的占位符用问号?表示:
<?php
// 假设已经建立了数据库连接$conn
$username = $_POST['username'];
$password = $_POST['password'];
// 带占位符的SQL模板,?是参数占位符
$sql = "SELECT id,username FROM users WHERE username = ? AND password = ?";
// 创建预处理语句对象
$stmt = mysqli_prepare($conn, $sql);
if (!$stmt) {
die("预处理失败: " . mysqli_error($conn));
}
?>
2. 绑定参数并执行
使用mysqli_stmt_bind_param绑定参数,第一个参数是预处理对象,第二个参数是参数类型字符串,s代表字符串,i代表整数,d代表浮点数,b代表二进制数据,后面依次传入要绑定的变量:
<?php
// 绑定参数,这里两个参数都是字符串类型,所以用ss
mysqli_stmt_bind_param($stmt, "ss", $username, $password);
// 执行预处理语句
mysqli_stmt_execute($stmt);
// 获取结果集
$result = mysqli_stmt_get_result($stmt);
if (mysqli_num_rows($result) > 0) {
echo "登录成功";
} else {
echo "用户名或密码错误";
}
// 关闭预处理对象
mysqli_stmt_close($stmt);
?>
使用PDO扩展实现预处理与参数绑定
PDO是PHP更通用的数据库扩展,支持多种数据库,预处理写法更简洁:
<?php
try {
// 创建PDO连接,这里以MySQL为例
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", "root", "123456");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$username = $_POST['username'];
$password = $_POST['password'];
// 准备预处理语句,也可以用命名占位符:username
$sql = "SELECT id,username FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
// 绑定参数并执行,也可以直接在执行时传入参数数组
$stmt->bindParam(":username", $username, PDO::PARAM_STR);
$stmt->bindParam(":password", $password, PDO::PARAM_STR);
$stmt->execute();
// 或者直接execute传入参数数组,无需单独绑定
// $stmt->execute([':username' => $username, ':password' => $password]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
echo "登录成功,欢迎" . $user['username'];
} else {
echo "用户名或密码错误";
}
} catch (PDOException $e) {
die("数据库操作失败: " . $e->getMessage());
}
?>
防注入的额外注意事项
- 不要相信任何用户输入,所有传入数据库的数据都通过预处理参数绑定处理,即使是内部系统的输入也不能省略这个步骤
- 数据库连接时设置正确的字符集,比如使用utf8mb4,避免宽字节注入问题
- 遵循最小权限原则,给数据库连接账号只分配必要的操作权限,即使发生注入也能降低损失
- 不要将数据库报错信息直接返回给用户,避免泄露数据库结构信息