用户注册时存入数据库的密码不应该明文保存,而是要通过 password_hash 生成哈希值。登录时则需要把用户输入的明文密码与数据库中的哈希值进行比对,这正是 password_verify 函数的职责。本文将从基本用法、常见错误、完整登录流程以及哈希升级策略几个方面,把这个函数讲清楚。

password_verify 的基本用法
password_verify 是 PHP 5.5 之后内置的密码校验函数,它只需要两个参数:第一个是用户提交的明文密码,第二个是数据库中存储的哈希字符串。函数返回布尔值,匹配返回 true,不匹配返回 false。它内部会自动从哈希字符串中解析出算法类型、成本因子和盐值,然后重新计算一次哈希并做恒定时间比较,全程不需要开发者手动处理盐值。
下面是一个最简单的示例,演示生成哈希和验证哈希是如何配合工作的:
<?php
// 注册时生成哈希
$password = 'mySecret123';
$hash = password_hash($password, PASSWORD_DEFAULT);
// 输出类似:$2y$10$abcdefghijklmnopqrstuv...
echo $hash;
// 登录时验证密码
$isValid = password_verify('mySecret123', $hash);
var_dump($isValid); // bool(true)
$isValid2 = password_verify('wrongPass', $hash);
var_dump($isValid2); // bool(false)
?>需要注意的一个细节是,数据库中存储哈希的字段长度一定要足够。bcrypt 算法生成的哈希固定为 60 个字符,建议字段类型用 VARCHAR(255),这样即使未来默认算法切换为 argon2id 等更长的格式也不会溢出截断。如果字段太短导致哈希被截断,password_verify 永远返回 false,而且这类问题非常隐蔽,日志里不会有任何报错。
开发者常犯的几个错误
第一个常见错误是自己先对密码做 md5 或 sha1,再用 password_verify 去验证哈希结果。这种双重哈希虽然技术上可行,但没有任何安全收益,反而让代码更复杂。md5 和 sha1 计算速度太快,本身就不适合存储密码。正确做法是直接把用户输入的原始密码交给 password_hash 处理即可。当然如果历史遗留数据已经是 md5 的,可以保留旧逻辑,并在用户成功登录后透明地重新哈希,这一点会在后文提到。
第二个错误是把哈希值做了额外处理,比如存库前用 trim 去空格没问题,但有些框架或 ORM 会自动对字段做过滤、转义或大小写转换,导致取出来的哈希和原始值不一致,验证永远失败。bcrypt 哈希是大小写敏感的,任何字符变动都会导致比对失败。排查这类问题时,可以把数据库里的哈希直接复制出来硬编码测试,缩小问题范围。
第三个错误是混淆了验证和比较。有人会用字符串相等来判断,写成 $hash === md5($password),这种写法存在时序攻击风险,而且完全绕过了盐值机制。凡是密码校验场景,一律使用 password_verify,不要自己写比较逻辑。
完整的注册与登录示例
下面给出一个贴近实际项目的最小示例,包含注册入库和登录校验两个环节。实际项目中还需要加上 CSRF 防护、输入过滤、登录次数限制等措施,这里聚焦密码部分。
<?php
// 注册处理
function register(string $username, string $password, PDO $pdo): void
{
if (strlen($password) < 8) {
throw new InvalidArgumentException('密码至少8位');
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
$stmt->execute([$username, $hash]);
}
// 登录校验
function login(string $username, string $password, PDO $pdo): bool
{
$stmt = $pdo->prepare('SELECT id, password FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
// 用户不存在也做一次哈希运算,避免通过响应时间探测账号是否存在
password_verify($password, '$2y$10$dummyhashdummyhashdummyhashdummyhashdummyhashdu');
return false;
}
return password_verify($password, $user['password']);
}
?>示例里有个容易被忽略的安全细节:当用户名不存在时也执行了一次假哈希运算。因为 password_verify 是故意设计得很慢的算法,如果用户存在时耗时几百毫秒而用户不存在时几乎立即返回,攻击者就能通过响应时间批量探测有效账号。加上假运算后,两种情况的耗时趋于一致。
用 password_needs_rehash 平滑升级算法
PASSWORD_DEFAULT 的值会随 PHP 版本演进而变化,早期是 bcrypt,PHP 7.3 之后的环境中可以切换到 argon2id。当默认算法升级或成本因子调高后,数据库里的旧哈希怎么办?答案是使用 password_needs_rehash。它在用户每次成功登录、明文密码在手里的时机,检查现有哈希是否还符合当前推荐的算法和成本,不符合就重新生成并更新数据库。
<?php
function loginAndRehash(string $username, string $password, PDO $pdo): bool
{
$stmt = $pdo->prepare('SELECT id, password FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, $user['password'])) {
return false;
}
// 检查旧哈希是否需要按当前默认算法重新生成
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$upd = $pdo->prepare('UPDATE users SET password = ? WHERE id = ?');
$upd->execute([$newHash, $user['id']]);
}
// 写入会话、记录登录日志等
return true;
}
?>总结一下:password_verify 必须与 password_hash 配对使用,不要手动加盐、不要 md5 预处理;数据库字段给足长度;校验失败时保持一致的响应行为;再配合 password_needs_rehash 做算法平滑迁移,密码存储的安全体系就基本完备了。这几行代码的成本极低,却能避免绝大多数密码泄露后的撞库风险,值得每一个 PHP 项目认真落实。
password_verify密码验证PHP安全修改时间:2026-08-31 03:53:10