sha1()的计算目标
sha1()对输入字符串执行SHA-1(SecureHashAlgorithm1)单向散列运算,返回固定长度的“指纹”。SHA-1产生160位(20字节)的输出,默认以40个十六进制小写字符表示。
<?php
// 代码号学习PHP:基础哈希计算
echo sha1("Hello PHP");
// a02e266c6f3a8ff0c4250e502828c4ebf179d252
// 第二个参数为 true 时返回原始二进制
echo bin2hex(sha1("Hello PHP", true));
// a02e266c6f3a8ff0c4250e502828c4ebf179d252
第二个参数raw控制输出格式:true返回20字节的原始二进制字符串,false(默认)返回40字符的十六进制表示。两者代表相同的摘要值,区别仅在编码形式。
空字符串与类型转换的边界行为
sha1()对输入执行字符串强制转换。理解这些转换规则对调试认证逻辑尤为重要。
<?php
// 代码号学习PHP:空值哈希的同一性
echo sha1(""); // da39a3ee5e6b4b0d3255bfef95601890afd80709
echo sha1(NULL); // da39a3ee5e6b4b0d3255bfef95601890afd80709
echo sha1(false); // da39a3ee5e6b4b0d3255bfef95601890afd80709
空字符串、null和false经字符串转换后均为空字符串,得到相同的哈希值。这导致一个经典:如果系统中两个用户的密码均为空,sha1($stored)==sha1($input)会返回true。PHP手册的社区注释建议对空密码单独判断,而非依赖哈希比较。
数值类型则产生不同结果:sha1(0)的哈希为b6589fc6...,与sha1("0")一致,但与sha1(false)不同。sha1(1)与sha1(true)则得到相同哈希,因为true转换为字符串"1"。
PHP版本差异:一个真实的兼容性案例
sha1()本身自PHP4.3.0起保持稳定,但输入字符串的解析可能因PHP版本而异。StackOverflow上的一个案例展示了这个问题:
<?php
define('SECRET', "vJs;ly-W\XDkD_2'-M7S2/ZRRBobxt5");
echo sha1(SECRET . '...');
同一段代码在PHP5.1.6和PHP5.3.2上产生不同结果。根因是双引号字符串中转义序列解析规则的变化:PHP5.3开始识别\X为十六进制转义(此前仅识别\x),导致字符串内容不同。这是PHP字符串解析器的变更,而非sha1()函数本身的行为差异。
单元测试与边界验证
基础一致性测试:
<?php
// 代码号学习PHP:sha1 与 hash('sha1') 一致性
$testCases = ["", "a", "Hello PHP", str_repeat("x", 1000)];
foreach ($testCases as $input) {
$sha1Result = sha1($input);
$hashResult = hash('sha1', $input);
assert($sha1Result === $hashResult, "Mismatch for: {$input}");
}
// raw 输出与 hex 输出的关系
$hex = sha1("test");
$raw = sha1("test", true);
assert(bin2hex($raw) === $hex);
sha1()与hash('sha1',$string)产生相同的结果,两者底层调用同一实现。
安全边界:为什么不应使用sha1()存储密码
sha1()的设计目标是快速计算,这一特性在密码存储场景中构成了根本性缺陷。SHA-1在现在GPU上的计算速度可达每秒数十亿次,这意味着破解和字典攻击的成本极低。
OWASP的移动应用安全文档明确指出:SHA-1和MD5存在的碰撞攻击,NIST已将其从所有安全用途中弃用。微软的PowerShell分析规则同样将SHA-1标记为“密码学破解的哈希算法”,仅在遗留系统向后兼容的场景下允许使用。
正确做法:使用password_hash()配合PASSWORD_DEFAULT(当前为bcrypt)或PASSWORD_ARGON2ID,并使用password_verify()验证。
<?php
// 代码号学习PHP:密码存储的正确方式
$hash = password_hash($userInput, PASSWORD_DEFAULT);
// 存储 $hash 到数据库
if (password_verify($userInput, $storedHash)) {
// 验证通过
}
sha1()与hash()的职责对比
| 维度 | sha1() | hash('sha1',...) | hash_hmac() |
|---|---|---|---|
| 算法支持 | 仅SHA-1 | 全部已注册算法 | HMAC密钥哈希 |
| 引入版本 | PHP4.3.0 | PHP5.1.2 | PHP5.1.2 |
| 用途 | 兼容旧代码、非安全校验 | 通用哈希计算 | 带密钥的消息认证 |
hash()函数提供了统一的接口,支持SHA-256、SHA-512、SHA-3等更安全的算法。对于新项目,hash('sha256',$data)是SHA-1的直接替代,输出64字符十六进制。如果需要密钥验证(如API签名),应使用hash_hmac()而非将密钥拼接进消息再哈希——后者容易受到长度扩展攻击。
常见误区速查
| 误区 | 实际行为 |
|---|---|
sha1("")返回空字符串 |
返回da39a3ee...,空输入的哈希 |
sha1()可以用于密码存储 |
速度过快,应使用password_hash() |
sha1($data.$key)安全等价于HMAC |
易受长度扩展攻击,应用hash_hmac() |
| raw输出可以直接用于显示 | 二进制字符串含不可打印字符,通常需bin2hex()转换 |
| 十六进制输出大小写敏感 | sha1()默认返回小写,输入计算时统一 |
现在框架与实践建议
Laravel、Symfony等框架的密码认证层均使用password_hash(),不再依赖sha1()。在需要文件完整性校验的场景中,sha1_file()或hash_file('sha256',...)是合适的工具。sha1()的合理使用范围限于:与遗留系统交互时的兼容哈希、非安全场景的数据指纹(如缓存键生成)、以及测试中构造预期值。
延伸练习
练习一:验证sha1()对空字符串、null、false和0的哈希结果,解释为什么sha1("")和sha1(NULL)相同但sha1(0)不同。
练习二:编写一个函数,接收用户密码,先使用sha1()模拟旧系统的验证逻辑,然后在首次成功登录时用password_hash()重新哈希存储,实现平滑迁移。
练习三:对比sha1($data)与hash_hmac('sha1',$data,$key)在密钥泄露场景下的安全性差异,理解长度扩展攻击的基本原理。
参考文献
PHP 官方手册:sha1()
https://www.php.net/manual/en/function.sha1.php
官方函数文档,包含函数签名、raw参数说明、空字符串哈希的社区讨论,以及关于长度扩展攻击的安全提醒。
PHP 官方手册:Hash 函数
https://www.php.net/manual/en/ref.hash.php
哈希函数总览,列出hash()、hash_hmac()、hash_file()等函数的职责,说明 SHA-1 在hash()中的可用性。
OWASP MASWE-0008: Improper Hashing
https://mas.owasp.org/MASWE/MASVS-CRYPTO/MASWE-0008/
OWASP 移动应用安全文档,明确SHA-1因碰撞攻击被NIST弃用,不得用于安全敏感场景,密码存储必须使用专用KDF。
Microsoft PowerShell AvoidUsingBrokenHashAlgorithms
https://learn.microsoft.com/zh-cn/powershell/utility-modules/psscriptanalyzer/rules/avoidusingbrokenhashalgorithms
微软分析规则文档,将SHA-1和MD5标记为密码学破解算法,仅在遗留系统兼容场景下允许使用。
PHP 手册社区注释:sha1("") 行为
https://www.php.net/manual/en/function.sha1.php#34043
官方手册社区注释,记录空字符串、null、false和数值类型哈希的精确值,以及空密码比较的陷阱和修正方案。