Spring Boot 整合支付宝

tech2024-12-30  12

文章目录

接入准备支付流程Spring Boot 整合支付结果异步通知

接入准备

在整合之前,需要你已经注册成为支付宝开发者。我在 jsp对接支付宝支付接口,实现网站在线支付 这篇文章中有写到接入流程,可供参考。

支付流程

在整合之前,先来看一下支付宝的支付流程,如下图所示: 调用顺序如下:

商户系统请求支付宝接口 alipay.trade.page.pay,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。

用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。

交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。

若由于网络等问题异步通知没有到达,商户可自行调用交易查询接口 alipay.trade.query 进行查询,根据查询接口获取交易以及支付信息(商户也可以直接调用查询接口,不需要依赖异步通知)。


支付结果有两种通知方式:

同步通知 简单来讲,同步通知就是通知给用户的。同步通知将支付结果通过界面的方式通知给用户,如下图所示的就是同步通知: 异步通知 异步通知是将支付结果通知给服务器,在我们扫码支付时,支付宝服务器会每隔一段时间调用这个异步通知接口,直到我们支付完成。

所以,由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。

请见官方文档:支付宝异步通知


了解了这些,接下来就可以整合支付接口了。


Spring Boot 整合

添加 Maven 依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 支付宝 --> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>3.7.26.ALL</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> 配置 import java.io.FileWriter; import java.io.IOException; public class AlipayConfig { // 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号 public static String app_id = ""; // 商户私钥,您的PKCS8格式RSA2私钥 public static String merchant_private_key = ""; // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 public static String alipay_public_key = ""; // 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 public static String notify_url = ""; // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 // 即支付成功之后,需要跳转到的页面,一般为网站的首页 public static String return_url = "http://www.baidu.com"; // 签名方式 public static String sign_type = "RSA2"; // 字符编码格式 public static String charset = "utf-8"; // 支付宝网关 public static String gatewayUrl = "https://openapi.alipaydev.com/gateway.do"; // 日志存储路径 public static String log_path = "C:\\"; /** * 写日志,方便测试(看网站需求,也可以改成把记录存入数据库) * @param sWord 要写入日志里的文本内容 */ public static void logResult(String sWord) { FileWriter writer = null; try { writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis()+".txt"); writer.write(sWord); } catch (Exception e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } }

也可以将这些配置信息放入 application.properties 配置文件中,通过 @ConfigurationProperties(prefix = "xxx") 或者 @Value 的方式进行赋值。

去付款界面 index.html <form action="/pay/topay"> <button type="submit">付款</button> </form> controller @Controller @RequestMapping("/pay") public class PayController { @Autowired private AlipayService alipayService; @GetMapping("/hello") public String hello() { return "index"; } /** * 跳转到支付界面 * @return * @throws Exception */ @GetMapping("/topay") @ResponseBody public String pay() throws Exception { String form = alipayService.toPay(String.valueOf(new Date().getTime()), 720.0, "易购商城", "订单描述"); return form; } } service @Service public class AlipayService { public String toPay(String orderId, double price, String orderName, String orderDesc) throws Exception{ //获得初始化的AlipayClient AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type); //设置请求参数 AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); alipayRequest.setReturnUrl(AlipayConfig.return_url); alipayRequest.setNotifyUrl(AlipayConfig.notify_url); //商户订单号,商户网站订单系统中唯一订单号,必填 String out_trade_no = orderId; //付款金额,必填 String total_amount = String.valueOf(price); //订单名称,必填 String subject = orderName; //商品描述,可空 String body = orderDesc; alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," + "\"total_amount\":\""+ total_amount +"\"," + "\"subject\":\""+ subject +"\"," + "\"body\":\""+ body +"\"," + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}") String form = ""; AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest); if (response.isSuccess()) { form = alipayClient.pageExecute(alipayRequest).getBody(); } // 这里返回的 form 是一个字符串,里面封装了支付的表单信息 //(即 html 标签 和 javascript 代码),直接将这个 form 输出到页面即可。 return form; } }

然后启动项目,访问 localhost:8080,如下:

点击付款,就可以到支付宝的支付界面了,如下: 扫码(使用沙箱环境的支付宝,不是真实的支付宝)付款即可。


通过上面的步骤,已经可以进行支付了。但是,这只是同步通知,真实的支付结果需要使用异步通知。

支付结果异步通知

首先,我们需要一个提供异步通知的 URL,并且,这个 URL 必须可以通过 外网 访问(如果没有服务器,可以使用内网穿透)。这里推荐一款内网穿透工具 NATAPP。


异步通知的特性:

支付宝是用 POST 方式发送通知信息,所有请求信息被保存在 request 中。服务器间的交互,不像页面跳转同步通知可以在页面上显示出来,这种交互方式是不可见的程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h)程序执行完成后,该页面不能执行页面跳转。如果执行页面跳转,支付宝会收不到 success 字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知cookies、session 等在此页面会失效,即无法获取这些数据;该方式的调试与运行必须在服务器上,即互联网上能访问;该方式的作用主要防止订单丢失,即页面跳转同步通知没有处理订单更新,所以应该在异步通知中更新订单;

验签:

在异步通知中,有一个验签的过程,这个过程就是为了保证我们这个接口的调用方是支付宝的服务器,而不是其他服务器。 验签过程如下:

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

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

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

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

看似复杂,其实只需调用验签的 API 即可,如下:

boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, ALIPAY_PUBLIC_KEY, CHARSET, SIGN_TYPE) //调用SDK验证签名

前面介绍了异步通知的流程,接下来就是进行异步通知接口的开发。

配置 在支付宝配置类 AlipayConfig.java中配置 notify_url 属性,这个属性就是异步通知的 URL。 // 比如 public static String notify_url = "http://xxx.top/pay/callback"; // xxx.top 为你的域名或者外网可访问的 ip 地址。 // pay/callback 为后台请求路径,即 controller 中的 RequestMapping。

同时,需要在沙箱环境中配置授权回调地址为异步通知的 URL:

controller /** * 异步通知支付结果 * @param request * @return * @throws AlipayApiException */ @PostMapping("/callback") public String alipayNotify(HttpServletRequest request) throws AlipayApiException { String success = "success"; String failure = "failure"; //获取支付宝的请求信息 Map<String,String> params = new HashMap<>(); Map<String,String[]> requestParams = request.getParameterMap(); // 将 Map<String,String[]> 转为 Map<String,String> for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) { String name = iter.next(); String[] values = requestParams.get(name); String valueStr = ""; for (int i = 0; i < values.length; i++) { valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ","; } params.put(name, valueStr); } params.remove("sign_type"); // 验签 boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type); // 验签通过 if (signVerified) { System.out.println("通过验签"); // 更新订单信息 String result = alipayService.updateOrder(params); if ("success".equals(result)) { System.out.println("controller支付成功"); return success; } } return failure; } service public String updateOrder(Map<String, String> params) { if (params == null || params.isEmpty()){ return "success"; } String orderId = params.get("out_trade_no"); System.out.println("service订单id:" + orderId); // PayOrderDTO order = orderService.getOrderById(orderId); // 如果订单不存在,则支付操作无意义 // 不让支付宝再继续调用异步通知(返回为 SUCCESS 后,支付宝将不再调用)。 // if (order == null) { // return "success"; // } // 判断订单状态是否已经被修改 // int orderStatus = orderService.getOrderStatus(orderId); // if (orderStatus == 1){ // return "success"; // } String tradeStatus = params.get("trade_status"); // 支付成功 if ("TRADE_SUCCESS".equals(tradeStatus)){ // 更新订单信息 // ... System.out.println("订单支付成功service"); return "success"; } return "failure"; }

到此,Spring Boot 就简单的整合了支付宝。完整代码如下:

AlipayConfig.java public class AlipayConfig { // 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号 public static String app_id = ""; // 商户私钥,您的PKCS8格式RSA2私钥 public static String merchant_private_key = ""; // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。 public static String alipay_public_key = ""; // 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 public static String notify_url = ""; // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 // 即支付成功之后,需要跳转到的页面,一般为网站的首页 // 便于测试,直接使用了 baidu public static String return_url = "http://www.baidu.com"; // 签名方式 public static String sign_type = "RSA2"; // 字符编码格式 public static String charset = "utf-8"; // 支付宝网关 public static String gatewayUrl = "https://openapi.alipaydev.com/gateway.do"; // 日志存储路径 public static String log_path = "C:\\"; /** * 写日志,方便测试(看网站需求,也可以改成把记录存入数据库) * @param sWord 要写入日志里的文本内容 */ public static void logResult(String sWord) { FileWriter writer = null; try { writer = new FileWriter(log_path + "alipay_log_" + System.currentTimeMillis()+".txt"); writer.write(sWord); } catch (Exception e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } } AlipayService.java @Service public class AlipayService { // 跳转到支付界面 public String toPay(String orderId, double price, String orderName, String orderDesc) throws Exception{ //获得初始化的AlipayClient AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.gatewayUrl, AlipayConfig.app_id, AlipayConfig.merchant_private_key, "json", AlipayConfig.charset, AlipayConfig.alipay_public_key, AlipayConfig.sign_type); //设置请求参数 AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest(); alipayRequest.setReturnUrl(AlipayConfig.return_url); alipayRequest.setNotifyUrl(AlipayConfig.notify_url); //商户订单号,商户网站订单系统中唯一订单号,必填 String out_trade_no = orderId; //付款金额,必填 String total_amount = String.valueOf(price); //订单名称,必填 String subject = orderName; //商品描述,可空 String body = orderDesc; alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\"," + "\"total_amount\":\""+ total_amount +"\"," + "\"subject\":\""+ subject +"\"," + "\"body\":\""+ body +"\"," + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}") String form = ""; AlipayTradePagePayResponse response = alipayClient.pageExecute(alipayRequest); if (response.isSuccess()) { form = alipayClient.pageExecute(alipayRequest).getBody(); } return form; } // 更新订单 public String updateOrder(Map<String, String> params) { if (params == null || params.isEmpty()){ return "success"; } String orderId = params.get("out_trade_no"); System.out.println("service订单id:" + orderId); // PayOrderDTO order = orderService.getOrderById(orderId); // 如果订单不存在,则支付操作无意义 // 不让支付宝再继续调用异步通知(返回为 SUCCESS 后,支付宝将不再调用)。 // if (order == null) { // return "success"; // } // 判断订单状态是否已经被修改 // int orderStatus = orderService.getOrderStatus(orderId); // if (orderStatus == 1){ // return "success"; // } String tradeStatus = params.get("trade_status"); // 支付成功 if ("TRADE_SUCCESS".equals(tradeStatus)){ // 更新订单信息 // ... System.out.println("订单支付成功service"); return "success"; } return "failure"; } } PayController.java @Controller @RequestMapping("/pay") public class PayController { @Autowired private AlipayService alipayService; @GetMapping("/hello") public String hello() { return "index"; } /** * 跳转到支付界面 * @return * @throws Exception */ @GetMapping("/topay") @ResponseBody public String pay() throws Exception { String form = alipayService.toPay(String.valueOf(new Date().getTime()), 720.0, "易购商城", "订单描述"); return form; } /** * 异步通知支付结果 * @param request * @return * @throws AlipayApiException */ @PostMapping("/callback") public String alipayNotify(HttpServletRequest request) throws AlipayApiException { String success = "success"; String failure = "failure"; //获取支付宝的请求信息 Map<String,String> params = new HashMap<>(); Map<String,String[]> requestParams = request.getParameterMap(); // 将 Map<String,String[]> 转为 Map<String,String> for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) { String name = iter.next(); String[] values = requestParams.get(name); String valueStr = ""; for (int i = 0; i < values.length; i++) { valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ","; } params.put(name, valueStr); } params.remove("sign_type"); // 验签 boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.alipay_public_key, AlipayConfig.charset, AlipayConfig.sign_type); // 验签通过 if (signVerified) { System.out.println("通过验签"); // 更新订单信息 String result = alipayService.updateOrder(params); if ("success".equals(result)) { System.out.println("controller支付成功"); return success; } } return failure; } } index.html <form action="/pay/topay"> <button type="submit">付款</button> </form>

官方文档: https://opendocs.alipay.com/open/270/105899 https://opendocs.alipay.com/open/270/105902

最新回复(0)