支付宝支付请求能否携带token并在回跳时返回
结论
支付宝支付请求可以携带用于业务关联的自定义信息,但不要把用户登录 token 直接传给支付宝,也不要依赖支付宝在回跳时将其返回。
建议采用以下方式:
- 创建支付订单时生成商户订单号
out_trade_no。 - 在服务端保存
out_trade_no与用户 ID、业务订单的绑定关系。 - 收到支付宝回调后,根据
out_trade_no查询用户和订单。 - 验证支付宝签名、商户身份、金额及支付状态,然后更新订单。
- 页面回跳只负责展示结果,最终支付状态应以异步通知或主动查询结果为准。
为什么不建议传递用户 token
登录 token 是身份凭证,可能被记录在浏览器地址栏、访问日志、代理日志、Referer、监控系统或第三方页面中。一旦泄露,攻击者可能用它冒充用户。
支付宝各类接口对扩展参数的支持范围也不完全相同。即使某个支付接口支持 passback_params,也不能认为任意自定义参数都会在页面跳转和同步回跳时原样返回。
以常见的支付宝电脑网站支付接口为例,passback_params 通常用于公用回传参数。它会通过哪些渠道返回、采用什么编码规则,应以当前接口的官方文档为准。不要把它当作传递登录会话的机制。
推荐实现流程
1. 创建本地支付订单
用户发起支付时,服务端从当前登录态中取得用户身份,并保存订单关联关系:
out_trade_no -> user_id -> business_order_id
例如:
202609091430001234 -> user_10086 -> order_98765
out_trade_no 应由服务端生成并保证唯一。不要直接信任客户端传入的用户 ID 或订单归属信息。
2. 发起支付宝支付
下面是 Java SDK 的常见写法。不同 SDK 版本使用的模型类和方法可能略有差异,请以项目的实际依赖为准。
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://example.com/pay/alipay/return");
request.setNotifyUrl("https://example.com/pay/alipay/notify");
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo("202609091430001234");
model.setTotalAmount("99.00");
model.setSubject("商品订单");
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
String paymentHtml = response.getBody();
传给支付宝的内容只需包含支付所需的业务信息。用户身份已经通过 out_trade_no 绑定在本地数据库中,无须传递登录 token。
3. 处理异步通知
支付宝会向 notify_url 发送支付结果。服务端收到通知后,必须先验证签名,再校验业务数据。
@PostMapping("/pay/alipay/notify")
@ResponseBody
public String handleNotify(HttpServletRequest request) throws Exception {
Map<String, String> params = extractAlipayParams(request);
boolean signatureValid = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
StandardCharsets.UTF_8.name(),
"RSA2"
);
if (!signatureValid) {
return "failure";
}
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
PaymentOrder order = paymentOrderService.findByOutTradeNo(outTradeNo);
if (order == null) {
return "failure";
}
// 还应校验 app_id、seller_id、金额和订单当前状态
if (!order.getAmount().toPlainString().equals(totalAmount)) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
paymentOrderService.markPaidIdempotently(
outTradeNo,
tradeNo
);
}
return "success";
}
extractAlipayParams 应完整读取支付宝提交的参数。验签前不要擅自修改参数值或字符编码,也不要删除参与验签的字段。
支付宝可能重复发送异步通知,因此订单更新必须具备幂等性。
4. 处理同步回跳
return_url 是浏览器跳转地址,可用于显示“支付处理中”或展示查询后的支付结果,但不能仅凭回跳参数认定支付成功。
@GetMapping("/pay/alipay/return")
public String handleReturn(
@RequestParam("out_trade_no") String outTradeNo,
HttpServletRequest request,
Model model) throws Exception {
Map<String, String> params = extractAlipayParams(request);
boolean signatureValid = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
StandardCharsets.UTF_8.name(),
"RSA2"
);
if (!signatureValid) {
return "pay/invalid";
}
PaymentOrder order = paymentOrderService.findByOutTradeNo(outTradeNo);
if (order == null) {
return "pay/not-found";
}
// 展示服务端保存的订单状态;必要时主动调用支付宝交易查询接口
model.addAttribute("order", order);
return "pay/result";
}
用户可能关闭页面,也可能因网络中断而无法完成回跳,所以同步回跳不能代替异步通知。
如果确实需要传递自定义参数
如果当前支付接口明确支持 passback_params,可以用它传递随机关联值,例如一次性 state。不要传递登录 token、手机号、用户 ID 或其他敏感信息。
String state = UUID.randomUUID().toString();
// 服务端保存 state 与订单的关联,并设置有效期和一次性使用限制
paymentStateService.save(state, outTradeNo);
model.setPassbackParams(
URLEncoder.encode(state, StandardCharsets.UTF_8)
);
收到回传值后,只能把它作为查找关联订单的线索,不能将其视为支付成功证明或用户认证凭证。服务端仍须完成支付宝验签和订单校验。
使用时还要注意:
passback_params会出现在同步回跳、异步通知还是两者之中,取决于具体支付接口的定义。- 参数通常有长度和编码限制。
- 如果接口要求 URL 编码,需要避免重复编码或提前解码。
- 不要依赖接口文档未声明的自定义字段,支付宝可能直接忽略这些字段。
用户身份应该如何识别
支付前,系统通过自己的登录态识别用户。支付过程中,通过 out_trade_no 关联用户。支付完成后,再从服务端保存的订单记录中恢复业务上下文。
如果原登录会话仍然有效,可以继续使用系统 Cookie 或现有认证机制。如果登录会话已经失效,也可以先展示不含敏感信息的支付结果,再要求用户重新登录查看订单。
需要明确区分三者的用途:token 用于用户认证,商户订单号用于支付关联,支付宝签名和订单校验用于确认支付结果。
备注:内容仅供参考。
