【Alibaba】支付宝支付服务

支付宝开放平台

官方文档:https://opendocs.alipay.com/common/02kkv7

进入支付宝开放平台的沙箱环境进行开发测试:

image-20220121192322486

设置自定义 RSA2 密钥:

image-20220121191335093

点击【支付宝密钥生成器】下载支付宝开放平台开发助手,生成应用私钥应用公钥

image-20220121194115930

在【加签管理】中添加应用公钥,支付宝将为我们生成支付宝公钥:

image-20220121191805902

基础概念

使用私钥和公钥进行加密解密的方法为非对称加密。双方使用同一把钥匙进行加密解密为对称加密。

什么是公钥、私钥、加密、签名和验签?

公钥私钥

公钥和私钥是一个相对概念。它们的公私性是相对于生成者来说的。一对密钥生成后,保存在生成者手里的就是私钥,生成者发布出去大家用的就是公钥

例如:支付宝自己拥有一个私钥,给每个商户发放一个支付宝公钥。商户自己拥有一个私钥,同时给支付宝保存一个商户公钥。

签名和验签

为保证发送方发来的消息是正确无误的(没有被黑客拦截篡改消息),需要进行签名和验签:

  • 签名(加签):发送方使用自己的私钥对传输数组进行加密,生成一段数字 sign
  • 验签:接收方收到该消息后,使用商户公钥验证该 sign 信息是否匹配消息内容,如果匹配则说明消息无误;否则说明消息已在中途被篡改

商户和支付宝间的传输过程示意图:

image-20201122174141878

Spring Boot 整合 Alipay

环境配置

  1. 导入 Maven 依赖:
1
2
3
4
5
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
  1. 注入 AlipayTemplate
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
package com.atguigu.gulimall.order.config;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.atguigu.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
// 在支付宝创建的应用的id
private String app_id = "2016092200568607";

// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key = "XXX";
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key = "XXX";
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
private String notify_url = "http://member.yunmall.com/memberOrder.html";

// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 同步通知,支付成功,一般跳转到成功页
private String return_url = "http://member.yunmall.com/memberOrder.html";

// 签名方式
private String sign_type = "RSA2";

// 字符编码格式
private String charset = "utf-8";
// 订单超时时间,到达超时时间后自动关闭订单不能再继续支付
private String timeout = "30m";

// 支付宝网关; https://openapi.alipaydev.com/gateway.do
private String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

public String pay(PayVo vo) throws AlipayApiException {
// 1. 根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);

// 2. 创建一个支付请求
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);

// 商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
// 付款金额,必填
String total_amount = vo.getTotal_amount();
// 订单名称,必填
String subject = vo.getSubject();
// 商品描述,可空
String body = vo.getBody();

// timeout_express 订单支付超时时间
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\"" + timeout + "\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

String result = alipayClient.pageExecute(alipayRequest).getBody();

// 会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);

return result;
}
}

其中:

  • notify_url:支付成功异步回调。当支付成功后支付宝将一直发出该回调请求,返回支付成功相关信息。如果该地址无响应,则会不断发送直到对方应答(最大努力型通知
  • return_url同步通知,支付成功后页面跳转到那里

支付数据模型

  1. 定义发送给支付宝的支付对象 PayVo
1
2
3
4
5
6
7
8
9
10
/**
* 支付使用Vo
*/
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
  1. 定义支付宝异步回调返回的信息实体类 PayAsyncVo
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
/**
* 支付宝回调参数Vo
*/
@ToString
@Data
public class PayAsyncVo {
private String gmt_create;
private String charset;
private String gmt_payment;
private Date notify_time;
private String subject;
private String sign;
private String buyer_id; // 支付者的id
private String body; // 订单的信息
private String invoice_amount; // 支付金额
private String version;
private String notify_id; // 通知id
private String fund_bill_list;
private String notify_type; // 通知类型 trade_status_sync
private String out_trade_no; // 订单号
private String total_amount; // 支付的总额
private String trade_status; // 交易状态 TRADE_SUCCESS
private String trade_no; // 流水号
private String auth_app_id;
private String receipt_amount; // 商家收到的款
private String point_amount;
private String app_id; // 应用id
private String buyer_pay_amount;// 最终支付的金额
private String sign_type; // 签名类型
private String seller_id; // 商家的id
}

订单服务收集支付信息跳转到支付页

  1. 编写 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
@Controller
public class PayWebController {

@Autowired
AlipayTemplate alipayTemplate;

@Autowired
OrderService orderService;

/**
* 1. 跳转到支付页面
* 2. 用户支付成功后,我们要跳转到用户的订单列表页
* produces = "text/html":明确方法会返回什么类型,这里返回的是html页面
* @param orderSn
* @return
* @throws AlipayApiException
*/
@ResponseBody
@GetMapping(value = "/payOrder", produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
// Service 层设置支付信息
PayVo payvo = orderService.payOrder(orderSn);
// 将返回支付宝的支付页面,需要将这个页面进行显示
String pay = alipayTemplate.pay(payvo);
// 支付宝给我们返回的是支付页面表单,是一个html文件
return pay;
}
}

alipayTemplate.pay(payvo) 返回的是一个 html 文本,其内容为:

image-20220122213232794

它其实就是一个表单,配置了订单的数据信息。并且该表单会立即提交,带着我们传入的订单数据重定向到支付宝网关,从而重定向到支付宝的支付页面:

image-20220122213532319

在该页面支付完成后,支付宝就会立即重定向到我们在 AlipayTemplate 中配置的 return_url 地址,即会员服务的订单详情页面:

image-20220122213844309

  1. Service 层封装需要支付的信息 PayVo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 计算商品支付需要的信息
* @param orderSn
* @return
*/
@Override
public PayVo payOrder(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity orderEntity = this.getOrderByOrderSn(orderSn); // 根据订单号查询到商品
// 数据库中付款金额小数有4位,但是支付宝只接受2位,所以向上取整两位数
BigDecimal decimal = orderEntity.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
payVo.setTotal_amount(decimal.toString());
// 商户订单号
payVo.setOut_trade_no(orderSn);
// 查询出订单项,用来设置商品的描述和商品名称
List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
.eq("order_sn", orderSn));
OrderItemEntity itemEntity = itemEntities.get(0);
// 订单名称使用商品项的名字
payVo.setSubject(itemEntity.getSkuName());
// 商品的描述使用商品项的属性
payVo.setBody(itemEntity.getSkuAttrsVals());
return payVo;
}

支付成功后异步回调到会员服务

  1. 支付成功后,支付宝将异步回调到 AlipayTemplate 中配置的 notify_url 地址。先验证签名后,保存订单流水,并更改订单的支付状态为已支付
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
/**
* 支付宝成功异步回调
* @author yuyun.zhao
* @Create 2022-01-22
*/
@RestController
public class OrderPayedListener {
@Autowired
AlipayTemplate alipayTemplate;

@Autowired
OrderService orderService;

/**
* 支付宝异步通知回调接口,需要拥有内网穿透或服务器
* @param request
* @return
*/
@PostMapping("/payed/notify")
public String handleAlipayed(PayAsyncVo vo, HttpServletRequest request) throws UnsupportedEncodingException, AlipayApiException {
/**
* 重要一步验签名
* 防止别人通过postman给我们发送一个请求,告诉我们请求成功,为了防止这种效果通过验签
*/
Map<String,String> params = new HashMap<String,String>();
Map<String,String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用
valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
// 支付宝验签,防止恶意提交
boolean signVerified = AlipaySignature.rsaCheckV1(params
, alipayTemplate.getAlipay_public_key()
, alipayTemplate.getCharset()
, alipayTemplate.getSign_type());
if (signVerified) {
// 处理支付结果:保存订单流水、更改订单的支付状态为已支付
String result = orderService.handleAlipayed(vo);
return result;
} else {
return "error";
}
}
}
  1. Service保存订单流水,并更改订单的支付状态为已支付
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public String handleAlipayed(PayAsyncVo vo) {
// 保存交易流水信息,每个月和支付宝进行对账
PaymentInfoEntity infoEntity = new PaymentInfoEntity();
// 设置核心字段
infoEntity.setOrderSn(vo.getOut_trade_no());
infoEntity.setAlipayTradeNo(vo.getTrade_no());
infoEntity.setPaymentStatus(vo.getTrade_status());
infoEntity.setCallbackTime(vo.getNotify_time());
// 保存订单流水
paymentInfoService.save(infoEntity);
/**
* 支付宝交易状态说明
* https://opendocs.alipay.com/open/270/105902
*/
// TRADE_FINISHED 交易结束、不可退款
// TRADE_SUCCESS 交易支付成功
if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")) {
String outTradeNo = vo.getOut_trade_no();
// 支付宝回调成功后,更改订单的支付状态为已支付
this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
}
return "success";
}