PAYATHON 2026

支付宝电脑网站支付跳转后持续报错的原因分析

支付阿杰

结论

pageExecute 或统一下单调用返回 form,只能说明 SDK 已生成支付请求,不能说明支付宝已经通过验签。

问题通常出在以下环节:

  1. 只打开了 form 的 action 地址,没有提交表单中的隐藏参数。
  2. 沙箱网关、沙箱 app_id、应用私钥和支付宝公钥不属于同一套环境。
  3. 混淆了”应用公钥”和”支付宝公钥”,或者代码中的应用私钥与后台上传的应用公钥不匹配。
  4. 请求完成签名后,参数又被修改、转义或重新编码。
  5. biz_content 缺少 product_code,或者金额、订单号等业务参数格式有误。
  6. 开放平台使用的公钥模式或证书模式与 SDK 配置不一致。

目前没有支付宝错误页上的具体错误码、完整请求参数和脱敏后的 form,因此还不能确定具体原因。可以按下面的顺序排查。

确认是否真的提交了 form

支付宝电脑网站支付返回的通常不是一个可以单独打开的 URL,而是一段包含隐藏字段的 HTML 表单,例如:

<form id="alipaysubmit"
      name="alipaysubmit"
      action="https://openapi-sandbox.dl.alipaydev.com/gateway.do?charset=UTF-8"
      method="POST">
    <input type="hidden" name="app_id" value="...">
    <input type="hidden" name="method" value="alipay.trade.page.pay">
    <input type="hidden" name="sign_type" value="RSA2">
    <input type="hidden" name="sign" value="...">
    <input type="hidden" name="biz_content" value="...">
</form>
<script>
    document.forms['alipaysubmit'].submit();
</script>

如果只复制或打开 action,请求不会携带 app_id、method、biz_content、sign 等字段,支付宝只能返回通用错误页。

正确的处理方式是把 SDK 返回的整个 form 原样写入浏览器响应,再由浏览器提交:

String form = alipayClient.pageExecute(request).getBody();

response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(form);
response.getWriter().flush();

不要只提取 action,也不要把 form 当成普通 URL 重定向:

// 错误思路:action 本身不是完整支付请求
response.sendRedirect(actionUrl);

可以在浏览器开发者工具的 Network 面板中查看实际发往支付宝网关的请求。至少应该包含以下字段:

app_id
method
format
charset
sign_type
sign
timestamp
version
biz_content

检查沙箱环境是否完全一致

沙箱支付必须完整使用一套沙箱配置,不能只替换其中几项。

典型配置如下:

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

需要逐项确认:

  • 网关使用沙箱地址,而不是生产网关。
  • app_id 来自当前沙箱应用。
  • appPrivateKey 是该应用对应的应用私钥。
  • alipayPublicKey 是支付宝开放平台提供的”支付宝公钥”,不是应用公钥。
  • sign_type 与密钥类型一致,通常为 RSA2。
  • 下单和付款使用沙箱提供的买家账号。
  • 如果重新生成过密钥,代码、部署环境和开放平台配置都要同步更新。

即使所有配置都从同一个页面复制,也不能排除密钥问题。比如把后台显示的应用公钥填入 alipayPublicKey,表面上配置齐全,验签仍然会失败。

密钥关系如下:

商户请求签名:应用私钥
支付宝验证请求:开放平台中配置的应用公钥

支付宝回调签名:支付宝私钥
商户验证回调:支付宝公钥

区分公钥模式和证书模式

应用采用普通公钥模式时,需要配置应用私钥和支付宝公钥。

如果采用证书模式,则要使用相应的证书客户端配置,例如:

CertAlipayRequest certRequest = new CertAlipayRequest();
certRequest.setServerUrl("https://openapi-sandbox.dl.alipaydev.com/gateway.do");
certRequest.setAppId(sandboxAppId);
certRequest.setPrivateKey(appPrivateKey);
certRequest.setFormat("json");
certRequest.setCharset("UTF-8");
certRequest.setSignType("RSA2");
certRequest.setCertPath(appCertPath);
certRequest.setAlipayPublicCertPath(alipayPublicCertPath);
certRequest.setRootCertPath(alipayRootCertPath);

AlipayClient alipayClient = new DefaultAlipayClient(certRequest);

如果开放平台启用了证书模式,就不能继续按普通公钥模式传入 alipayPublicKey。证书文件也不能使用其他应用或生产环境中的文件。

核对业务参数

电脑网站支付通常使用以下 API:

alipay.trade.page.pay

biz_content 至少要包含有效的订单号、金额、标题和产品码:

AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://example.com/alipay/return");
request.setNotifyUrl("https://example.com/alipay/notify");

request.setBizContent("""
{
  "out_trade_no": "ORDER_20260909_001",
  "total_amount": "0.01",
  "subject": "测试订单",
  "product_code": "FAST_INSTANT_TRADE_PAY"
}
""");

检查以下内容:

  • product_code 应为 FAST_INSTANT_TRADE_PAY。
  • total_amount 应使用合法的十进制金额字符串,例如 "0.01"。
  • out_trade_no 在同一商户应用下应保持唯一。
  • subject 不能为空。
  • JSON 必须合法,不能有多余逗号、错误引号或未转义字符。
  • timestamp 格式通常为 yyyy-MM-dd HH:mm:ss,服务器时间不能明显漂移。
  • 请求字符集和签名字符集应统一使用 UTF-8。

return_url 和 notify_url 配置错误,通常不会导致支付页在打开前立即验签失败,但仍应填写完整 URL。浏览器应能跳转到 return_url,支付宝应能访问 notify_url。不要把 notify_url 设置为 localhost 或仅内网可访问的地址。

检查签名后是否修改过参数

签名根据请求参数计算。签名完成后,如果实际提交的内容发生变化,就可能与签名原文不一致。常见情况包括:

  • 修改 biz_content。
  • 再次格式化或转义 JSON。
  • 错误地把 + 转换为空格。
  • 重复对 sign 进行 URL 编码或解码。
  • 通过代理、模板引擎或前端框架重新拼接隐藏字段。
  • 使用其他字符集读取 UTF-8 内容。
  • 手工排序和拼接参数时,把空值或 sign 本身加入待签名字符串。

使用官方 SDK 时,最好让 SDK 负责参数排序、签名和表单生成。业务代码不要再次处理返回的 HTML:

AlipayTradePagePayResponse payResponse =
    alipayClient.pageExecute(request, "POST");

if (!payResponse.isSuccess()) {
    throw new IllegalStateException(
        payResponse.getCode() + ": " +
        payResponse.getMsg() + " / " +
        payResponse.getSubCode() + ": " +
        payResponse.getSubMsg()
    );
}

String form = payResponse.getBody();

部分 SDK 或接口调用方式即使能够取得 body,也要同时检查 isSuccess()、code、subCode 和 subMsg,不能只看 form 是否为空。

建议的排查顺序

  1. 在浏览器 Network 面板确认请求方式为 POST,并检查隐藏字段是否实际提交。
  2. 确认程序没有只打开 action 地址。
  3. 检查 action 是否指向沙箱网关。
  4. 检查请求中的 app_id 是否属于当前沙箱应用。
  5. 重新验证应用私钥和开放平台配置的应用公钥是否为同一密钥对。
  6. 确认代码中填写的是”支付宝公钥”,不是”应用公钥”。
  7. 确认公钥模式或证书模式与开放平台配置一致。
  8. 检查 method、sign_type、charset、timestamp 和 biz_content。
  9. 暂时绕过前端模板和中间页面,直接把 SDK 返回的完整 form 写入响应。
  10. 记录脱敏后的完整请求参数和支付宝返回的错误码,再根据 sub_code 定位问题。

如果直接输出 SDK 生成的完整 form 后可以进入收银台,问题就在原来的跳转或页面处理环节。如果仍然报错,应优先检查沙箱网关、密钥对应关系、签名模式和请求参数。支付成功后的异步通知验签属于后续环节,不要把它和进入收银台前的请求验签混在一起。

备注:内容仅供参考。