ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP crypt()函数的用法讲解

PHP crypt()函数的用法讲解 前言crypt()是 PHP 里历史最悠久的字符串函数之一它直接来自 C 标准库的crypt(3)被设计出来只为一件事把用户口令变成一段不可逆的散列值写进类似 Unix 口令文件那样的存储里。围绕它有两个非常普遍的误解。第一个是把crypt()当成「加密函数」以为加密之后还能解密回来——它不是加密而是单向散列one-way hashing没有密钥也没有逆运算salt 只影响「同一个密码得到不同的散列」以及「一次散列要花多少计算量」。第二个误解是把它当成今天存密码的推荐做法——这也是错的PHP 5.5 起提供的password_hash()/password_verify()已经完全覆盖了crypt()的用途而且替你把 salt 生成、算法前缀、失败检测都处理好了。本文要讲清三件事crypt()的签名以及 salt 前缀如何决定算法、PHP 8.0 对它的行为改动、以及在必须面对老代码和老数据时怎么安全地过渡到password_hash()。一、签名与 salt 前缀决定算法crypt(string $string, string $salt): string注意第三个事实crypt()没有「算法」参数。用哪个算法完全由salt字符串的开头几个字符决定。这是 Unix 口令文件格式留下的设计也是所有初学困惑的根源。salt 形式对应常量说明两个普通字符如rlCRYPT_STD_DES传统 DES只取密码前 8 字节输出 13 字符强度早已不足_J9..开头的 9 字符CRYPT_EXT_DES扩展 DES$1$开头CRYPT_MD5MD5 cryptsalt 最多 8 字符输出 34 字符$2y$开头CRYPT_BLOWFISHbcrypt本节重点$5$开头CRYPT_SHA256SHA-256 cryptsalt 最多 16 字符$6$开头CRYPT_SHA512SHA-512 cryptsalt 最多 16 字符这些常量不是「配置项」而是探测结果它们只有在当前系统的 libcrypt 支持该算法时才被定义。所以判断某个算法能不能用必须写成?php // 适用于 PHP 7.0// 常量只在系统支持时才存在直接用会报 undefined constantif (defined(CRYPT_BLOWFISH) CRYPT_BLOWFISH) {echo bcrypt 可用\n;}二、bcrypt 的 salt 长什么样bcrypt 的 salt 不是「随便一串字符」它有严格格式拼错了crypt()会返回一个失败值而不是抛出异常——这正是最容易造成「密码存进去了但谁也登不上」的地方。格式是$2y$ 两位 cost $ 22 个来自./A-Za-z0-9的字符。总共 29 个字符输出固定 60 个字符。?php // 适用于 PHP 8.0// 生成一个符合 bcrypt 规范的 salt$cost 10;$salt sprintf($2y$%02d$, $cost). substr(strtr(base64_encode(random_bytes(16)), , .), 0, 22);$hash crypt(correct horse battery staple, $salt);// 校验格式bcrypt 的输出一定是 60 个字符且以 $2y$ 开头var_dump(strlen($hash)); // int(60)var_dump(str_starts_with($hash, $2y$)); // bool(true)// 校验密码把「已存库的完整散列」当成 salt 再算一次$ok hash_equals($hash, crypt(correct horse battery staple, $hash));var_dump($ok); // bool(true)var_dump(hash_equals($hash, crypt(wrong, $hash))); // bool(false)两个细节值得说明。第一random_bytes()在 PHP 7.0 才可用它拿不到足够熵时会抛Exception这比mt_rand()拼 salt 安全得多mt_rand()是伪随机数绝不可用于安全场景。第二比较必须用hash_equals()而不是或因为字符串比较会提前返回理论上可被时序攻击timing attack逐字节试探。三、PHP 8.0 的行为变化与失败返回值crypt()的失败信号非常隐蔽。手册的说明是失败时返回一个短于 13 个字符、且保证与该 salt 不同的字符串。也就是说它不会返回false也不会抛异常你拿到的仍然是一个「看起来像字符串」的东西。?php // 适用于 PHP 8.0// 故意给一个不合法的 saltbcrypt 前缀但 salt 太短$bad crypt(password, $2y$10$tooshort);var_dump($bad); // 不是 60 字符的合法散列var_dump(strlen($bad) 13); // 失败判据之一版本上要特别注意PHP 5.6 起省略salt参数会触发E_NOTICE安全警告是 notice不是E_DEPRECATED——手册的 changelog 写的是「Raise E_NOTICE security warning if salt is omitted」。PHP 8.0 起salt参数不再可以省略必须显式传入。此时它已经是签名里的必填参数漏传不再是警告而是直接抛ArgumentCountErrorcrypt() expects exactly 2 parameters, 1 given。老代码里crypt($password)这种单参数写法在 PHP 8 上会中断执行不能照抄。mcrypt_*系列是另一回事它在 PHP 7.2 被废弃、7.2 之后需要 PECL 安装已不适合新项目。但它和crypt()无关——crypt()一直属于标准库。四、为什么新代码应该直接写 password_hash()把上面几节的内容合起来看crypt()要求调用者自己负责 salt 生成、格式拼接、失败检测、常量时间比较——任何一步写错都可能是「静默失效」的。password_hash()把这一整套封装掉了?php // 适用于 PHP 7.4PASSWORD_DEFAULT 推荐用 PHP 8.0 的默认 bcrypt$hash password_hash($password, PASSWORD_DEFAULT);// 校验password_verify() 会自动识别 $2y$ / $argon2i$ 等前缀if (password_verify($password, $hash)) {echo 密码正确\n;}// 参数升级例如以后把 cost 调高后用 needs_rehash 检测if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {$newHash password_hash($password, PASSWORD_DEFAULT);// 此处应把 $newHash 写回数据库}// 想用 Argon2id 需要 PHP 7.3 且编译时带 Argon2 支持if (defined(PASSWORD_ARGON2ID)) {$argon password_hash($password, PASSWORD_ARGON2ID);}还有一个实用的过渡点password_verify()可以校验crypt()生成的 bcrypt 散列因为两者用的是同一套格式。所以老系统迁移时不必逼所有用户立刻改密码——先用password_verify()接管校验登录成功时判断password_needs_rehash()顺手把散列升级成password_hash()的输出即可。另外要澄清一个容易误解的点bcrypt 只看密码的前 72 字节超出部分被直接忽略。这是算法本身的限制password_hash()用 bcrypt 时也受同样约束如果确实需要超长口令得先用hash(sha256, $pwd, true)之类做一次预处理再交给 bcrypt但这样会引入自己的兼容性问题普通业务不必这么做。常见坑点❌ 用密码自己当 saltcrypt($password, $password)想靠「算出来一样就说明对」来验证。这样每次 salt 都不同永远对不上。✅ 校验时把存库的完整散列当 salthash_equals($stored, crypt($password, $stored))或者直接用password_verify($password, $stored)。❌ 用比较散列if ($stored crypt($input, $stored))。字符串比较不是常量时间。✅hash_equals($stored, $computed)。❌ 省略 salt$h crypt($password);。PHP 5.67.x 只是发E_NOTICE容易被错误抑制掩盖PHP 8.0 起salt变成必填参数直接抛ArgumentCountError脚本中断。✅password_hash($password, PASSWORD_DEFAULT)完全不用自己拼 salt。❌ salt 里混入 base64 的//~等非 bcrypt 字母表字符crypt()静默返回失败值代码却当成功写库。✅ 生成后校验输出必须以$2y$开头且长度为 60否则当成错误处理。❌ 把crypt()当可逆加密到处找「解密」办法。✅ 它是单向散列。真要可逆用sodium_crypto_secretbox()PHP 7.2 内置或openssl_encrypt()并且每次都用随机 nonce / IV。❌ 用$2a$或$2x$前缀以为和$2y$等价。$2x$/$2y$是针对早期实现中符号扩展缺陷的修正变体混用会导致同一密码在不同实现上算出不同结果。✅ 新代码统一使用$2y$或干脆交给password_hash()。❌ 用md5($password)/sha1($password)存密码或者用md5($salt . $password)自己「加盐」。✅ 快哈希MD5/SHA-1/SHA-256在这类场景下就是错误工具它们是设计成「快」的。存密码只能用password_hash()/password_verify()。❌ 认为 13 字符的 DES crypt 输出仍然够安全拿它存新密码。✅CRYPT_STD_DES只使用密码前 8 个字符且密钥空间极小现代机器可暴力枚举只能用于识别老数据不能用于新数据。总结关注点老写法现行推荐生成散列crypt($pwd, $salt)自己拼 saltpassword_hash($pwd, PASSWORD_DEFAULT)校验密码crypt($pwd, $hash) $hashpassword_verify($pwd, $hash)常量时间比较hash_equals()参数升级手工重建 saltpassword_needs_rehash() 重新password_hash()算法DES / MD5 cryptbcrypt默认或 Argon2idPHP 7.3 且编译支持可逆加密误用crypt()sodium_crypto_secretbox()/openssl_encrypt() 随机 IVcrypt()不是坏函数但它把太多安全细节暴露给了调用者任何一个细节写错都是静默失效。新代码直接使用password_hash()/password_verify()只有在维护老系统的存量 bcrypt 散列时才需要理解 salt 前缀这套机制而且理解它的最好用途就是——把数据平滑迁移出去。
返回列表