个人沙箱调用 alipay.trade.app.pay 提示 invalid-signature
结论
私钥和公钥能够匹配,只能说明密钥对本身没有问题。出现 invalid-signature,通常是因为支付宝实际收到并用于验签的字符串,与代码签名时使用的字符串不完全一致。
错误信息中的验签字符串没有 biz_content,但 alipay.trade.app.pay 必须提交业务参数。建议先检查以下几项:
biz_content是否确实发送到了支付宝;- 生成签名后,其他参数是否又被修改;
- 是否混用了沙箱和正式环境的
app_id、网关或应用公钥; sign是否经过了错误的 URL 编码、Base64 处理或字符集转换;- 是否误把 HTML 中显示的
&当作实际的参数分隔符。
先核对支付宝收到的验签字符串
报错中的字符串,是支付宝网关根据实际收到的请求参数重新拼接出的待验签内容:
app_id=2016102600766171&charset=utf-8&format=JSON&method=alipay.trade.app.pay¬ify_url=http://2324o79f50.iask.in/alipay/notify&sign_type=RSA2×tamp=2020-11-06 17:55:55&version=1.0
页面显示的 & 只是 HTML 对 & 的转义,不能直接参与签名。
把这段字符串与代码打印出的“签名前原文”逐字符比较。下面这些差异都会造成验签失败:
- 缺少参数或多出参数;
- 参数排序不同;
- 大小写不同;
- 存在多余的空格、换行或不可见字符;
- 参数值在签名前已经过 URL 编码;
- 生成签名后又修改了参数;
- JSON 的字段顺序、空格或转义方式发生变化。
当前错误中最值得注意的是,验签字符串里没有 biz_content。如果代码签名时包含了 biz_content,发送请求时却没有带上,支付宝用于验签的原文就会与签名原文不同。
正确的参数处理顺序
手动签名时,参数应按以下顺序处理:
- 准备全部公共参数和业务参数;
- 排除
sign以及值为空的参数; - 按参数名的 ASCII 顺序排序;
- 使用参数原始值拼接待签名字符串;
- 使用应用私钥和
SHA256withRSA生成 RSA2 签名; - 对最终请求参数进行 URL 编码;
- 发送请求前,不再修改任何参与签名的参数。
待签名字符串中应包含 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¬ify_url=http://2324o79f50.iask.in/alipay/notify&sign_type=RSA2×tamp=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 为什么没有出现在支付宝生成的验签字符串中,这很可能就是验签失败的直接原因。
备注:内容仅供参考。
