PAYATHON 2026

个人沙箱调用 alipay.trade.app.pay 提示 invalid-signature

支付阿杰

结论

私钥和公钥能够匹配,只能说明密钥对本身没有问题。出现 invalid-signature,通常是因为支付宝实际收到并用于验签的字符串,与代码签名时使用的字符串不完全一致。

错误信息中的验签字符串没有 biz_content,但 alipay.trade.app.pay 必须提交业务参数。建议先检查以下几项:

  1. biz_content 是否确实发送到了支付宝;
  2. 生成签名后,其他参数是否又被修改;
  3. 是否混用了沙箱和正式环境的 app_id、网关或应用公钥;
  4. sign 是否经过了错误的 URL 编码、Base64 处理或字符集转换;
  5. 是否误把 HTML 中显示的 & 当作实际的参数分隔符。

先核对支付宝收到的验签字符串

报错中的字符串,是支付宝网关根据实际收到的请求参数重新拼接出的待验签内容:

app_id=2016102600766171&charset=utf-8&format=JSON&method=alipay.trade.app.pay&notify_url=http://2324o79f50.iask.in/alipay/notify&sign_type=RSA2&timestamp=2020-11-06 17:55:55&version=1.0

页面显示的 & 只是 HTML 对 & 的转义,不能直接参与签名。

把这段字符串与代码打印出的“签名前原文”逐字符比较。下面这些差异都会造成验签失败:

  • 缺少参数或多出参数;
  • 参数排序不同;
  • 大小写不同;
  • 存在多余的空格、换行或不可见字符;
  • 参数值在签名前已经过 URL 编码;
  • 生成签名后又修改了参数;
  • JSON 的字段顺序、空格或转义方式发生变化。

当前错误中最值得注意的是,验签字符串里没有 biz_content。如果代码签名时包含了 biz_content,发送请求时却没有带上,支付宝用于验签的原文就会与签名原文不同。

正确的参数处理顺序

手动签名时,参数应按以下顺序处理:

  1. 准备全部公共参数和业务参数;
  2. 排除 sign 以及值为空的参数;
  3. 按参数名的 ASCII 顺序排序;
  4. 使用参数原始值拼接待签名字符串;
  5. 使用应用私钥和 SHA256withRSA 生成 RSA2 签名;
  6. 对最终请求参数进行 URL 编码;
  7. 发送请求前,不再修改任何参与签名的参数。

待签名字符串中应包含 biz_content,例如:

app_id=2016102600766171&biz_content={"subject":"测试订单","out_trade_no":"202011060001","total_amount":"0.01","product_code":"QUICK_MSECURITY_PAY"}&charset=utf-8&format=JSON&method=alipay.trade.app.pay&notify_url=http://2324o79f50.iask.in/alipay/notify&sign_type=RSA2&timestamp=2020-11-06 17:55:55&version=1.0

这段内容是签名前的原文。最终请求中的 JSON、空格和转义方式,必须与生成签名时完全一致。

建议使用官方 SDK 生成订单字符串

使用 Java SDK 时,可以让 SDK 负责参数排序、签名和编码,减少手动处理带来的差异:

AlipayClient alipayClient = new DefaultAlipayClient(
    "沙箱网关地址",
    "2016102600766171",
    APP_PRIVATE_KEY,
    "JSON",
    "UTF-8",
    ALIPAY_PUBLIC_KEY,
    "RSA2"
);

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl("http://2324o79f50.iask.in/alipay/notify");

AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setSubject("测试订单");
model.setOutTradeNo("202011060001");
model.setTotalAmount("0.01");
model.setProductCode("QUICK_MSECURITY_PAY");

request.setBizModel(model);

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderString = response.getBody();

response.getBody() 返回客户端支付所需的完整订单参数字符串。不要再取出其中的字段重新排序、拼接,也不要再次对整个字符串编码。

如果当前 SDK 版本不支持这些模型类,也可以直接设置 JSON,但必须保证实际提交的 JSON 与签名时使用的内容一致:

request.setBizContent(
    "{\"subject\":\"测试订单\","
    + "\"out_trade_no\":\"202011060001\","
    + "\"total_amount\":\"0.01\","
    + "\"product_code\":\"QUICK_MSECURITY_PAY\"}"
);

检查环境和密钥配置

沙箱和正式环境的配置不能交叉使用,需要同时确认:

  • 请求发往沙箱网关,而不是正式网关;
  • app_id 属于当前沙箱应用;
  • 签名使用的是该沙箱应用对应的应用私钥;
  • 沙箱应用后台配置的应用公钥与该私钥配对;
  • SDK 中用于验证支付宝响应的是“支付宝公钥”,不是“应用公钥”。

两类公钥的用途不同:

  • 应用公钥:上传到支付宝,供支付宝验证商户的请求签名;
  • 支付宝公钥:配置在商户程序中,用于验证支付宝返回结果或异步通知。

如果后台重新上传过应用公钥,旧私钥生成的签名会立即失效。即使旧私钥与本地保存的旧应用公钥仍然匹配,也不能说明它与支付宝后台当前配置的公钥一致。

排查 URL 编码问题

RSA2 签名通常是 Base64 字符串,可能包含 +、/ 和 =。如果直接将它放进查询字符串却没有正确编码,+ 可能会被解析成空格。

正确的处理顺序是:

原始参数值 → 拼接待签名字符串 → RSA2 签名 → 对 sign 做 URL 编码

不要按下面的顺序处理:

URL 编码参数值 → 拼接 → 签名

也不要重复编码 sign。例如:

+  → %2B      正常的一次编码
%2B → %252B   重复编码

如果订单字符串已经由 SDK 的 sdkExecute() 生成,通常不应再对整个字符串调用 URLEncoder.encode()。

其他容易忽略的细节

  • sign_type=RSA2 必须使用 SHA256withRSA,不能实际使用 SHA1withRSA。
  • 所有签名输入都应使用 UTF-8。
  • 私钥格式必须符合所用 SDK 的要求,例如部分 Java 实现要求使用 PKCS#8 私钥。
  • 不要在私钥正文中加入引号、空格或错误的换行符。
  • timestamp、notify_url、charset 等参数一旦参与签名,发送时就必须保持原值。
  • biz_content 参与签名前不能先做 URL 编码。
  • 不要用格式化后的 JSON 生成签名,却发送压缩后的 JSON。即使两者语义相同,只要字符串不同,验签就会失败。

定位问题时,与其继续比较“签名结果”,不如同时打印下面三项:

1. 代码使用的完整待签名字符串
2. 最终发送的原始请求参数
3. 支付宝错误信息返回的验签字符串

然后逐字符比较三者。结合当前错误信息,首先要查清 biz_content 为什么没有出现在支付宝生成的验签字符串中,这很可能就是验签失败的直接原因。

备注:内容仅供参考。