hash_hmac比md5或sha1更安全,因其采用RFC 2104标准的“密钥前置+两次哈希”机制,可防御长度扩展攻击和哈希碰撞;而md5($data.$secret)易被破解且不满足HMAC规范。

为什么 hash_hmac 是比 md5 或 sha1 更安全的选择
直接用 md5($data . $secret) 签名 AI API 请求,等于把密钥暴露在哈希碰撞和长度扩展攻击的风险里。hash_hmac() 内部采用「密钥前置 + 两次哈希」的 HMAC 标准(RFC 2104),能有效防御这类攻击。PHP 从 5.1.2 起内置支持,无需额外扩展。
常见错误是误用 hash() 替代 hash_hmac(),比如写成 hash('sha256', $secret . $data) —— 这不是 HMAC,也不等价,签名可被伪造。
-
hash_hmac()第二个参数是原始密钥(不要 base64_decode 或 hex2bin 预处理,除非你明确知道对方要求) - 算法选
'sha256'足够,'sha512'在多数 AI API 场景下无实际收益,反而增加计算开销 - 输出格式统一用
raw_output = false(默认),得到 64 字符十六进制字符串,便于 HTTP header 传输
构造签名时必须包含哪些字段
AI API 的请求体(body)常含 timestamp、nonce、model、prompt 等动态内容。只对 body 做签名,忽略 header 或 query,会导致服务端校验失败;但若把整个 cURL 请求 dump 出来签名,又会因 header 顺序/空格差异导致不一致。
正确做法是:和服务端约定一个**确定性签名原文(canonical string)**。典型结构如下:
立即学习“PHP免费学习笔记(深入)”;
$canonical = implode("\n", [
$http_method, // 'POST'
$path, // '/v1/chat/completions'
$query_string, // 'temperature=0.7&stream=false'(需按字母序排序)
$content_type, // 'application/json'
$body_json // 原始 JSON 字符串,不缩进、不空格
]);关键点:
-
$query_string必须用http_build_query()生成后,再按 key 字母序排序(ksort()),否则不同语言 SDK 生成结果不一致 -
$body_json不能用json_encode($arr, JSON_UNESCAPED_UNICODE)后再 trim —— 要确保和服务端完全相同的编码方式(例如是否转义斜杠/) - timestamp 建议用秒级 Unix 时间戳(
time()),避免毫秒导致服务端时钟漂移校验失败
如何防止重放攻击(replay attack)
HMAC 本身只防篡改,不防重放。AI API 若允许同一签名重复使用,攻击者截获一次请求就能反复调用,绕过计费或限流。
必须配合时间窗口 + nonce 校验:
- 在
$canonical中加入$timestamp,服务端检查abs($server_time - $timestamp) (5 分钟) - 添加一次性随机字符串
$nonce = bin2hex(random_bytes(16)),并存入 Redis 设置 5 分钟过期;服务端先查是否存在,存在则拒绝 - 不要用
microtime(true)当 nonce —— 太容易预测,且高并发下可能重复
注意:$nonce 必须参与签名原文构造,否则攻击者可替换它而不破坏签名。
调试签名不匹配时的排查顺序
最常遇到的是「本地算出的签名,服务端校验失败」。别急着怀疑密钥,先按这个顺序查:
- 确认服务端使用的算法和 PHP 完全一致:
hash_hmac('sha256', $msg, $key, false)—— 尤其注意第四个参数false(hex)还是true(raw) - 用
var_dump($canonical)打印签名原文,复制到服务端 debug 日志中对比 —— 常见差异:换行符是\n还是\r\n、JSON 中是否有不可见 Unicode 字符、query string 是否漏了空值参数 - 检查密钥是否被意外 trim() 或包含 BOM —— 用
bin2hex($key)看开头是不是'efbbbf' - 确认服务端解析 body 时没有自动修改 JSON(如把数字转成字符串、删掉尾随逗号)
真正麻烦的点往往不在哈希函数本身,而在「你以为一致、其实不一致」的字符串构造细节上。



















