Java对接支付宝支付功能(三)异步通知

支付宝回调异步通知的相关内容以及故障排查

支付宝回调异步通知

当面付异步通知

当商户调用预下单请求API生成二维码展示给用户后,用户通过手机扫描二维码进行支付,支付宝会将该笔订单的变更信息,沿着商户调用预下单请求时所传入的通知地址主动推送给商户。

通知触发条件:交易状态为TRADE_SUCCESS

服务器异步通知页面特性

  1. 必须保证服务器异步通知页面(notify_url)上无任何字符,如空格、HTML标签、开发系统自带抛出的异常提示信息等;
  2. 支付宝是用POST方式发送通知信息,因此该页面中获取参数的方式,如:request.Form(“out_trade_no”)、$_POST[‘out_trade_no’];
  3. 支付宝主动发起通知,该方式才会被启用;
  4. 只有在支付宝的交易管理中存在该笔交易,且发生了交易状态的改变,支付宝才会通过该方式发起服务器通知(即时到账中交易状态为“等待买家付款”的状态默认是不会发送通知的);
  5. 服务器间的交互,不像页面跳转同步通知可以在页面上显示出来,这种交互方式是不可见的;
  6. 第一次交易状态改变(即时到账中此时交易状态是交易完成)时,不仅会返回同步处理结果,而且服务器异步通知页面也会收到支付宝发来的处理结果通知;
  7. 程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是success这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
  8. 程序执行完成后,该页面不能执行页面跳转。(不能有重定向)如果执行页面跳转,支付宝会收不到success字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知;
  9. cookies、session等在此页面会失效,即无法获取这些数据;
  10. 该方式的调试与运行必须在服务器上,即互联网上能访问;(如果本地调试,需要使用内网穿透)
  11. 该方式的作用主要防止订单丢失,即页面跳转同步通知没有处理订单更新,它则去处理;
  12. 当商户收到服务器异步通知并打印出success时,服务器异步通知参数notify_id才会失效。也就是说在支付宝发送同一条异步通知时(包含商户并未成功打印出success导致支付宝重发数次通知),服务器异步通知参数notify_id是不变的。

异步返回结果的验签

实例:某商户设置的通知地址为https://api.xx.com/receive_notify.htm,对应接收到通知的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
https://api.xx.com/receive_notify.htm?
gmt_payment=2015-06-11 22:33:59
&notify_id=42af7baacd1d3746cf7b56752b91edcj34
&[email protected]
&notify_type=trade_status_sync
&sign=kPbQIjX+xQc8F0/A6/AocELIjhhZnGbcBN6G4MM/HmfWL4ZiHM6fWl5NQhzXJusaklZ1LFuMo+lHQUELAYeugH8LYFvxnNajOvZhuxNFbN2LhF0l/KL8ANtj8oyPM4NN7Qft2kWJTDJUpQOzCzNnV9hDxh5AaT9FPqRS6ZKxnzM=&trade_no=2015061121001004400068549373
&out_trade_no=21repl2ac2eOutTradeNo322
&gmt_create=2015-06-11 22:33:46
&seller_id=2088211521646673
&notify_time=2015-06-11 22:34:03
&subject=FACE_TO_FACE_PAYMENT_PRECREATE中文
&trade_status=TRADE_SUCCESS
&sign_type=RSA2

第一步: 在通知返回参数列表中,除去sign、sign_type两个参数外,凡是通知返回回来的参数皆是待验签的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
logger.debug("**********************支付宝回调开始**********************");		
// 得到并遍历回调传来的参数
Map<String, String[]> requestParams = request.getParameterMap();
Map<String, String> params = new HashMap<>();
// 将前台的参数转换为"xxx"->"aaa,bbb"的格式存入params中,实际上回调传来的参数每个key都只对应一个value
for (Map.Entry<String, String[]> entry: requestParams.entrySet()) {
String key = entry.getKey();
String values[] = entry.getValue();
StringBuilder valStr = new StringBuilder();
for (int i = 0; i < values.length; i ++) {
if ( i != values.length - 1) {
valStr.append(values[i]).append(",");
} else {
valStr.append(values[i]);
}
}
params.put(key, valStr.toString());
}
// 日志打印回调信息,包括签名,支付状态,所有参数
logger.info("alipay_callback, sign:{}, trade_status:{}, params:{}", params.get("sign"), params.get("trade_status"),params.toString());

// 需要除去sign、sign_type两个参数,而sign已经在#rsaCheckV2方法中除去了
params.remove("sign_type");

第二步: 将剩下参数进行url_decode, 然后进行字典排序,组成字符串,得到待签名字符串

第三步: 将签名参数(sign)使用base64解码为字节码串。

​ 第二步和第三步在支付宝提供的rsaCheckV2方法中已经实现

第四步: 使用RSA的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名。

1
2
3
4
5
6
7
8
9
10
try {
// 使用RSA的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名
boolean rsaCheckV2 = AlipaySignature.rsaCheckV2(params, Configs.getAlipayPublicKey(), "utf-8", Configs.getSignType());
// 验签失败
if ( !rsaCheckV2 ) {
return ServerResponse.createByErrorMessage("验签失败,检测到非法请求");
}
} catch (AlipayApiException e) {
logger.error("支付宝回调异常");
}

需要注意的是rsaCheckV2有两个重载的方法,一个是带signType参数的,我们需要使用这个方法指定签名类型为RSA2(可以从之前装配的Configs类直接获取),而不带signType的方法默认使用SHA1WithRSA的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 支付宝提供的验签方法,内部已经实现了除去sign参数、字典排序成待签名字符串、将sign使用base64解码为字节码串
public static boolean rsaCheckV2(Map<String, String> params, String publicKey,
String charset,String signType) throws AlipayApiException {
String sign = params.get("sign");
String content = getSignCheckContentV2(params);

return rsaCheck(content, sign, publicKey, charset,signType);
}

public static String getSignCheckContentV2(Map<String, String> params) {
if (params == null) {
return null;
}

params.remove("sign");

StringBuffer content = new StringBuffer();
List<String> keys = new ArrayList<String>(params.keySet());
Collections.sort(keys);

for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
content.append((i == 0 ? "" : "&") + key + "=" + value);
}

return content.toString();
}

public static boolean rsaCheck(String content, String sign, String publicKey, String charset,
String signType) throws AlipayApiException {
if (AlipayConstants.SIGN_TYPE_RSA.equals(signType)) {
return rsaCheckContent(content, sign, publicKey, charset);
// 这里使用的是RSA2的签名方法
} else if (AlipayConstants.SIGN_TYPE_RSA2.equals(signType)) {
return rsa256CheckContent(content, sign, publicKey, charset);
} else {
throw new AlipayApiException("Sign Type is Not Support : signType=" + signType);
}
}

第五步:需要严格按照如下描述校验通知数据的正确性。

商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,并判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),同时需要校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email),上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。

注意:

  • 状态TRADE_SUCCESS的通知触发条件是商户签约的产品支持退款功能的前提下,买家付款成功;

  • 交易状态TRADE_FINISHED的通知触发条件是商户签约的产品不支持退款功能的前提下,买家付款成功;或者,商户签约的产品支持退款功能的前提下,交易已经成功并且已经超过可退款期限。

1
2
3
4
5
6
7
// 商户需要校验通知数据的正确性
ServerResponse serverResponse = iOrderService.alipayCallback(params);
// 校验成功,一定要打印出“success”
if (serverResponse.isSuccuess()) {
return Const.AlipayCallback.RESPONSE_SUCCESS;
}
return Const.AlipayCallback.RESPONSE_FAILED;

商户需要依次校验通知数据的正确性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public ServerResponse alipayCallback(Map<String, String> params) {
// 支付宝外部订单号,也就是商城订单号
Long orderNo = Long.valueOf(params.get("out_trade_no"));
logger.debug("out_trade_no: {}", orderNo.toString());
// 支付宝交易号
String tradeNo = params.get("trade_no");
// 交易状态
String tradeStatus = params.get("trade_status");
logger.debug("trade_status: {}", tradeStatus);
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
return ServerResponse.createByErrorMessage("不是该商城订单,忽略回调");
}
// 判断交易状态
// 交易状态为已付款,已发货,订单完成,订单关闭时,为重复调用
if (order.getStatus() >= Const.OrderStatusEnum.PAID.getCode()) {
return ServerResponse.createBySuccess("支付宝重复调用");
}
// 交易成功,记录付款时间,更新订单状态为已付款
if (Const.AlipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)) {
order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
order.setStatus(Const.OrderStatusEnum.PAID.getCode());
orderMapper.updateByPrimaryKeySelective(order);
}
// 组装支付信息对象,略
...
return ServerResponse.createBySuccess();
}

回调出错的排查

如果回调出现问题,支付宝提供了一些自查方案可以先自行排查

收不到异步通知自查方案-支付宝接口常见错误系列

收不到异步通知「自检方案」

  1. 需http://或者https://格式的完整路径
    例:https://您的域名/notify_url.php ,支持ip地址方式。(推荐使用域名
  2. 不能加?id=123这类自定义参数
    错误示例:https://您的域名/notify_url.php?id=123&test=abc
  3. 必须外网可以正常访问,这个不难理解,在您的异步地址没有代码逻辑的情况下,直接访问应该是一个空白 页面并且http状态是200(不支持http200以外的状态)
  4. 不能有重定向 如:http302
  5. 使用POST方式接收,请确保服务器路由已经开放POST通知