SpringBoot集成沙箱支付——不墨迹版
一、获取沙箱配置信息
先进入支付宝的个人沙箱应用页面 https://openhome.alipay.com/develop/sandbox/app
图中 黑色框框 圈出来的我们需要的四个配置信息。
以下步骤省略创建 Spring Boot 项目过程。
教程采用版本:
- spring boot 2.6.13
- java 8
二、在 pom.xml 中引入支付宝SDK依赖
1 2 3 4 5 6
| <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.34.0.ALL</version> </dependency>
|
经监测,开源的Java开发组件Fastjson存在远程代码执行漏洞,攻击者可利用上述漏洞远程执行任意代码。Java SDK(alipay-sdk-java)在4.34.0版本之前使用了存在漏洞的Fastjson版本(详情可查看 关于Fastjson漏洞预警的公告)。
建议将上述SDK升级至 4.34.0 及以上版本
——摘自官方文档
三、往 application.yml 中写入第一步获取到的配置信息
1 2 3 4 5 6
| myalipay: gateway: appId: appPrivateKey: alipayPublicKey:
|
四、编写 AlipayConfig 沙箱支付配置类
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package com.example.alipaysandboxdemo.config;
import com.alipay.api.DefaultAlipayClient; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Data @ConfigurationProperties("myalipay") @Configuration public class AlipayConfig {
private String gateway;
private String appId;
private String appPrivateKey;
private String alipayPublicKey;
public static final String FORMAT = "JSON";
public static final String CHARSET = "UTF-8";
public static final String SIGN_TYPE = "RSA2";
@Bean public DefaultAlipayClient defaultAlipayClient() { return new DefaultAlipayClient( gateway, appId, appPrivateKey, FORMAT, CHARSET, alipayPublicKey, SIGN_TYPE ); }
}
|
从上述代码可见,向 AlipayConfig 类注入了第三步中写入的配置信息,并创建了一个 DefaultAlipayClient 的 Bean,后续沙箱支付的操作基本都使用 DefaultAlipayClient 完成。
五、编写沙箱支付相关接口
在编写接口请求之前,需要知道一件事:
在服务器通过沙箱支付成功后,支付宝会发出一个携带本次支付相关信息参数的请求,通知该服务器
服务器:这里指本地的 Springboot 后端程序
既然是请求,就需要给它提供一个接口访问,让它能够把本次支付信息传过来。
问题是:支付宝发出的请求只能访问外网的地址,而在本地的 tomcat 服务器属于内网。
因此需要通过使用 内网穿透 工具获取一个临时的公网域名,让支付宝能够正常访问到。
内网穿透
这里使用 netapp 搭建
netapp 官方教程:https://natapp.cn/article/natapp_newbie
==注意:隧道对应的本地端口应改为自己的 Spring Boot 项目启动端口==
如果忘记改了,也可以自己在 “我的隧道” 那里配置刚才创建的隧道
运行成功后,得到如下界面:
==注意:圈出来的是临时域名,每次重新运行都会更改,应该保证代码里写的是最新的域名==
编写 AlipayController 类
为了省事,业务逻辑全部写在 Controller 中了,读者可自行分层封装,降低代码耦合。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| package com.example.alipaysandboxdemo.controller;
import com.alibaba.fastjson.JSONObject; import com.alipay.api.AlipayApiException; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.internal.util.AlipaySignature; import com.alipay.api.request.AlipayTradePagePayRequest; import com.alipay.api.request.AlipayTradeRefundRequest; import com.alipay.api.response.AlipayTradeRefundResponse; import com.example.alipaysandboxdemo.config.AlipayConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map;
@Slf4j @RequestMapping("/alipay") @RestController public class AlipayController {
@Resource private AlipayConfig alipayConfig;
@Resource private DefaultAlipayClient defaultAlipayClient;
private static final String NOTIFY_PATH = "http://eiabc3.natappfree.cc" + "/alipay/notify";
@GetMapping("/pay") public String alipay(String tradeNo, Double amount) { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", tradeNo); bizContent.put("subject", "遥遥领先 华为meta60"); bizContent.put("total_amount", amount); bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); request.setNotifyUrl(NOTIFY_PATH); request.setBizContent(bizContent.toString()); String formPage; try { formPage = defaultAlipayClient.pageExecute(request).getBody(); } catch (AlipayApiException e) { throw new RuntimeException("订单支付异常:" + tradeNo, e); } return formPage; }
@PostMapping("/notify") public void notify(HttpServletRequest request) throws AlipayApiException { String tradeStatus = request.getParameter("trade_status"); String tradeNo = request.getParameter("out_trade_no"); Double amount = Double.valueOf(request.getParameter("total_amount")); if (!"TRADE_SUCCESS".equals(tradeStatus)) { System.out.println("订单支付失败:" + tradeNo); } Map<String, String> params = new HashMap<>(); for (String name : request.getParameterMap().keySet()) { params.put(name, request.getParameter(name)); } String content = AlipaySignature.getSignCheckContentV1(params); String alipayPublicKey = alipayConfig.getAlipayPublicKey(); String sign = request.getParameter("sign"); boolean check = AlipaySignature.rsa256CheckContent(content, sign, alipayPublicKey, AlipayConfig.CHARSET); if (!check) { System.out.println("订单验签异常:" + tradeNo); } System.out.println("订单支付成功:" + tradeNo); }
@GetMapping("/refund") public void refund(String tradeNo, Double amount) throws AlipayApiException { AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); JSONObject bizContent = new JSONObject(); bizContent.put("out_trade_no", tradeNo); bizContent.put("refund_amount", amount); request.setBizContent(bizContent.toString()); AlipayTradeRefundResponse response = defaultAlipayClient.execute(request);; if (!response.isSuccess()) { System.out.println("订单退款失败:" + tradeNo); } System.out.println("订单退款成功:" + tradeNo); } }
|
前后端分离
/alipay/pay 支付接口返回的 formPage 实际上是一个 HTML片段,支付宝将我们发出的支付请求参数封装成了一个 form表单 并通过 script脚本 立即执行,用于跳转支付宝支付页面(需要联网)。
这里由于标注了 @RestController
注解,所以直接访问该接口直接渲染成一个页面。
如果你的项目是前后端分离,可以采用如下操作:
将返回的 formPage 字符串数据插入到页面中,并切割掉 script脚本 手动执行。
<script>form[0].submit()</script>
因为 script脚本 默认执行页面中的第一个表单。
搭建 iframe 容器,插入 formPage。
六、测试
测试支付接口
这里的账号和密码是沙箱环境中的 沙箱账号
http://localhost:8101/alipay/pay?tradeNo=1772919741251236385&amount=888
支付完成后,支付宝发出通知到指定地址:
测试退款接口
将刚刚支付的订单退款:
http://localhost:8101/alipay/refund?tradeNo=1772919741251236385&amount=888