SpringBoot集成沙箱支付——不墨迹版
一、获取沙箱配置信息
先进入支付宝的个人沙箱应用页面 https://openhome.alipay.com/develop/sandbox/app
图中 黑色框框 圈出来的我们需要的四个配置信息。
以下步骤省略创建 Spring Boot 项目过程。
教程采用版本:
- spring boot 2.6.13
- java 8
二、在 pom.xml 中引入支付宝SDK依赖
<!-- alipay -->
<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 中写入第一步获取到的配置信息
# 支付宝沙箱
myalipay:
gateway: # 支付宝网关地址
appId: # 填入APPID
appPrivateKey: # 填入Java应用私钥
alipayPublicKey: # 填入应用公钥
四、编写 AlipayConfig 沙箱支付配置类
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") // 第三步中yml配置的前缀
@Configuration
public class AlipayConfig {
/**
* 沙箱支付网关
*/
private String gateway;
/**
* 应用Id
*/
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 中了,读者可自行分层封装,降低代码耦合。
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;
/**
* 支付成功通知地址
* todo: 确保更改为最新域名
*/
private static final String NOTIFY_PATH = "http://eiabc3.natappfree.cc" + "/alipay/notify";
/**
* 支付
*
* @param tradeNo 交易单号
* @param amount 商品名称
* @return
*/
@GetMapping("/pay")
public String alipay(String tradeNo, Double amount) {
// 封装支付请求体
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
// json请求体
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;
}
/**
* 支付成功通知接口
*
* @param request
*/
@PostMapping("/notify")
public void notify(HttpServletRequest request) throws AlipayApiException {
// 除了以下三个参数外,还有其他参数,可自行debug查看
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);
}
/**
* 退款
*
* @param tradeNo 交易单号
* @param amount 商品名称
* @return
*/
@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