签名不一致主因是参数排序与处理不统一:需用uksort+strcmp强制ascii序、过滤空值、时间戳用客户端原始值、http_build_query拼串、hash_hmac密钥严格对齐且trim、禁用md5/sha1、json body用json_encode标准化、明确签名覆盖范围、记录原始签名串比对。

签名生成必须用确定性排序的参数
服务端和客户端对同一组参数生成不同签名,八成是因为参数顺序不一致。PHP 的 ksort() 默认按字典序排序键名,但遇到数字键、大小写混合键或中文键时行为不稳定;更稳妥的是用 uksort($params, 'strcmp') 强制 ASCII 字节序,或者统一转成小写再排。
- 所有参与签名的参数必须先过滤空值(
array_filter($params, function($v) { return $v !== null && $v !== ''; })),否则''和unset在两端表现可能不一致 - 时间戳字段(如
timestamp)建议服务端校验有效期(±5 分钟),但签名计算时必须用客户端传入的原始值,不能替换成服务端当前时间 - 签名前拼接字符串推荐用
http_build_query($sorted_params, '', '&', PHP_QUERY_RFC3986),避免手拼导致空格/斜杠编码差异
hash_hmac 用法和密钥处理要严格对齐
客户端和服务端用 hash_hmac('sha256', $data, $secret) 是最常见做法,但容易出错的点在于:密钥是否 base64 解码、是否 trim 空格、是否被 URL 解码过。比如服务端配置里写的是 base64:xxxx,而客户端直接当明文用了,签名必然失败。
- 密钥建议统一走环境变量加载,避免硬编码;读取后立即
trim()并检查长度(SHA256 推荐密钥至少 32 字节,太短会降低安全性) - 不要用
md5()或sha1()做签名摘要,它们已被证明不安全;也不要用hash('hmac-sha256', ...)—— 这个函数不存在,正确是hash_hmac('sha256', ...) - 签名结果统一转小写十六进制(
bin2hex()),不要用strtoupper()或 base64 编码,除非双方明确约定
验签时别忽略请求体和查询参数的混合场景
很多接口同时接受 GET 参数和 JSON body,但签名只覆盖其中一部分。典型错误是:客户端把 sign 放在 query string 里,却把 body 里的字段漏进签名串;或者服务端用 $_GET 拼参,却忘了合并 file_get_contents('php://input') 解析出的 JSON 字段。
- 明确约定签名覆盖范围——是仅 query?仅 body?还是两者合并?建议在文档里写死字段白名单,比如必须包含
app_id、timestamp、nonce、data - 如果 body 是 JSON,服务端解析后必须用
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)再参与签名,否则换行、空格、Unicode 转义会导致两端不一致 - 不要信任
$_REQUEST,它混了 GET、POST、COOKIE,顺序不可控;验签前务必手动合并并去重
调试阶段一定要打原始签名串日志
线上验签失败却找不到原因?大概率是某一方悄悄改了参数名、加了默认值、或中间件自动注入了 header 字段(比如 X-Forwarded-For)。最有效的办法是在客户端和服务端都记录「最终参与签名的原始字符串」,而不是只记签名结果。
立即学习“PHP免费学习笔记(深入)”;
- 服务端验签前加一行:
error_log('[SIGN_DEBUG] raw: ' . $raw_string . ' | sign: ' . $_GET['sign']); - 客户端发请求前也 log 同样的
$raw_string,两边对照——只要这个字符串一致,签名结果就一定一致 - 注意别把密钥打到日志里;也别在生产环境长期开启,只用于定位问题
签名本身不难,难的是两端对“哪些数据、以什么格式、按什么顺序”达成完全一致。任何看似微小的编码、排序、过滤差异,都会让 hash_hmac 输出彻底不同。











