首页 > 技术文章 > Spring Boot 整合支付宝支付

daijf 2020-09-04 17:35 原文

接入准备

在整合之前,需要你已经注册成为支付宝开发者。我在 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

推荐阅读