SpringBoot集成沙箱支付——不墨迹版

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 注解,所以直接访问该接口直接渲染成一个页面。

如果你的项目是前后端分离,可以采用如下操作:

  1. 将返回的 formPage 字符串数据插入到页面中,并切割掉 script脚本 手动执行。

<script>form[0].submit()</script>

因为 script脚本 默认执行页面中的第一个表单。

  1. 搭建 iframe 容器,插入 formPage。

六、测试

测试支付接口

这里的账号和密码是沙箱环境中的 沙箱账号

http://localhost:8101/alipay/pay?tradeNo=1772919741251236385&amount=888

在这里插入图片描述
在这里插入图片描述

支付完成后,支付宝发出通知到指定地址:

在这里插入图片描述

测试退款接口

将刚刚支付的订单退款:

http://localhost:8101/alipay/refund?tradeNo=1772919741251236385&amount=888

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值