前言
crc32()计算一段字符串的 CRC-32 校验值(cyclic redundancy check,循环冗余校验),返回一个 32 位整数。它最常见的用途是「快速判断两段数据是不是同一份」:缓存键的后缀、ETag、内容去重、短链接的 ID 前缀,以及检测传输或存储过程中有没有发生意外的字节损坏。
这里必须先纠正一个非常常见的误解:CRC-32 是校验和,不是哈希,更不是加密。它的设计目标是「发现随机错误」,而不是「抵抗蓄意篡改」。任何人都可以针对一个给定的 CRC-32 值构造出大量碰撞数据,因为 CRC 是线性的。所以它能告诉你「文件传丢了几个字节」,但绝不能告诉你「这个数据没有被改过」——后者要用hash('sha256', ...)或hash_hmac()。
另一个高频困惑是返回值有时是负数。这不是 bug,而是 32 位平台上整数符号性导致的,本文会给出统一的无符号表示办法。
一、签名与返回值
crc32(string $string): int它接受字符串,返回整数。需要明确的几点:
- CRC-32 是确定性的:相同输入永远得到相同输出,没有任何随机成分或 salt。这既是它有用的原因,也是它不能用于安全场景的原因。
- 它使用的多项式与 gzip、PNG 等格式里的 CRC-32 一致,在
hash()里对应的算法名是crc32b。 - 输入为空字符串时结果是
0。
在 64 位 PHP 上,int是 64 位有符号整数,而 CRC-32 的值域是 0 到 4294967295,完全放得下,所以结果永远是非负的。但在32 位 PHP上(很多老旧的 x86 服务器、部分嵌入式环境),int是 32 位有符号的,超过 2147483647 的校验值会显示为负数。这会导致「同一份代码在两台机器上算出不同的数」这种极其难查的问题,因为负数一旦被写进数据库或拼进缓存键,两张表就对不上了。
<?php // 适用于 PHP 7.0+
$s = 'hello world';
$c = crc32($s);
printf("默认输出 : %d\n", $c);
printf("无符号十进制 : %u\n", $c); // %u 按无符号解释,32/64 位都稳
printf("十六进制 : %08x\n", $c); // 固定 8 位小写十六进制%u和%08x是跨平台统一表示的稳妥做法:%u把整数按无符号解释,%08x在 32 位和 64 位平台上都会给出 8 位十六进制(因为高 32 位在两种平台上都是 0 或被符号扩展后取低 32 位)。
二、与 hash() 的 crc32b 等价
PHP 的hash()扩展里有一个算法名crc32b,它和crc32()是同一种算法的两种出口:一个给整数,一个给十六进制。
<?php // 适用于 PHP 7.0+
$s = 'hello world';
$intValue = crc32($s);
$hexValue = hash('crc32b', $s);
var_dump($hexValue === sprintf('%08x', $intValue)); // bool(true)这个等价关系有两个实际价值。第一,需要把校验值作为字符串参与比较或存储时,用十六进制更不容易被平台符号性影响。第二,hash_file()支持crc32b这个算法名,可以在不把整个文件读进内存的情况下算文件的 CRC:
<?php // 适用于 PHP 7.0+
// 相对大文件:边读边算,PHP 内部按块处理
$path = __DIR__ . '/data.bin';
if (is_readable($path)) {
$fileCrc = hash_file('crc32b', $path); // 十六进制字符串
printf("%s -> %s\n", basename($path), $fileCrc);
} else {
echo "文件不存在或不可读\n";
}注意文件路径要用__DIR__拼接绝对路径,而不是相对路径——相对路径取决于「当前工作目录」,在 CLI 和 Web 两种运行方式下完全可能不同。
三、典型用法与它适合的边界
适合:
<?php // 适用于 PHP 8.0+
// 1) 给缓存键加一个短后缀,降低不同内容撞到同一个键的概率
$key = 'article:' . $id . ':' . sprintf('%08x', crc32($body));
// 2) 内容去重:先比长度和 CRC,再比完整内容(CRC 只用来快速淘汰)
function sameContent(string $a, string $b): bool
{
if (strlen($a) !== strlen($b)) {
return false;
}
if (crc32($a) !== crc32($b)) {
return false; // 绝大多数不同的内容在这里就被排除了
}
return hash_equals(hash('sha256', $a), hash('sha256', $b)); // 最终裁决
}
// 3) 传输完整性:发送方带 CRC,接收方重算
$payload = 'some data';
$check = sprintf('%08x', crc32($payload));
printf("payload=%s crc=%s\n", $payload, $check);第 2 点是 CRC 正确的用法:把 CRC 当作廉价的预筛选,最终的判定仍然交给密码学哈希。只比 CRC 就断言「内容相同」是不严谨的。
不适合:
- 用户令牌、会话 ID、密码散列——用
random_bytes()和password_hash(),CRC 是可逆推的。 - 防篡改、数据签名——用
hash_hmac()(带密钥)或sodium_crypto_generichash()。 - 「防重复提交」的幂等键——如果用户能猜到该内容对应的 CRC,就能伪造。
四、输出编码与比较的正确姿势
CRC 是用来「比」的,不是用来「看」的,所以比较方式比计算方式更重要。
<?php // 适用于 PHP 8.0+
$expected = 'c87258b7'; // 别处传过来的十六进制校验值
$actual = hash('crc32b', 'hello world');
// ✅ 转成同一种规范形式再比,忽略大小写差异
if (hash_equals(strtolower($expected), strtolower($actual))) {
echo "校验一致\n";
}
// ❌ 用 == 比较字符串与数字,会触发 PHP 8 的语义变化
var_dump('c87258b7' == 0); // bool(false)(PHP 8 中字符串与数字比较已改规则)
var_dump((string) crc32('x') == crc32('x')); // 类型不同,不要这么比PHP 8 修改了字符串与数字的比较规则:数字字符串与数字比较时按数值比较,非数字字符串与数字比较时,数字会被转成字符串再比。这条规则很绕,最稳的做法是两边都显式转成同一种类型,用hash_equals()或===,不要依赖隐式转换。
常见坑点
- ❌
if (crc32($a) == crc32($b))就认定两段内容完全一样。
✅ CRC-32 的碰撞是很容易刻意制造的,它只适合「快速排除不同」。最终判定用hash('sha256', ...)配合hash_equals()。
- ❌ 把
crc32()的结果当作「找不到」的哨兵值:$idx = crc32($key); if ($idx === 0) { /* 当作没有 */ }。
✅ 空字符串的 CRC 就是0,合法数据也会算出0。要区分「没有」,请单独用isset()/array_key_exists()判断,而不是用返回值。
- ❌ 在 32 位机器上直接输出:
echo crc32('hello world');得到负数,写进数据库后再和 64 位机器算的值比较就不相等。
✅ 统一用sprintf('%08x', crc32($s))或sprintf('%u', crc32($s)),把结果规范化成字符串后再存储和比较。
- ❌ 用
crc32()生成短链接 ID 或优惠券码,认为 32 位足够「不可猜」。
✅ 可枚举、可碰撞。需要不可猜就用bin2hex(random_bytes(16)),需要短就用base_convert()或哈希截断并且加上服务端的唯一性校验。
- ❌ 用
crc32()校验「登录令牌有没有被改过」,即crc32($user . $secret)这种自拼的签名。
✅ 这是典型的「自己造 MAC」。用hash_hmac('sha256', $data, $secret),校验时用hash_equals()。
- ❌ 把
hash('crc32b', $s)和hash('crc32', $s)当成同一个东西互换使用。
✅ 只有crc32b与crc32()的十六进制形式对应;hash()里还有其他名字相近的算法,别想当然地等价替换。
- ❌ 用
crc32(file_get_contents($bigFile))校验大文件。
✅ 这会把整个文件读进内存。用hash_file('crc32b', $path),它按块读取。
- ❌
crc32($arr)或crc32(null)直接调用。
✅ 参数是string类型,数组会抛TypeError;null在 PHP 8 会被转换成空字符串并算出0(还会出一条废弃提示),显式传''更清楚。
总结
| 问题 | 结论 |
|---|
| 签名 | crc32(string $string): int |
| 值域 | 0 ~ 4294967295(32 位无符号) |
| 32 位平台 | 结果可能显示为负数,需用%u/%08x规范化 |
与hash() | hash('crc32b', $s)等于sprintf('%08x', crc32($s)) |
| 文件版本 | hash_file('crc32b', $path) |
| 是否安全 | 否。不是哈希算法对抗攻击用的,更不是加密 |
| 正确用途 | 缓存键后缀、内容去重的预筛选、传输完整性检测 |
把crc32()定位成「廉价的完整性探测工具」就不会用错:它能高效地发现意外损坏,但对蓄意篡改毫无抵抗力。凡是涉及安全语义的地方——令牌、签名、密码——一律换成random_bytes()、hash_hmac()或password_hash()。