PAYATHON 2026

支付宝参数值以@开头导致回调签名错误

支付小周

结论

问题通常不在支付宝的业务规则,而在部分支付宝 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 序列化结果都可能影响签名。
  • 不要通过跳过验签来解决问题,否则攻击者可能伪造支付成功通知。
  • 最好限制业务字段的格式,避免使用具有协议含义的首字符。如果无法限制,应在发送端统一编码,并在验签成功后解码。

备注:内容仅供参考。