支付宝参数值以@开头导致回调签名错误
结论
问题通常不在支付宝的业务规则,而在部分支付宝 SDK 的兼容逻辑。这些 SDK 会沿用旧版 cURL 的文件上传语法,把以 @ 开头的值当作文件路径,并在生成待签名字符串时排除对应字段。
如果普通业务数据恰好以 @ 开头,请求方与支付宝参与签名的字段可能不一致,最终导致回调验签失败。
建议先升级支付宝官方 SDK。如果当前版本仍有这类逻辑,可对业务值进行可逆编码,保证传给支付宝的参数值不以 @ 开头。不要关闭回调验签,也不要修改回调参数后再验签。
原因
旧版 PHP cURL 使用下面的写法上传文件:
'file' => '@/path/to/file.jpg'
为排除文件内容,一些早期 SDK 在拼接待签名字符串时,会跳过值以 @ 开头的参数。代码通常类似:
foreach ($params as $key => $value) {
if (substr($value, 0, 1) !== '@') {
$signContent .= $key . '=' . $value . '&';
}
}
文件上传参数一般不直接参与普通文本参数签名,但这段逻辑无法区分以下两种值:
@/tmp/example.jpg
@customer_001
前者可能是待上传文件,后者只是普通业务数据。如果 SDK 不作区分并全部排除,生成的待签名字符串就会缺少字段。
具体行为与开发语言、SDK 版本和接口类型有关。出现这个问题,并不代表支付宝网关会统一过滤所有以 @ 开头的字段。判断时应以项目实际使用的 SDK 签名和验签实现为准。
解决步骤
1. 确认字段在哪一侧被排除
分别记录以下内容:
- 发起请求前的原始参数;
- SDK 实际生成的待签名字符串;
- 支付宝回调的原始参数;
- 回调验签时生成的待验签字符串。
检查以 @ 开头的字段是否出现在待签名字符串中,以及请求和回调两侧的处理是否一致。
日志中不要记录私钥、完整的用户隐私数据或其他敏感信息。
2. 升级官方 SDK
先升级到适用于当前项目的支付宝官方 SDK,再检查其中的签名实现。不要直接复制其他 SDK 版本的签名代码。不同接口对字符集、嵌套 JSON、文件参数和特殊字段的处理可能并不相同。
如果升级后的官方实现能够正确区分文件参数与普通字符串,直接使用该实现即可。
3. 对业务值进行可逆编码
如果暂时无法升级,可以让发送值不再以 @ 开头。比如使用 Base64 编码,并加上固定前缀:
<?php
$originalValue = '@customer_001';
$encodedValue = 'b64:' . base64_encode($originalValue);
// 将 $encodedValue 作为业务参数发送
$params['external_id'] = $encodedValue;
收到回调并完成验签后,再进行解码:
<?php
$value = $notifyParams['external_id'] ?? '';
if (str_starts_with($value, 'b64:')) {
$originalValue = base64_decode(substr($value, 4), true);
if ($originalValue === false) {
throw new RuntimeException('Invalid Base64 value');
}
}
固定前缀既能避免把编码结果误认为原始文本,也能确保参数值不会以 @ 开头。
如果业务参数支持 JSON,也可以使用结构化封装:
<?php
$params['biz_ext'] = json_encode(
['value' => '@customer_001'],
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
封装后,参数值以 { 开头,不会触发针对 @ 的旧式过滤逻辑。
4. 先验签,再处理业务数据
回调应按以下顺序处理:
<?php
// 保留支付宝回调的原始参数用于验签
$notifyParams = $_POST;
$verified = $aopClient->rsaCheckV1(
$notifyParams,
$alipayPublicKey,
'RSA2'
);
if (!$verified) {
throw new RuntimeException('Alipay notify signature verification failed');
}
// 验签成功后,才能解码和处理业务字段
$value = $notifyParams['external_id'] ?? '';
if (str_starts_with($value, 'b64:')) {
$value = base64_decode(substr($value, 4), true);
if ($value === false) {
throw new RuntimeException('Invalid business parameter');
}
}
实际使用的验签方法名和参数顺序,应以项目采用的官方 SDK 为准。
不建议的处理方式
不要只对 @ 进行 URL 编码,就认为问题已经解决:
@customer_001
%40customer_001
请求解析时通常会还原 URL 编码。签名究竟按编码前还是解码后的值计算,也取决于 SDK 和接口规则。因此,只把 @ 替换成 %40,无法保证双方生成的待签名内容一致。
也不要删除 SDK 中所有涉及 @ 的判断。如果项目确实包含文件上传接口,这种修改可能破坏文件参数的签名或上传流程。确实需要修补 SDK 时,只应排除明确的文件字段,而不能仅根据参数值的首字符作判断:
<?php
$fileFields = ['file_content'];
foreach ($params as $key => $value) {
if (in_array($key, $fileFields, true)) {
continue;
}
// 按接口签名规范继续拼接
}
这类改动需要结合具体接口文档和当前 SDK 实现进行验证,不能作为所有支付宝接口的通用补丁。
注意事项
- 回调验签应使用支付宝公钥,不能使用应用公钥或应用私钥。
- 验签前不要修改字段值,也不要对整个回调数组重复执行 URL 解码。
sign和sign_type是否参与待验签字符串,应严格遵循官方 SDK 和接口规范。- 字符集、空值处理、参数排序和 JSON 序列化结果都可能影响签名。
- 不要通过跳过验签来解决问题,否则攻击者可能伪造支付成功通知。
- 最好限制业务字段的格式,避免使用具有协议含义的首字符。如果无法限制,应在发送端统一编码,并在验签成功后解码。
备注:内容仅供参考。
