答案是必须使用password_hash()和password_verify()配合PASSWORD_DEFAULT。该方案自动加盐、慢哈希、支持算法演进,且哈希值内嵌算法标识与成本参数,验证时具备时序攻击防护,是PHP密码安全存储的唯一推荐方式。

PHP密码存储绝不能明文,这不是“建议”,而是安全底线。一旦数据库泄露,明文密码等于直接交出用户账户。
password_hash() 是唯一推荐的哈希入口
PHP 内置的 password_hash() 已封装 bcrypt(当前默认)或 Argon2(PHP 7.2+ 可选),自动完成加盐、慢哈希、算法版本管理。它生成的哈希字符串自带算法标识、成本因子和盐值,全部内嵌在结果中,无需你手动维护。
- 必须用
PASSWORD_DEFAULT,而非PASSWORD_BCRYPT—— 前者未来可随 PHP 升级自动切换更强算法,后者会锁死在 bcrypt,错过安全演进 - 避免传入空字符串、
null或过短密码(如 1~2 字符),password_hash()不报错但输出无效哈希;应在调用前用strlen($pwd) >= 8等做基础校验 - 不要截断哈希结果存数据库:
password_hash()输出长度不固定(bcrypt 约 60 字符,Argon2 可达 90+),字段至少设为VARCHAR(255)
验证必须用 password_verify(),别手写 strcmp 或正则
哈希验证不是字符串比对,password_verify() 内部做了时序攻击防护(constant-time comparison),且能自动识别哈希头并调用对应算法解析。手写 == 或 hash_equals()(虽安全但无算法适配能力)都绕过了这个关键保护。
- 传入的明文密码必须与原始输入完全一致(包括空格、换行),建议用
trim($_POST['password'])预处理 - 如果数据库里存的是旧 MD5/SHA1 哈希,
password_verify()会直接返回false,此时应触发平滑升级逻辑(见下一条) - 不要在验证失败时透露“用户名不存在”或“密码错误”的区别,统一返回“登录失败”,防止用户枚举
老系统迁移时,别删旧密码字段,要渐进升级
上线新哈希逻辑后,旧用户首次登录成功,就该立刻用 password_hash() 重写其密码哈希到数据库。这叫“lazy migration”,不需批量转换,也不用停服。
立即学习“PHP免费学习笔记(深入)”;
- 检查旧哈希是否需要升级:
! password_needs_rehash($dbHash, PASSWORD_DEFAULT)返回true才执行更新 - 数据库表必须保留原密码字段(如
password_legacy),升级后才可清空;切勿在迁移中途删除字段导致无法回滚 - 若旧系统用了自定义加盐(如
md5($pwd.$salt)),无法用password_verify()直接兼容,需临时加一层适配逻辑,验证通过后再生成新哈希
环境变量管数据库密码,和用户密码是两回事
很多人混淆“数据库连接密码”和“用户登录密码”的存储方式。前者是服务间认证凭据,必须通过环境变量(getenv('DB_PASSWORD'))注入,禁止出现在 config.php 或 .env 明文文件中;后者是用户数据,必须用 password_hash() 单向哈希,两者技术路径完全不同,混用等于双重违规。
- 框架如 Laravel 的
.env文件虽方便,但必须确保 Web 服务器(Nginx/Apache)已禁用对其的直接访问,否则/.env可被下载 - 别用 base64 或简单 XOR “伪装”数据库密码——这不算加密,只是编码,毫无安全意义
- Docker 中优先用
environment:块而非env_file:,减少本地 .env 泄露风险
最常被忽略的一点:哈希不是一劳永逸。PHP 每次大版本升级都可能调整 PASSWORD_DEFAULT 的底层算法,你要定期检查 password_needs_rehash() 返回值,并确保数据库字段长度足够容纳新格式哈希——这点在迁移到 Argon2 时尤其容易翻车。



















