PAYATHON 2026

pageExecute()返回的URL打开后提示invalid-signature

支付小周

结论

invalid-signature 表示支付宝无法使用当前应用配置的“应用公钥”验证请求签名。常见原因主要有两种:

  1. 服务端签名所用的应用私钥,与支付宝开放平台中该 app_id 配置的应用公钥不属于同一密钥对。
  2. pageExecute() 生成 URL 后,前端、网关或模板引擎又对 URL 或参数进行了编码、解码或重新拼接,导致验签内容发生变化。

先确认密钥、app_id 和运行环境完全对应,再让浏览器直接跳转到 SDK 生成的原始 URL。不要对整个 URL 再执行 URLEncoder.encode(),也不要拆分、排序或重新拼装其中的参数。

一、检查密钥配置是否匹配

支付宝验证请求签名时,使用的是开放平台中配置的“应用公钥”;服务端生成签名时,使用的是对应的“应用私钥”。两者必须属于同一密钥对。

逐项检查以下配置:

  • 当前请求中的 app_id 是否确实为 2021002190618823。
  • 服务端加载的私钥是否属于该应用,而非其他应用或沙箱环境。
  • 支付宝开放平台中配置的是否为“应用公钥”,不要误填“支付宝公钥”。
  • SDK 配置中的 alipayPublicKey 是支付宝公钥,供商户验证支付宝响应,不能代替应用公钥。
  • sign_type 是否与实际签名算法一致。请求使用 RSA2 时,应采用 SHA256withRSA。
  • 修改应用密钥后,部署环境是否仍在读取旧配置。
  • 多实例部署时,各实例加载的密钥是否一致。

可以从私钥导出公钥,再与开放平台中的应用公钥进行比较。例如使用 OpenSSL:

openssl rsa -in app_private_key.pem -pubout -out derived_public_key.pem

比较时忽略 PEM 头尾、空格和换行,只检查 Base64 主体。如果两者不同,需要把正确的应用公钥重新配置到对应应用,或者改用与平台现有应用公钥匹配的私钥。

不要把应用私钥粘贴到日志、工单或前端页面中。

二、不要修改 pageExecute() 生成的 URL

SDK 已经处理了参数排序、签名和 URL 编码。生成 URL 后再做处理,很容易破坏签名。

错误示例:

String payUrl = alipayClient.pageExecute(request, "GET");
String encodedUrl = URLEncoder.encode(payUrl, StandardCharsets.UTF_8);
return encodedUrl;

也不要拆解 URL 中的参数再重新拼接:

// 不建议:重新解析、排序、编码或拼装 pageExecute() 的结果
Map<String, String> params = parseQuery(payUrl);
String rebuiltUrl = rebuildUrl(params);

建议直接重定向:

AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setNotifyUrl("https://landscaperoad.cn/hwgateway/api-honghu-pay/pay/alipay_notify");
request.setBizContent(bizContent);

String payUrl = alipayClient.pageExecute(request, "GET");
response.sendRedirect(payUrl);

如果必须把 URL 返回给前端,可以将其放入 JSON 字段,由前端直接跳转:

Map<String, String> result = new HashMap<>();
result.put("payUrl", alipayClient.pageExecute(request, "GET"));
return result;

前端直接使用:

window.location.href = response.payUrl;

不要再次调用:

window.location.href = encodeURIComponent(response.payUrl);

三、检查 &amp; 和 &quot; 是否进入了实际请求

问题中展示的验签字符串包含:

&amp;
&quot;

如果这只是错误页面显示时产生的 HTML 转义,通常不会影响请求。页面上显示的是 &amp;,底层原始内容仍可能是 &。

如果实际请求 URL 中确实存在字面量 &amp;,参数就会被破坏。例如:

app_id=2021002190618823&amp;biz_content=...

正确的查询参数分隔符应为:

app_id=2021002190618823&biz_content=...

同样,biz_content 应传入正常的 JSON,再由 SDK 进行 URL 编码。不要提前将其替换成 HTML 实体:

{"body":"1232022051116365125","out_trade_no":"4431652262692070"}

调用 SDK 前不应变成:

{&quot;body&quot;:&quot;1232022051116365125&quot;}

可以在浏览器开发者工具的 Network 面板中查看最终请求地址,确认实际发送的是 %22、%26 等 URL 编码,还是错误的 %26amp%3B、&amp; 等 HTML 实体。

四、检查 + 是否被转换为空格

RSA2 签名经过 Base64 编码后,可能包含 +、/、=。如果中间系统先解码查询参数,再用错误的方式重新编码,+ 可能被当成空格,最终导致签名失效。

需要排查:

  • 前端是否调用 decodeURIComponent() 后重新拼接 URL。
  • API 网关是否修改了查询参数。
  • 重定向接口是否先解析 sign,再生成新的 URL。
  • 模板引擎是否对 URL 进行了 HTML 转义。
  • 日志中显示的空格是否原本为 +。

建议记录并对比以下内容:

  • pageExecute() 刚返回时的 URL。
  • 后端实际返回给前端的 URL。
  • 浏览器最终发往支付宝的 URL。

记录时可以对 sign 脱敏,但不要改动其他参数,这样才能判断请求是在哪一层被修改的。

五、核对 SDK 初始化参数

普通公钥模式下,初始化方式可以参考:

AlipayClient alipayClient = new DefaultAlipayClient(
    "https://openapi.alipay.com/gateway.do",
    appId,
    appPrivateKey,
    "json",
    "UTF-8",
    alipayPublicKey,
    "RSA2"
);

检查以下内容:

  • 正式应用使用正式网关,沙箱应用使用沙箱网关。
  • appId、应用私钥和应用公钥属于同一环境。
  • appPrivateKey 是应用私钥。
  • alipayPublicKey 是支付宝公钥。
  • 字符集统一为 UTF-8。
  • 签名算法统一为 RSA2。

如果应用使用证书模式,应采用对应的证书客户端配置,不能混用普通公钥模式的初始化方式。具体构造方式取决于当前支付宝 Java SDK 版本和应用采用的密钥模式。

六、使用 SDK 构造业务参数

尽量让 SDK 处理签名和编码,不要手动生成签名字符串:

AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();

request.setNotifyUrl(
    "https://landscaperoad.cn/hwgateway/api-honghu-pay/pay/alipay_notify"
);

request.setBizContent("""
{
  "out_trade_no": "4431652262692070",
  "total_amount": "10.01",
  "subject": "【明星徐百慧推荐】摩洛哥精油免蒸染烫修复干枯补水顺滑深层护理滋养官方正品发膜",
  "body": "1232022051116365125",
  "product_code": "QUICK_MSECURITY_PAY",
  "timeout_express": "300m",
  "passback_params": "443_1232022051116365125_123_goods_alipay_app"
}
""");

String payUrl = alipayClient.pageExecute(request, "GET");

如果当前 Java 版本不支持文本块,可以使用 JSON 序列化库生成 biz_content。不要自行拼接包含中文、引号和特殊字符的 JSON。

推荐排查顺序

  1. 确认 app_id、应用私钥和开放平台应用公钥属于同一应用、同一环境。
  2. 确认没有混用普通公钥模式和证书模式。
  3. 在 pageExecute() 返回后,立即保存一份脱敏 URL。
  4. 将其与浏览器最终请求的 URL 对比,检查中间是否发生了重新编码或拼接。
  5. 检查实际 URL 中是否出现 &amp;、&quot;,以及 sign 中原本的 + 是否变成了空格。
  6. 暂时绕过前端传递,由后端直接调用 sendRedirect()。如果此时可以正常打开,问题基本出在前端或中间网关处理 URL 的过程中。
  7. 如果直接访问原始 URL 仍然报错,优先重新核对密钥对和 SDK 初始化配置。

仅凭给出的验签字符串,还不能判断问题究竟是密钥不匹配,还是 URL 被修改。这两项最常见,也最容易验证。通过“后端直接重定向测试”和“从私钥导出公钥进行对比”,通常可以很快缩小排查范围。

备注:内容仅供参考。