一、函数定位与语法
crypt()是PHP字符串函数库中用于单向哈希的函数。它接受一个明文字符串和一个可选的盐值(salt),返回哈希后的字符串。所谓单向,是指无法从哈希结果反推出原始输入——这是密码存储的基本前提。
语法结构:
string crypt ( string $str [, string $salt ] )
| 参数 | 说明 | 是否必需 |
|---|---|---|
$str |
待哈希的字符串 | 必需 |
$salt |
盐值字符串,决定使用哪种算法 | 可选 |
盐值在crypt()中不只是"加点随机数据"那么简单。盐值的格式决定了crypt()使用哪种哈希算法。这是理解该函数的核心,也是它与其他哈希函数(如md5、sha1)较大的区别。
crypt()的行为依赖底层操作系统的C库实现。不同Unix系统支持的算法种类可能不同,这也是为什么PHP提供了CRYPT_*系列常量用于检测当前环境支持哪些算法。
二、支持的哈希算法与常量
PHP定义了一组常量,值为1表示当前系统支持对应算法:
| 常量 | 算法 | 盐值格式特征 |
|---|---|---|
| CRYPT_STD_DES | 标准DES | 2个字符 |
| CRYPT_EXT_DES | 扩展DES | 9个字符,以_开头 |
| CRYPT_MD5 | MD5 | 以$1$开头 |
| CRYPT_BLOWFISH | Blowfish | 以$2a$、$2y$等开头 |
| CRYPT_SHA_256 | SHA-256 | 以$5$开头 |
| CRYPT_SHA_512 | SHA-512 | 以$6$开头 |
每种算法对盐值长度和格式有不同要求。如果盐值格式不符合任何已知算法,crypt()会退回到标准DES,只取盐值前2个字符。这个"降级"行为在排查问题时经常被忽略。
三、代码号学习编程:各算法调用示例
标准DES(2字符盐值):
<?php
if (CRYPT_STD_DES == 1) {
echo "Standard DES: " . crypt('codehao', 'jt') . "\n";
} else {
echo "Standard DES not supported.\n";
}
标准DES的盐值只有2个字符,哈希空间有限。现在密码存储不应使用它,这里仅作原理演示。
MD5算法(11前缀):
<?php
if (CRYPT_MD5 == 1) {
echo "MD5: " . crypt('codehao', '$1$codehaolearn$') . "\n";
}
盐值以$1$开头,PHP会按MD5-crypt格式解析。输出的哈希串同样以$1$开头,包含盐值和哈希结果。
Blowfish算法(2a2a前缀):
<?php
if (CRYPT_BLOWFISH == 1) {
$salt = '$2a$09$anexamplestringforsalt$';
echo "Blowfish: " . crypt('codehao', $salt) . "\n";
}
$2a$后面的09是cost参数,表示迭代次数的对数(2^9=512轮)。cost越高,计算越慢,破解成本越大。Blowfish的盐值需要22个base64字符,格式要求较严格。
SHA-256算法(55前缀):
<?php
if (CRYPT_SHA_256 == 1) {
$salt = '$5$rounds=5000$anexamplestringforsalt$';
echo "SHA-256: " . crypt('codehao', $salt) . "\n";
}
rounds=5000指定迭代轮数,默认值就是5000。可以调高到更大值来增加计算成本。
SHA-512算法(66前缀):
<?php
if (CRYPT_SHA_512 == 1) {
$salt = '$6$rounds=5000$anexamplestringforsalt$';
echo "SHA-512: " . crypt('codehao', $salt) . "\n";
}
SHA-512与SHA-256的调用方式接近,只是前缀和输出长度不同。
四、项目反思:crypt()的历史地位与现实选择
我在2026年接手一个2010年前后开发的用户系统,密码字段用的是crypt()配合MD5格式存储。这段维护经历让我对crypt()的定位有了具体认识。
踩坑经历一:盐值格式决定一切
系统日志中偶尔出现"密码验证失败"的投诉,但用户坚称密码没错。排查后发现,部分老用户的密码哈希是用标准DES生成的,而新代码统一用MD5格式的盐值调用crypt()。由于crypt()只根据盐值判断算法,验证老密码时传入MD5格式盐值,计算出的哈希自然对不上。解决方案是先读取数据库中存储的哈希前缀($1$、$5$等),用相同的格式构造盐值再验证。
踩坑经历二:跨平台兼容性
crypt()依赖操作系统的crypt实现。同一段代码在Linux和macOS上支持的算法可能不同。我曾遇到在开发机(macOS)上Blowfish正常,部署到某台老Linux服务器后CRYPT_BLOWFISH返回0,函数自动降级到DES,导致新注册用户的密码哈希强度骤降。部署前检查CRYPT_*常量的值,应该成为上线流程的一部分。
踩坑经历三:为什么现在不直接用它
PHP5.5引入了password_hash(),它封装了crypt()的底层能力,但做了几件关键的事:自动生成安全盐值、自动选择当前推荐的算法、提供统一的验证接口。相比之下,手动调用crypt()需要开发者自己处理盐值生成、算法选择、格式解析,出错空间大。
我后续把所有密码存储逻辑迁移到password_hash()+password_verify(),代码量减少,安全性反而提升。crypt()在这个场景中已经不需要直接出现。
个人建议:新项目一律使用password_hash()。crypt()的价值在于理解密码哈希的底层机制,以及在维护老系统时能够正确解析和迁移历史数据。不要在新代码中手写crypt()调用。
五、本节课程知识要点
-
crypt()是单向哈希函数,盐值格式决定使用的算法。
-
支持DES、扩展DES、MD5、Blowfish、SHA-256、SHA-512。
-
盐值格式不符合已知算法时会降级到标准DES。
-
crypt()行为依赖操作系统实现,跨平台部署需检查CRYPT_*常量。
-
新项目应使用password_hash()和password_verify()。
-
维护老系统时,需按存储的哈希前缀构造盐值进行验证。
六、crypt()与password_hash()对比
| 维度 | crypt() | password_hash() |
|---|---|---|
| 盐值生成 | 手动 | 自动 |
| 算法选择 | 靠盐值格式 | 自动选择推荐算法 |
| 验证方式 | 手动比对哈希串 | password_verify() |
| 错误处理 | 无 | 有返回值语义 |
| 算法升级 | 需手动迁移 | password_needs_rehash() |
| 推荐场景 | 遗留系统维护 | 新项目 |
password_hash()目前默认使用bcrypt(即Blowfish的现在形式),并支持Argon2。它把盐值生成、算法标识、哈希结果编码进一个字符串,验证时自动解析,开发者不需要接触底层细节。
七、一个容易忽略的细节
crypt()返回的哈希串中已经包含了盐值。以MD5格式为例,输出$1$codehaolearn$/QyBQ/V7dJjcGaOq83EhL0中,$1$是算法标识,codehaolearn是盐值,后面的部分是哈希结果。验证密码时不需要单独存储盐值——直接从哈希串中提取即可。这也是为什么数据库只需存一个字段,而不是"哈希+盐"两个字段。
但要注意,Blowfish和SHA-2系列的盐值格式更复杂,手动解析容易出错。如果必须处理老数据,建议用password_verify()尝试验证——它内部能识别多种crypt格式。