
直接在 sql 查询中通过 `where user=? and pass=?` 比对密码哈希值是严重不安全的做法,根本原因在于现代密码哈希(如 `password_hash()` 生成的 bcrypt)包含动态盐值(salt),且盐值唯一、不可预测,导致无法在数据库层面进行等值查询。
现代 PHP 密码哈希函数(如 password_hash())默认使用 bcrypt(或 argon2i/argon2id),其输出格式为:
$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi
该字符串完整编码了算法标识、成本因子、随机盐值和最终哈希值。其中盐值是每次调用 password_hash() 时自动生成的 16 字节随机数据——这意味着即使同一密码被两次哈希,结果也完全不同。
因此,若尝试在 SQL 中写:
SELECT id FROM users WHERE username = ? AND password_hash = ?;
你必须提前知道目标用户的完整哈希字符串才能构造查询参数。但用户登录时只提供明文密码,服务端无法在不先查出该用户原始哈希的前提下生成可比对的哈希值——而“先查哈希”恰恰需要按用户名查询(非密码),这正是 password_verify() 的标准流程:
立即学习“PHP免费学习笔记(深入)”;
// ✅ 正确做法:先按用户名获取用户记录,再用 password_verify() 校验
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
// 登录成功
} else {
// 失败(注意:对不存在的用户名也应延迟响应,防枚举)
}⚠️ 反例风险分析:
- 无盐哈希(如 MD5/SHA1 直接存):虽支持 SQL 等值查询,但极易被彩虹表破解,且无法抵御批量撞库;
- 固定盐值:等同于无盐,违背“每个密码独立抗攻击”原则;
- 时间侧信道误判:认为 SQL 查询耗时恒定就能防计时攻击——实际数据库索引查找、行锁、缓存命中率等因素仍会引入可观测差异;而 password_verify() 内部已强制恒定时间比较(针对相同长度哈希),更可靠;
- 逻辑漏洞:若错误地允许 WHERE username=? AND password_hash=MD5(?),等于暴露系统仍在用弱哈希,且丧失升级算法的能力(如从 bcrypt 迁移至 Argon2)。
✅ 最佳实践总结:
- 始终使用 password_hash() 生成哈希(默认 bcrypt),存储完整字符串;
- 登录时先通过用户名查出用户记录(注意统一失败响应时间,避免用户存在性泄露);
- 严格使用 password_verify($input, $storedHash) 进行校验;
- 定期调用 password_needs_rehash() 检查是否需更新哈希强度。
安全不是“看起来快”,而是“设计上不可绕过”。哈希不可逆、盐值唯一、验证恒时——三者缺一不可。











