
使用 `password_verify()` 是 php 安全验证的唯一推荐方式;而通过 sql 查询(如 `select * from users where username=? and password_hash=?`)直接比对哈希值是严重错误,根本原因在于现代密码哈希(如 `password_hash()` 生成的 bcrypt)包含随机盐值(salt),无法预先计算用于 where 条件匹配。
在现代 Web 安全实践中,密码绝不能以明文、固定哈希(如 MD5/SHA1)或无盐哈希形式存储。PHP 的 password_hash() 函数默认采用 bcrypt 算法,并自动为每个密码生成唯一随机盐值,并将盐与哈希结果一同编码为单个字符串(例如:$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi)。这个字符串中隐含了算法标识、成本因子和盐——所有这些信息对验证至关重要。
这意味着:你无法在 SQL 层面“搜索”哈希值,因为:
- 数据库并不知道用户输入密码对应的盐;
- 同一密码多次调用 password_hash() 会产生完全不同的哈希字符串;
- 若强行在 WHERE 子句中比对哈希字段,实际等价于要求攻击者已提前获知正确的哈希值(即已成功窃取数据库),此时验证逻辑不仅无效,反而暴露了存储结构缺陷。
✅ 正确做法是「先查用户,再验密」:
// 1. 根据用户名安全获取用户记录(注意:仅凭用户名查询,不涉及密码)
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
// 2. 使用 password_verify() 在 PHP 层完成恒定时间验证
if ($user && password_verify($inputPassword, $user['password_hash'])) {
// 登录成功 —— 验证过程自动处理盐提取、算法匹配与防时序攻击
startSession($user['id']);
} else {
// 失败:统一响应,不区分“用户不存在”或“密码错误”
http_response_code(401);
echo "Invalid credentials.";
}⚠️ 补充关键注意事项:
立即学习“PHP免费学习笔记(深入)”;
- 永远不要在 SQL 中拼接或比对密码字段:这违背了密码学设计原则,也使系统无法支持未来升级哈希算法(如从 bcrypt 迁移到 Argon2);
- password_verify() 内部已实现恒定时间比较,有效防御计时攻击;而数据库字符串比较(即使哈希长度固定)受索引、字符集、优化器行为等影响,无法保证时序安全;
- 若旧系统存在 WHERE username=? AND password_hash=SHA1(?) 类逻辑,说明其完全未使用盐、算法过时,应立即强制重置所有用户密码,并迁移至 password_hash() / password_verify() 栈。
简言之:密码验证是不可逆的单向操作,必须由专用密码函数在应用层完成——数据库只负责存储和检索,不参与密码语义判断。这是纵深防御中不可妥协的一环。











