公司项目中需要用到支付宝开放平台实现支付功能,因为前端有多个平台,所以将关于支付的所有操作都集中在服务器端实现,给前端的由服务器端接好支付宝接口的黑盒暴露的接口。便于整理记忆,特摘此博客记录在其中踩过的坑。
使用的应用为手机网站支付2.0
本人电脑为WIN10版本,因此使用WINDOWS版工具(2017/03/29),使用工具生成PKCS8密钥后,需要上传应用公钥到开放平台。开放平台->
坑1:由于工具上面的工具栏中的签名功能里生成的sign必须含有sing_type参数,与开放平台上面验证上传应用公钥正确性上的参数需求只有a=123不符合,因此该工具生成的签名无法在开放平台上上传应用公钥上的验证密钥正确性上成功通过。但是生成的是公钥与私钥是正确的。
上传应用公钥成功后会生成支付宝公钥,该公钥与应用公钥需要区分,并且自己保存好在服务器端。
tip1:开放平台上面写的RSA1与RSA2密钥与生成的密钥没有关系,只是不同的加签算法而已,在你需要验签以及自己对参数加签的时候使用不同的算法而已。官方推荐使用SHA256那么就将工具生成的应用公钥上传到RSA2(SHA256)密钥处即可。
本人使用 alipay-sdk-java20170307171631.jar 版本,从SDK中的pay.jsp中可以分析出,需要自己定义好AlipayClient所需的各种参数。可以参照SDK中的AlipayConfig:
public class AlipayConfig {
// 商户appid
public static String APPID = "";
// 私钥 pkcs8格式的
public static String RSA_PRIVATE_KEY = "";
// 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
public static String notify_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/notify_url.jsp";
// 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址
public static String return_url = "http://商户网关地址/alipay.trade.wap.pay-JAVA-UTF-8/return_url.jsp";
// 请求网关地址
public static String URL = "https://openapi.alipay.com/gateway.do";
// 编码
public static String CHARSET = "UTF-8";
// 返回格式
public static String FORMAT = "json";
// 支付宝公钥
public static String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjrEVFMOSiNJXaRNKicQuQdsREraftDA9Tua3WNZwcpeXeh8Wrt+V9JilLqSa7N7sVqwpvv8zWChgXhX/A96hEg97Oxe6GKUmzaZRNh0cZZ88vpkn5tlgL4mH/dhSr3Ip00kvM4rHq9PwuT4k7z1DpZAf1eghK8Q5BgxL88d0X07m9X96Ijd0yMkXArzD7jg+noqfbztEKoH3kPMRJC2w4ByVdweWUT2PwrlATpZZtYLmtDvUKG/sOkNAIKEMg3Rut1oKWpjyYanzDgS7Cg3awr1KPTl9rHCazk15aNYowmYtVabKwbGVToCAGK+qQ1gT3ELhkGnf3+h53fukNqRH+wIDAQAB";
// 日志记录目录
public static String log_path = "/log";
// RSA2
public static String SIGNTYPE = "RSA2";
}
具体代码就不贴出来,只记录一下支付宝的支付流程。
pay.jsp中当调用AlipayClient的pageExecute()方法的时候,会返回一个含有HTML标签的表单内容,该表单内容的作用是跳转到支付宝的支付页面,与服务器端的页面无关联,该表单可以自定义的放置在页面的任意位置,然而实际上跳转1s左右就会发生。
支付宝文档有区分前后回跳与异步通知,这点比微信支付只有异步通知上多了一个前后回跳。
前后回跳也就是支付成功后会自动跳转到已定义好的return_url地址,服务器端能收到支付宝传过来的支付结果里面的参数。服务器端需要对返回进行自己的后台操作,当支付成功了则更新流水,失败则提示用户失败信息。
异步通知在实际测试中,比前后回跳要更快发出,也就是说服务器端更先收到前后回跳,理论上是服务器的异步通知处理逻辑要比前后回跳更快。为了金钱的安全性,优先级中异步>前后。
前后回跳与异步通知需要注意幂等性。
前后回跳不需要验签,异步通知需要验签。
以上则是整个支付流程。
支付接口调用,参数使用SDK上的AlipayTradeWapPayModel,调起支付代码(不贴出具体的参数):
Long userId;
Long auctionId;
AlipayTradeWapPayModel payParam;
@Test
public void payInsurance() throws AlipayApiException {
payParam.setDisablePayChannels("credit_group"); // 禁用信用卡渠道
TransmissionResult getOrderInsuranceResult = userBidService.getOrderInsurance(userId, auctionId);
AuctionItemVO auctionItemVO = (AuctionItemVO) auctionItemService.getAuctionITem(auctionId).getVO();
payParam.setTotalAmount(auctionItemVO.getInsurance() + ""); // 从数据库提取金额
if (!getOrderInsuranceResult.isSuccess()) {
payParam.setOutTradeNo(VarPayParamUtils.generateOutTradeNo()); // 生成商户订单号
// 创建保险订单
OrderInsuranceVO orderInsuranceVO = new OrderInsuranceVO();
orderInsuranceVO.setUserId(userId);
orderInsuranceVO.setAuctionId(auctionId);
orderInsuranceVO.setInsuranceStatus(InsuranceStatusEnum.WAITTING_PAY.getCode()); // -> 待支付状态
orderInsuranceVO.setOutTradeNo(payParam.getOutTradeNo());
// orderInsuranceVO.setInsurance(payParam.getTotal_amount());
orderInsuranceVO.setInsurance(auctionItemVO.getInsurance() + "");
userBidService.addOrderInsurance(orderInsuranceVO);
// 添加概括表
TradeGenerationDO tradeGenerationDO = new TradeGenerationDO();
tradeGenerationDO.setUserId(userId);
tradeGenerationDO.setAuctionId(auctionId);
tradeGenerationDO.setOutTradeNo(payParam.getOutTradeNo());
tradeGenerationDO.setModule(PayModuleEnum.INSURANCE_MODULE.getCode());
userBidService.addTradeGeneration(tradeGenerationDO);
} else {
payParam.setOutTradeNo(((OrderInsuranceDO) getOrderInsuranceResult.getVO()).getOutTradeNo());
}
// 调用支付宝API支付
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setReturnUrl(alipayNeedUrl + "insurancePay/return_url");
request.setNotifyUrl(alipayNeedUrl + "insurancePay/notify_url");
request.setBizModel(payParam);
String form = AlipayConfig.alipayClient.pageExecute(request).getBody();
Assert.assertEquals(false,StringUtls.isEmpty(form));
System.out.println(form);
}
AlipayConfig.alipayClient则是以下代码静态生成维护:
public static AlipayClient alipayClient = new DefaultAlipayClient(OPEN_URL,APP_ID,APP_PRIVATE_KEY,FORMAT,CHARSET,ALIPAY_PUBLIC_KEY,SIGN_TYPE);
支付宝发送异步通知将参数发送给服务器端后,拿到数据进行验签单测。
以下是本人的数据并进行验签(可取出sign字段,将剩下的字符到工具中生成签名,再放回该字段进行验签,该字符串与自己的应用公钥匹配):
tip2:SDK似乎没有异步通知的参数DTO,需要自己定义类来接收数据。
tip3:SDK中的AlipaySignature中校验方法有rsaCheckV1与rsaCheckV2,用来区分SHA1与SHA256密钥的验证。实际上rsaCheckV1也可以验签sha256密钥。
tip4:使用rsaCheckV1验签sha256密钥时候,无论有没有sign_type都能验签通过,然而rsaCheckV2验签的时候,不能含有sign_type参数。
坑2:在我询问了技术人员后才知道,原来文档上漏了一个参数,参照文档上异步通知的参数是只有31个的,实际上少了auth_app_id,该参数值与app_id一致。
以下是验签代码:
Map map = new HashMap();
String resultInfo ="gmt_create=2017-06-27 15:38:[email protected]&subject=测试订单&sign=eV1dZxV1mtSK+7IB/pfBBiaMSGPKY3zk/fJYflcc7XahwT9F7lXewwePebxO3cPp00PC1pUlkMRajIJKHHompojSFXsUdLc2pLtSrghnxMi+Q/W7HxIHAl4dnKkmUU890qFequkBoqnrlrcgcobNZOKv4i4RtsDFxDDyy2aTyb7yi8NW30muSvT9kcOr96hEtxnIDMgXxU4I76My5cqyiCgV3wRzpZeiVoURrRnLem1WDIImJ2+LEwuE8VLcyJs/jWEAEwFKpKKE8r5rZ9NeCAyLe47a9pP2IvPUmF8IMrzVCzaNn2sUDpwEJQk8Bn6yOiXLVNBGMbP7Myi1/1qs+Q==&body=测试订单内容&buyer_id=2088112008646530&invoice_amount=0.01¬ify_id=cd46801bb75798a5adbbd7458045548k3a&fund_bill_list=[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]¬ify_type=trade_status_sync&trade_status=TRADE_SUCCESS&receipt_amount=0.01&app_id=2016082601806424&buyer_pay_amount=0.01&sign_type=RSA2&seller_id=2088812720933216&gmt_payment=2017-06-27 15:38:46¬ify_time=2017-06-27 16:02:31&version=1.0&out_trade_no=149854908578621066&total_amount=0.01&trade_no=2017062721001004530210421009&auth_app_id=2016082601806424&buyer_logon_id=177****6223&point_amount=0.00";
StringTokenizer items;
for(StringTokenizer entrys = new StringTokenizer(resultInfo, "&");
entrys.hasMoreTokens(); map.put(items.nextToken(), items.hasMoreTokens() ? ((items.nextToken())) : null)){
items = new StringTokenizer(entrys.nextToken(), "="); }
map.put("sign","eV1dZxV1mtSK+7IB/pfBBiaMSGPKY3zk/fJYflcc7XahwT9F7lXewwePebxO3cPp00PC1pUlkMRajIJKHHompojSFXsUdLc2pLtSrghnxMi+Q/W7HxIHAl4dnKkmUU890qFequkBoqnrlrcgcobNZOKv4i4RtsDFxDDyy2aTyb7yi8NW30muSvT9kcOr96hEtxnIDMgXxU4I76My5cqyiCgV3wRzpZeiVoURrRnLem1WDIImJ2+LEwuE8VLcyJs/jWEAEwFKpKKE8r5rZ9NeCAyLe47a9pP2IvPUmF8IMrzVCzaNn2sUDpwEJQk8Bn6yOiXLVNBGMbP7Myi1/1qs+Q==");
Assert.assertEquals(true, AlipaySignature.rsaCheckV1(map, AlipayConfig.ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, "RSA2"));