
本文详解如何将易受 sql 注入的动态拼接查询(如基于姓名拆分的模糊/精确搜索)重构为参数化预处理语句,并保持原有业务逻辑完整,避免常见错误(如占位符未识别、返回布尔值而非结果集)。
本文详解如何将易受 sql 注入的动态拼接查询(如基于姓名拆分的模糊/精确搜索)重构为参数化预处理语句,并保持原有业务逻辑完整,避免常见错误(如占位符未识别、返回布尔值而非结果集)。
在 PHP 中,直接拼接用户输入(如 $key)到 SQL 字符串中构成严重 SQL 注入风险。原代码使用 mysqli_query() 执行动态构造的查询,既不安全也不利于维护。正确做法是统一使用 MySQLi 面向对象风格的预处理语句(Prepared Statement),通过 ? 占位符与 bind_param() 绑定变量,确保输入被严格类型化处理。
✅ 核心改造步骤
- 提前归一化参数:无论 $key 是单名(如 "Alice")还是全名(如 "Alice Smith"),都提取出两个待匹配字段值($n1 和 $n2),避免在 SQL 中重复分支逻辑;
- 统一 SQL 模板:使用 ? 占位符代替字符串拼接,复用同一查询结构,提升可读性与可维护性;
- 正确执行与获取结果集:调用 execute() 后必须使用 get_result() 获取 mysqli_result 对象(而非 mysqli_query() 返回的资源),否则后续 num_rows 或 fetch_assoc() 将失败(常见报错:Call to a member function num_rows() on bool)。
以下是重构后的完整示例:
<?php
$name = explode(' ', $key, 2); // 拆分为最多两项:[first, last]
// 统一参数赋值:单名时 firstname 和 lastname 均匹配该值;双名时分别匹配
$n1 = $name[0];
$n2 = isset($name[1]) && !empty($name[1]) ? $name[1] : $name[0];
// 构建参数化 SQL(支持两种匹配逻辑)
if (empty($name[1])) {
$sql = "SELECT * FROM users WHERE user_firstname = ? OR user_lastname = ?";
} else {
$sql = "SELECT * FROM users WHERE user_firstname = ? AND user_lastname = ?";
}
// 预处理 + 绑定 + 执行
$stmt = $conn->prepare($sql);
$stmt->bind_param('ss', $n1, $n2); // 'ss' 表示两个字符串参数
$stmt->execute();
$query = $stmt->get_result(); // ⚠️ 关键!必须用 get_result() 获取结果集对象
include 'includes/userquery.php';
?>对应 includes/userquery.php 的适配代码(注意:$query 现为 mysqli_result 实例,非布尔值):
<?php
if ($query->num_rows === 0) {
echo '<div class="post">没有匹配结果,请尝试扩大搜索范围。</div><br>';
} else {
while ($row = $query->fetch_assoc()) {
include 'includes/post.php';
echo '<br>';
}
}
?>⚠️ 注意事项与最佳实践
- 始终验证连接状态:在 prepare() 前建议检查 $conn->connect_error;
-
错误处理增强:生产环境应捕获 prepare() 和 execute() 异常,例如:
if (!$stmt = $conn->prepare($sql)) { throw new Exception("Prepare failed: " . $conn->error); } - 避免冗余包含:userquery.php 若仅用于此逻辑,建议内联或封装为函数,提升可测试性;
- 考虑性能优化:对 user_firstname 和 user_lastname 字段建立联合索引(如 INDEX idx_name (user_firstname, user_lastname))可显著加速 AND 查询。
通过以上改造,代码不仅消除了 SQL 注入隐患,还提升了健壮性与可扩展性——未来若需增加邮箱或用户名搜索,只需扩展参数绑定逻辑,无需重写 SQL 结构。


















