数据库安全与最佳实践
这篇讲 PHP 数据库操作中的常见陷阱和安全实践:SQL 注入防御、事务处理、连接管理和错误处理。写出能跑的功能不难,写出安全可靠的数据库交互才是区分专业与业余的分水岭。
SQL 注入防御
SQL 注入是最常见、也最危险的 Web 安全漏洞之一。攻击者通过在输入中注入 SQL 代码来操纵数据库。
错误做法:拼接 SQL
<?php
// 危险——永远不要这样做!
$username = $_GET['username'];
$sql = "SELECT * FROM users WHERE username = '$username'";
$result = $pdo->query($sql);
// 攻击者输入: ' OR '1'='1' --
// 导致: SELECT * FROM users WHERE username = '' OR '1'='1' --'
// 结果: 返回所有用户数据
?>
```sql
### 正确做法:参数化查询
```php
<?php
// 安全——参数与 SQL 分离传递
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username');
$stmt->execute(['username' => $_GET['username']]);
$user = $stmt->fetch();
?>
核心原则:绝不将用户输入直接拼接到 SQL 字符串中。参数化查询是防御 SQL 注入的唯一正确方式——不要试图用
mysqli_real_escape_string() 等函数手动"清洗"数据。事务处理
事务确保多个数据库操作要么全部成功,要么全部回滚——这对数据一致性至关重要(如转账、下单扣库存):
<?php
try {
$pdo->beginTransaction();
// 从 A 账户扣款
$stmt = $pdo->prepare('UPDATE accounts SET balance = balance - :amount WHERE id = :id');
$stmt->execute(['amount' => 100, 'id' => 1]);
// 向 B 账户存款
$stmt->execute(['amount' => -100, 'id' => 2]); // 注意这里是负数取反
$pdo->commit(); // 全部成功,提交
echo '转账成功';
} catch (Exception $e) {
$pdo->rollBack(); // 任何一步失败,回滚
echo '转账失败:' . $e->getMessage();
}
?>
```text
## 连接管理
```php
<?php
// 设置持久连接(复用连接,减少开销)
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_PERSISTENT => true,
]);
// 脚本结束或设为 null 时关闭连接
$pdo = null;
?>
持久连接可以提升高并发场景的性能,但需要合理配置
max_connections。对于中小项目,普通连接足够,不必过早优化。错误处理
<?php
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->prepare('INSERT INTO users (name) VALUES (:name)');
$stmt->execute(['name' => 'test']);
} catch (PDOException $e) {
// 记录详细错误到日志
error_log('数据库错误: ' . $e->getMessage() . ' | SQLSTATE: ' . $e->getCode());
// 向用户返回通用错误信息(不要暴露数据库细节)
http_response_code(500);
echo '操作失败,请稍后重试。';
}
?>
```text
## 最佳实践清单
| 实践 | 说明 |
| --- | --- |
| 参数化查询 | 所有含用户输入的 SQL 都必须使用预处理语句 |
| 异常模式 | `PDO::ERRMODE_EXCEPTION`——让错误可见、可捕捉 |
| 事务保护 | 多步骤写操作包裹在 `beginTransaction` / `commit` / `rollBack` 中 |
| 最小权限 | 应用连接数据库的账号只授予必要的权限,不用 `root` |
| 字符集 | DSN 中明确指定 `charset=utf8mb4` |
| 关闭模拟预处理 | `PDO::ATTR_EMULATE_PREPARES => false`——让数据库原生处理 |
| 错误不暴漏 | 生产环境绝不向前端输出数据库错误详情 |
## 一句话小结
SQL 注入是最大的数据库安全风险——参数化查询是唯一正解。事务保证数据一致性,异常模式让错误可控。开发时 `ERRMODE_EXCEPTION`,生产时错误记日志不暴漏。更多基础内容参见 [PHP 基础](../../PHP基础/01PHP简介/)。
最后更新于