永远不要将用户输入直接拼接到 SQL 查询语句中。 这会导致严重的 SQL 注入漏洞。
现代 PHP 提供了三种主要的方式来处理查询参数,按推荐和安全级别排序如下:
- 预处理语句 (Prepared Statements) - 强烈推荐,最安全
- MySQLi 事务 (MySQLi Transactions) - 安全,但代码稍显冗长
- PDO 事务 (PDO Transactions) - 安全,代码简洁,推荐
预处理语句 - 最佳实践
预处理语句是处理查询参数的黄金标准,它的工作原理是:
- 发送模板:你向数据库发送一个 SQL 查询的“模板”,其中变量部分用 (占位符) 或
name(命名占位符) 代替。 - 数据库解析:数据库服务器会解析这个模板,并创建一个编译后的内部查询计划。
- 绑定参数:你将实际的变量值“绑定”到这个模板的占位符上。
- 执行:数据库执行这个预编译好的查询。
这个过程的关键在于,数据和 SQL 命令是分开传输的,数据库引擎永远不会将绑定的值作为 SQL 代码来执行,从而从根本上杜绝了 SQL 注入。
使用 MySQLi (面向过程风格)
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "myDB";
// 创建连接
$conn = new mysqli($servername, $username, $password, $dbname);
// 检查连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
// 1. 准备 SQL 语句模板 (使用 ? 作为占位符)
$stmt = $conn->prepare("SELECT id, firstname, lastname FROM users WHERE email = ? AND status = ?");
// 2. 绑定参数
// "ss" 表示两个参数都是字符串
// "i" 表示整数, "d" 表示双精度浮点数
$stmt->bind_param("ss", $email, $status);
// 3. 设置参数并执行
$email = "user@example.com";
$status = "active";
$stmt->execute();
// 4. 获取结果
$result = $stmt->get_result();
// 5. 输出数据
while($row = $result->fetch_assoc()) {
echo "ID: " . $row["id"]. " - Name: " . $row["firstname"]. " " . $row["lastname"]. "<br>";
}
// 6. 关闭连接
$stmt->close();
$conn->close();
?>
使用 MySQLi (面向对象风格)
<?php
// ... (连接代码同上)
// 1. 准备 SQL 语句
$stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
// 2. 绑定参数
// "sss" 表示三个字符串参数
$stmt->bind_param("sss", $firstname, $lastname, $email);
// 3. 设置参数并执行
$firstname = "John";
$lastname = "Doe";
$email = "john.doe@example.com";
$stmt->execute();
echo "新记录插入成功";
$stmt->close();
$conn->close();
?>
使用 PDO (PHP Data Objects)
PDO 是一个更现代、更通用的数据库抽象层,它支持多种数据库,并且其语法通常更简洁。
<?php
$host = 'localhost';
$db = 'myDB';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 抛出异常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认关联数组获取
PDO::ATTR_EMULATE_PREPARES => false, // 禁用模拟预处理语句
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
// 1. 准备 SQL 语句模板 (使用命名占位符 :name)
$sql = "SELECT id, firstname, lastname FROM users WHERE email = :email AND status = :status";
$stmt = $pdo->prepare($sql);
// 2. 绑定参数并执行 (可以一次性完成)
$params = [
':email' => 'user@example.com',
':status' => 'active'
];
$stmt->execute($params);
// 3. 获取结果
// fetch() 获取下一行,fetchAll() 获取所有行
while ($row = $stmt->fetch()) {
echo "ID: " . $row["id"]. " - Name: " . $row["firstname"]. " " . $row["lastname"]. "<br>";
}
?>
旧方法:直接拼接字符串 (极其危险!)
警告:以下方法存在严重的 SQL 注入风险,绝对不要在生产环境中使用!
// 危险!不要这样做! $user_input = "admin' -- "; // 恶意输入 // 直接将用户输入拼接到 SQL 中 $sql = "SELECT * FROM users WHERE username = '" . $user_input . "'"; // 最终执行的 SQL 变成了: SELECT * FROM users WHERE username = 'admin' -- ' // -- 是 SQL 注释符,后面的内容会被忽略,这个查询会返回所有 'admin' 用户,完全绕过了密码验证
mysql_* 函数系列 (已废弃)
PHP 5.5.0 版本起,mysql_* 系列函数已被废弃,并在 PHP 7.0.0 版本中被移除,这些函数没有内置的防止 SQL 注入的机制,并且功能有限,性能也不佳。
如果你还在使用这些函数,请立即停止并迁移到 MySQLi 或 PDO。
// 这是错误且过时的代码
$conn = mysql_connect("localhost", "root", "");
mysql_select_db("myDB", $conn);
$result = mysql_query("SELECT * FROM users WHERE id = " . $_GET['id']); // 极度危险
while ($row = mysql_fetch_assoc($result)) {
// ...
}
mysql_close($conn);
总结与对比
| 特性 | 预处理语句 (MySQLi/PDO) | 直接拼接字符串 | mysql_* 函数 |
|---|---|---|---|
| 安全性 | 极高,从根本上防止 SQL 注入 | 极低,极易受 SQL 注入攻击 | 极低,且已废弃 |
| 性能 | 高,查询只需编译一次,可多次执行 | 低,每次查询都需要重新编译 | 低 |
| 代码可读性 | 良好,逻辑清晰 | 差,字符串拼接难以维护 | 简单但过时 |
| 推荐度 | 强烈推荐 | 绝对禁止 | 已废弃,不要使用 |
最终建议
- 总是使用预处理语句,这是处理用户输入和构建动态查询的唯一安全方式。
- 优先选择 PDO,它更通用,支持多种数据库,语法也更优雅,如果你的项目只使用 MySQL,MySQLi 也是一个很好的选择。
- 启用错误报告:在开发环境中,确保
error_reporting设置为E_ALL,并开启display_errors,以便及时发现错误。 - 使用
try...catch块:当使用 PDO 时,将数据库操作放在try...catch块中,可以优雅地捕获和处理异常。
遵循这些原则,你的 PHP 应用程序将更加安全、健壮和易于维护。
