支付宝回调异步通知的相关内容以及故障排查
支付宝回调异步通知
当面付异步通知
当商户调用预下单请求API生成二维码展示给用户后,用户通过手机扫描二维码进行支付,支付宝会将该笔订单的变更信息,沿着商户调用预下单请求时所传入的通知地址主动推送给商户。
通知触发条件:交易状态为TRADE_SUCCESS
服务器异步通知页面特性
- 必须保证服务器异步通知页面(notify_url)上无任何字符,如空格、HTML标签、开发系统自带抛出的异常提示信息等;
- 支付宝是用POST方式发送通知信息,因此该页面中获取参数的方式,如:request.Form(“out_trade_no”)、$_POST[‘out_trade_no’];
- 支付宝主动发起通知,该方式才会被启用;
- 只有在支付宝的交易管理中存在该笔交易,且发生了交易状态的改变,支付宝才会通过该方式发起服务器通知(即时到账中交易状态为“等待买家付款”的状态默认是不会发送通知的);
- 服务器间的交互,不像页面跳转同步通知可以在页面上显示出来,这种交互方式是不可见的;
- 第一次交易状态改变(即时到账中此时交易状态是交易完成)时,不仅会返回同步处理结果,而且服务器异步通知页面也会收到支付宝发来的处理结果通知;
- 程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是success这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);
- 程序执行完成后,该页面不能执行页面跳转。(不能有重定向)如果执行页面跳转,支付宝会收不到success字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知;
- cookies、session等在此页面会失效,即无法获取这些数据;
- 该方式的调试与运行必须在服务器上,即互联网上能访问;(如果本地调试,需要使用内网穿透)
- 该方式的作用主要防止订单丢失,即页面跳转同步通知没有处理订单更新,它则去处理;
- 当商户收到服务器异步通知并打印出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 ¬ify_id=42af7baacd1d3746cf7b56752b91edcj34 &[email protected] ¬ify_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 ¬ify_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<>();
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());
params.remove("sign_type");
|
第二步: 将剩下参数进行url_decode, 然后进行字典排序,组成字符串,得到待签名字符串
第三步: 将签名参数(sign)使用base64解码为字节码串。
第二步和第三步在支付宝提供的rsaCheckV2
方法中已经实现
第四步: 使用RSA的验签方法,通过签名字符串、签名参数(经过base64解码)及支付宝公钥验证签名。
1 2 3 4 5 6 7 8 9 10
| try { 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
| 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); } 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时,支付宝才会认定为买家付款成功。
注意:
1 2 3 4 5 6 7
| ServerResponse serverResponse = iOrderService.alipayCallback(params);
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(); }
|
回调出错的排查
如果回调出现问题,支付宝提供了一些自查方案可以先自行排查
收不到异步通知自查方案-支付宝接口常见错误系列
收不到异步通知「自检方案」
- 需http://或者https://格式的完整路径
例:https://您的域名/notify_url.php ,支持ip地址方式。(推荐使用域名)
- 不能加?id=123这类自定义参数
错误示例:https://您的域名/notify_url.php?id=123&test=abc
- 必须外网可以正常访问,这个不难理解,在您的异步地址没有代码逻辑的情况下,直接访问应该是一个空白 页面并且http状态是200(不支持http200以外的状态)
- 不能有重定向 如:http302
- 使用POST方式接收,请确保服务器路由已经开放POST通知