本文目录导读:

在PHP项目中,预编译语句(Prepared Statements)是防止SQL注入最有效的方法之一,它通过将SQL语句结构和数据分离来实现安全性,下面是详细的实现方式:
使用PDO(PHP Data Objects)
基本示例
<?php
// 数据库连接
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$username = 'root';
$password = '';
try {
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟预编译
]);
} catch (PDOException $e) {
die('连接失败: ' . $e->getMessage());
}
// 使用预编译语句
$sql = "SELECT * FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
// 绑定参数
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
// 设置参数值(这些值来自用户输入)
$username = $_POST['username'];
$password = $_POST['password'];
// 执行查询
$stmt->execute();
$result = $stmt->fetchAll();
?>
使用位置占位符(?)
<?php $sql = "SELECT * FROM users WHERE username = ? AND password = ?"; $stmt = $pdo->prepare($sql); // 绑定参数(位置顺序) $stmt->bindParam(1, $username); $stmt->bindParam(2, $password); // 或者使用数组方式 $stmt->execute([$username, $password]); ?>
使用MySQLi
面向对象方式
<?php
// 数据库连接
$mysqli = new mysqli('localhost', 'root', '', 'testdb');
if ($mysqli->connect_error) {
die('连接失败: ' . $mysqli->connect_error);
}
// 设置字符集
$mysqli->set_charset('utf8mb4');
// 预编译语句
$sql = "SELECT * FROM users WHERE username = ? AND password = ?";
$stmt = $mysqli->prepare($sql);
// 绑定参数(i=整数, d=浮点数, s=字符串, b=二进制)
$stmt->bind_param("ss", $username, $password);
// 设置参数
$username = $_POST['username'];
$password = $_POST['password'];
// 执行
$stmt->execute();
// 获取结果
$result = $stmt->get_result();
$users = $result->fetch_all(MYSQLI_ASSOC);
// 关闭语句
$stmt->close();
$mysqli->close();
?>
插入数据示例
<?php
// 插入数据(绝对安全)
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("ssi", $username, $email, $age);
$username = $_POST['username'];
$email = $_POST['email'];
$age = (int)$_POST['age'];
$stmt->execute();
echo "新增记录ID: " . $stmt->insert_id;
?>
预编译语句如何防止注入
原理图解
用户输入: ' OR '1'='1
↓
传统SQL拼接: SELECT * FROM users WHERE username = '' OR '1'='1'
→ SQL注入成功!
预编译方式:
1. 发送SQL模板: SELECT * FROM users WHERE username = ?
2. 数据库解析SQL结构
3. 发送参数: "' OR '1'='1"
4. 数据库将参数当作字符串处理,不解析SQL语法
→ 安全!
最佳实践建议
配置优化
<?php
// PDO最佳配置
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 异常模式
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 关联数组
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟预编译
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4" // 设置字符集
];
$pdo = new PDO($dsn, $username, $password, $options);
?>
多种数据类型绑定
<?php
// 批量插入
$users = [
['Alice', 'alice@example.com', 25],
['Bob', 'bob@example.com', 30],
['Charlie', 'charlie@example.com', 28]
];
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);
$pdo->beginTransaction();
foreach ($users as $user) {
$stmt->execute($user);
}
$pdo->commit();
?>
查询结果处理
<?php
// 获取单条记录
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
// 获取多条记录
$stmt = $pdo->prepare("SELECT * FROM users WHERE age > ?");
$stmt->execute([$minAge]);
$users = $stmt->fetchAll();
// 获取影响行数
$stmt = $pdo->prepare("UPDATE users SET status = ? WHERE id = ?");
$stmt->execute(['active', $userId]);
echo "更新了 " . $stmt->rowCount() . " 行";
?>
常见误区
❌ 错误做法
<?php
// 错误:混合使用
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = $id"); // 仍然不安全
// 错误:不完全使用
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = " . $_GET['username']);
$stmt->execute(); // 仍然不安全
// 错误:忘记绑定参数
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(); // 没有绑定参数会导致错误
?>
✅ 正确做法
<?php
// 始终使用占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
// 或者命名占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute([':username' => $_POST['username']]);
?>
完整安全示例
<?php
class Database {
private $pdo;
public function __construct($config) {
try {
$this->pdo = new PDO(
"mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4",
$config['username'],
$config['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
throw new Exception("数据库连接失败");
}
}
public function query($sql, $params = []) {
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
} catch (PDOException $e) {
throw new Exception("查询执行失败");
}
}
public function fetch($sql, $params = []) {
return $this->query($sql, $params)->fetch();
}
public function fetchAll($sql, $params = []) {
return $this->query($sql, $params)->fetchAll();
}
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, array_values($data));
return $this->pdo->lastInsertId();
}
}
// 使用示例
$db = new Database([
'host' => 'localhost',
'dbname' => 'test',
'username' => 'root',
'password' => ''
]);
// 安全查询
$user = $db->fetch("SELECT * FROM users WHERE id = ?", [$_GET['id']]);
$users = $db->fetchAll("SELECT * FROM users WHERE age > ?", [18]);
// 安全插入
$newId = $db->insert('users', [
'username' => $_POST['username'],
'email' => $_POST['email'],
'password' => password_hash($_POST['password'], PASSWORD_DEFAULT)
]);
?>
预编译语句防止SQL注入的核心机制:
- SQL结构预定义:数据库先编译SQL语句结构
- 数据后绑定:参数值在编译后单独发送
- 自动转义:数据库自动处理特殊字符
- 类型安全:严格区分SQL代码和数据
永远不要相信用户输入,始终使用预编译语句处理所有数据库操作!