alipay.trade.app.pay 返回 302 且无跳转地址
结论
alipay.trade.app.pay 的正确调用流程是:服务端生成已签名的 orderString,返回给 App,再由 App 调用支付宝客户端 SDK 发起支付。服务端不应在请求网关后等待网页跳转。
因此,HTTP 302 不是该接口正常的业务结果,也不能用来判断签名是否正确。两段签名一致,只能说明两种签名实现在处理同一份原文时得到了相同结果,无法证明请求地址、传输方式、参数编码和应用配置都没有问题。
建议先检查以下两点:
- 网关必须使用 HTTPS:
https://openapi.alipay.com/gateway.do
如果使用了 http://openapi.alipay.com/gateway.do、错误的域名,或请求经过反向代理、WAF、统一登录网关,可能会先收到 302。
- 不要自行请求网关并查找
Location。App 支付应通过 Java SDK 的sdkExecute()生成订单字符串,然后将response.getBody()原样交给支付宝客户端 SDK。
推荐的服务端实现
使用支付宝 Java SDK 生成支付参数,可以减少排序、签名和 URL 编码顺序出错的可能:
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.response.AlipayTradeAppPayResponse;
public class AlipayOrderService {
private static final String GATEWAY =
"https://openapi.alipay.com/gateway.do";
public String createOrderString() throws AlipayApiException {
AlipayClient alipayClient = new DefaultAlipayClient(
GATEWAY,
"你的应用APP_ID",
"你的应用私钥",
"json",
"UTF-8",
"支付宝公钥",
"RSA2"
);
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("唯一商户订单号");
model.setTotalAmount("0.01");
model.setSubject("支付测试");
model.setProductCode("QUICK_MSECURITY_PAY");
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizModel(model);
request.setNotifyUrl("https://你的域名/alipay/notify");
AlipayTradeAppPayResponse response =
alipayClient.sdkExecute(request);
String orderString = response.getBody();
if (orderString == null || orderString.isEmpty()) {
throw new IllegalStateException(
"未生成支付宝 App 支付订单字符串,subCode="
+ response.getSubCode()
+ ", subMsg="
+ response.getSubMsg()
);
}
return orderString;
}
}
服务端将 orderString 作为普通字符串返回给 App。App 收到后,直接把它传给支付宝客户端 SDK,不要解析后重新排序,也不要再次进行 URL 编码。
手动签名时的正确顺序
如果确实需要手动生成订单字符串,必须分清“待签名原文”和“最终传输字符串”。
1. 构造待签名参数
排除 sign 参数和空值参数,再按参数名升序排列。此时不能预先对参数值进行 URL 编码:
app_id=...&biz_content={"out_trade_no":"..."}&charset=utf-8&format=JSON&method=alipay.trade.app.pay&sign_type=RSA2×tamp=...&version=1.0
2. 计算签名
使用以下算法:
SHA256WithRSA
先对 UTF-8 编码后的待签名原文进行签名,再对签名结果进行 Base64 编码。
3. 拼装订单字符串
完成签名后,再分别对每个参数值进行 UTF-8 URL 编码,并加入 sign:
app_id=...&biz_content=...&charset=utf-8&...&sign=...
不要用已经编码过的 biz_content 参与签名,也不要重复编码完整的订单字符串,否则容易导致 % 再次变成 %25,或空格在 %20 和 + 之间发生变化。
示例代码:
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8);
实际拼接时,应分别编码每个参数值,而不是一次性编码整条查询字符串。
为什么签名一致仍然可能失败
两段签名完全一致,说明手动代码和 SDK 使用了相同的私钥、算法及签名原文,但以下项目仍需逐一检查:
- 是否使用正式环境的 HTTPS 网关。
app_id、应用私钥和支付宝公钥是否属于同一个应用及同一环境。- 应用是否已经签约并开通 App 支付能力。
timestamp是否为当前时间,服务器时钟是否准确。out_trade_no是否符合要求并保持唯一。total_amount建议以字符串"0.01"传入,避免 JSON 数字格式发生变化。product_code是否为QUICK_MSECURITY_PAY。- 请求编码和签名编码是否统一使用 UTF-8。
- 签名完成后是否修改过
biz_content。 - 请求是否经过代理、网关或负载均衡,并被这些中间环节改写。
- 是否混用了证书模式和公钥模式。
问题中的时间戳是 2022-10-20 17:45:14。如果这不是历史日志,而是实际重放的请求,就必须换成当前时间并重新生成签名。
如何定位 302 的来源
先关闭 HTTP 客户端的自动跳转,查看最初返回的状态行和响应头:
curl -v --max-redirs 0 \
-X POST \
'https://openapi.alipay.com/gateway.do' \
-H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' \
--data-raw '完整请求参数'
重点检查:
HTTP/1.1 302
Location:
Server:
Via:
Set-Cookie:
如果 Location 指向 HTTPS、登录页、代理地址或公司内部域名,这个 302 很可能来自 HTTP 跳转或中间代理,而不是支付宝业务接口。
如果程序中看不到 Location,可能是 HTTP 库自动跟随了跳转,或日志只记录了响应体,没有记录响应头。可以暂时关闭自动跳转,并输出以下信息:
- 最终请求 URL
- HTTP 状态码
- 全部响应头
- 响应体
- 是否经过代理
- 重定向历史
记录日志时,需要对 sign、应用私钥、用户信息及其他敏感字段进行脱敏。
关于“跳转地址”
正常情况下,alipay.trade.app.pay 不会返回一个供服务端访问的 Location 地址。它返回的是客户端支付所需的订单字符串,之后的跳转和收银台拉起由支付宝客户端 SDK 处理。
如果需要在浏览器中打开支付宝收银台,应先确认业务场景是否适合使用手机网站支付或电脑网站支付接口,而不是 App 支付接口。不同接口的调用流程和返回形式并不相同。
仅凭一个缺少响应头和响应体的 302,无法判断它的具体来源。但有一点可以确定:不要再从这个状态码中寻找 App 支付订单结果。应改用官方 Java SDK 的 sdkExecute() 生成 orderString,同时检查 HTTPS 网关和中间代理,以便定位问题。
备注:内容仅供参考。
