Spring Boot版本:2.3.4.RELEASE
支付宝的支付能力有多种,常用的有当面付、App支付、手机网站支付和电脑网站支付等,关于支付能力的说明介绍可以看支付能力概览
本文以支付宝沙箱支付的电脑网站支付为示例,就是在网页中点击支付,然后跳转到支付页面。
支付宝Maven依赖
新建Spring Boot项目,添加支付宝的Maven依赖:
<!--支付宝支付-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.19.10.ALL</version>
</dependency>
依赖一直在更新,可以在 支付宝Maven项目依赖 获取最新的版本。
应用信息
要使用支付宝的支付功能首先要创建一个应用,登录支付宝开发平台的控制台,找到开发服务中的研发服务,这里就是沙箱支付模块。
沙箱已经提供了APPID、支付宝网关等信息给我们,其中我们需要自行配置RSA2密钥
RSA2密钥
支付宝有提供在线生成密钥功能,十分方便,点击生成,保存私钥公钥到本地即可。
别忘了将生成的公钥配置上,将应用的公钥拷贝进去,会生成支付宝公钥,我们要的就是这个支付宝公钥,这点要注意。
钱包
支付宝提供了沙箱支付的钱包应用下载,仅提供Android版本。
打开这个钱包应用,输入右侧的沙箱账号登录,不是自己真正的支付宝账号哦!
配置文件
在项目中将我们目前有的信息放到配置文件中:
application.yml:
server:
port: 8888
# 支付宝沙箱支付配置
alipay:
# 应用Id
appId:
# 支付宝私钥公钥
privateKey:
publicKey:
# 支付完成后同步跳转的路径
# returnUrl: http://IP:端口/success.html
# 支付完成后异步调用的地址,返回结果中,确认收款成功的key值是 trade_status
# notifyUrl:
# 签名方式
signType: RSA2
# 编码格式
charset: utf-8
# 支付宝网关
gatewayUrl: https://openapi.alipaydev.com/gateway.do
再次说明下,配置文件的publicKey,是在配置RSA2密钥的时候,通过在线生成密钥生成的公钥再次生成的支付宝公钥,这两个公钥别搞错了。
实战示例
我们需要创建以下的类:
- alipay
- AlipayOrder:alipay的订单实体类,字段是根据接口文档创建的
- AlipayProperties:alipay配置信息类
- AlipayService:alipay支付调用类
- config
- GlobalCorsConfig:跨域设置,因为等会要写一个html来测试
- controller
- PayController:支付接口
- model
- OrderVo:我们自己的订单业务实体类,这是为了更好的模拟实际场景
如果报错误:支付存在钓鱼风险!防钓鱼网站的方法,关掉支付宝开发中心相关的页面或者换浏览器就可以。
AlipayOrder:
package com.cc.alipay;
/**
* alipay的订单实体类,根据接口文档,字段要用下划线
*
* @author cc
* @date 2021-12-01 15:31
*/
public class AlipayOrder {
/**
* 用户订单号,必填
*/
private String out_trade_no;
/**
* 订单名称,必填
*/
private String subject;
/**
* 订单总金额,必填
*/
private String total_amount;
/**
* 销售产品码,必填,注:目前电脑支付场景下仅支持FAST_INSTANT_TRADE_PAY,所以可以写死
*/
private final String product_code = "FAST_INSTANT_TRADE_PAY";
...
}
AlipayProperties:
package com.cc.alipay;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 支付宝配置信息类
* @author cc
* @date 2021-12-01 15:51
*/
@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayProperties {
private String appId;
private String privateKey;
private String publicKey;
private String returnUrl;
private String notifyUrl;
private String signType;
private String charset;
private String gatewayUrl;
...
}
AlipayService:
package com.cc.alipay;
import com.alibaba.fastjson.JSON;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import org.springframework.stereotype.Component;
/**
* 支付宝调用服务
* @author cc
* @date 2021-12-01 15:53
*/
@Component
public class AlipayService {
private final AlipayProperties alipayProperties;
public AlipayService(AlipayProperties alipayProperties) {
this.alipayProperties = alipayProperties;
}
/**
* 支付接口
* @author cc
* @date 2021-12-01 15:54
*/
public String pay(AlipayOrder order) throws AlipayApiException {
// 支付宝网关
String serverUrl = alipayProperties.getGatewayUrl();
// APPID
String appId = alipayProperties.getAppId();
// 私钥
String privateKey = alipayProperties.getPrivateKey();
// 格式化为 json 格式
String format = "json";
// 字符编码格式
String charset = alipayProperties.getCharset();
// 公钥,对应APPID的那个
String alipayPublicKey = alipayProperties.getPublicKey();
// 签名方式
String signType = alipayProperties.getSignType();
// 页面跳转同步通知页面路径
String returnUrl = alipayProperties.getReturnUrl();
// 服务器异步通知页面路径
String notifyUrl = alipayProperties.getNotifyUrl();
// 1. 初始化client
AlipayClient client = new DefaultAlipayClient(serverUrl, appId, privateKey, format, charset, alipayPublicKey, signType);
// 2. 设置请求参数
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(returnUrl);
request.setNotifyUrl(notifyUrl);
System.out.println("JSON.toJSONString(order):" + JSON.toJSONString(order));
request.setBizContent(JSON.toJSONString(order));
// 3. 调用支付并获取支付结果
String result = client.pageExecute(request).getBody();
return result;
}
}
GlobalCorsConfig:
package com.cc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 全局跨域配置
* @author cc
* @date 2021-07-12 10:18
*/
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
// 允许所有域名进行跨域调用
configuration.addAllowedOrigin("*");
// configuration.addAllowedOriginPattern("*"); // SpringBoot2.4.0以后用这个
// 允许跨域发送cookie
configuration.setAllowCredentials(true);
// 放行全部原始头信息
configuration.addAllowedHeader("*");
// 允许所有请求方法跨域调用
configuration.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
}
PayController:
package com.cc.controller;
import com.alipay.api.AlipayApiException;
import com.cc.alipay.AlipayOrder;
import com.cc.alipay.AlipayService;
import com.cc.model.OrderVo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 支付接口
* @author cc
* @date 2021-12-01 16:01
*/
@RestController
public class PayController {
private final AlipayService alipayService;
public PayController(AlipayService alipayService) {
this.alipayService = alipayService;
}
@PostMapping("/order/alipay")
public String alipay(@RequestBody OrderVo orderVo) throws AlipayApiException {
AlipayOrder alipayOrder = new AlipayOrder();
alipayOrder.setOut_trade_no(orderVo.getOrderNo());
alipayOrder.setSubject(orderVo.getOrderName());
alipayOrder.setTotal_amount(orderVo.getPrice());
return alipayService.pay(alipayOrder);
}
}
OrderVo:
package com.cc.model;
/**
* 自定义的订单请求类
* @author cc
* @date 2021-12-01 16:02
*/
public class OrderVo {
// 订单编号
private String orderNo;
// 订单名
private String orderName;
// 订单金额
private String price;
...
}
然后我们要写前端页面来测试:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
</head>
<body>
<label>订单编号:</label><input id="orderNo" name="orderNo" type="text" value="123"><br>
<label>订单名:</label><input id="orderName" name="orderName" type="text" value="订单1号"><br>
<label>金额:</label><input id="price" name="price" type="text" value="1.00"><br>
<button onclick="toPay()">去支付</button>
<div id="div">
</div>
</form>
<script>
function toPay() {
var outTradeNo = $("#orderNo").val();
var subject = $("#orderName").val();
var totalAmount = $("#price").val();
var params = {
"orderNo": outTradeNo,
"orderName": subject,
"price": totalAmount
}
console.log(JSON.stringify(params))
$.ajax({
url: 'http://localhost:8888/order/alipay',
type: 'post',
headers: { "Content-Type": "application/json" },
data: JSON.stringify(params),
success: function (result) {
console.log(result)
const divForm = document.getElementsByTagName("div");
if (divForm.length) {
document.body.removeChild(divForm[0]);
}
const div = document.createElement("div");
div.innerHTML = result; // data就是接口返回的form 表单字符串
document.body.appendChild(div);
document.forms[0].setAttribute("target", "_blank"); // 新开窗口跳转
document.forms[0].submit();
},
error: function (e) {
console.log("ajax请求出现错误: ");
console.log(e);
},
});
}
</script>
</body>
</html>
前端代码很简单,简单说明下,用ajax发送post请求调用我们上面写的alipay接口,接口会返回一个html的form信息,前端接受form,将它添加到自己的模块中并调用,就会弹出一个新窗口,这个窗口就是支付宝的支付窗口。
在支付窗口扫码支付,或者输入买家的账密支付即可,这个买家信息在我们开头提到的沙箱模块中,请不要用自己的真实支付宝扫码。
内网穿透(FRP)
到这里我们有一个地方没有处理好,就是当我们扫码支付成功之后,页面无法识别不会跳转,因为我们是在本地开发环境调试的,所以在applicaiton配置文件中的returnUrl和notifyUrl都没有生效,这两个字段的生效需要公网环境,为了在本地调试,我们用内网穿透技术来实现。
内网穿透需要有一台公网服务器,这是必要条件,然后是两个闲置端口,后面会用到。
关于内网穿透,本文不具体展开,只介绍它在我们支付demo中的用法。
首先,公网服务器(Linux)和本地(Windows)下载FRP,不同的系统需要下载不同版本的,请在FRP各种版本的下载链接中下载好。
公网服务器启动server服务
找到frps.ini文件,修改内容为:
[common]
bind_port = 19000
bind_port是frp服务与frp客户端连接的端口,后面客户端要通过这个端口与服务端连接。
启动server服务:
./frps -c ./frps.ini
以下是启动成功的输出示例:
[root@izbp1id8uez7g43wcgry88z frp_0.34.3_linux_386]# ./frps -c ./frps.ini
2021/01/18 12:59:11 [I] [service.go:190] frps tcp listen on 0.0.0.0:19000
2021/01/18 12:59:11 [I] [root.go:215] start frps success
本地开发环境启动client服务
找到frpc.ini文件,注意不要和服务端文件搞错了,修改内容为:
[common]
server_addr = 服务器的ip
server_port = 19000
[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 8888
remote_port = 19001
- server_addr:公网服务器ip
- server_port 是服务器监听的端口,即frps.ini配置文件的bind_port,两者要一致
- local_ip和local_port:映射本地127.0.0.1:8888
- remote_port:通过访问服务器的这个端口来进入内网,这个可能不好理解,先照着走就明白了
在我的示例中,端口以19000和19001为示例,这个大家自行修改即可。
启动客户端服务:
./frpc -c frpc.ini
完成,此时访问 服务器ip:remote_port 就可以看到内网服务了
但是这时候我们还没有准备好内网服务,所以访问也是没用的,抓紧做下一步吧。
支付完成跳转和支付结果通知
为了方便,我们用thymeleaf来显示html页面,因为这样只需要demo的8888端口,如果我们用nginx等服务器来启动html服务,那么内网穿透就要做多一个端口,这就太麻烦啦。
导入thymeleaf框架:
<!--支持html页面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
新建两个页面,一个是提交支付的index页面,一个是支付成功后跳转的页面success
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
</head>
<body>
<label>订单编号:</label><input id="orderNo" name="orderNo" type="text" value="123"><br>
<label>订单名:</label><input id="orderName" name="orderName" type="text" value="订单1号"><br>
<label>金额:</label><input id="price" name="price" type="text" value="1.00"><br>
<button onclick="toPay()">去支付</button>
<div id="div">
</div>
</form>
<script>
function toPay() {
var outTradeNo = $("#orderNo").val();
var subject = $("#orderName").val();
var totalAmount = $("#price").val();
var params = {
"orderNo": outTradeNo,
"orderName": subject,
"price": totalAmount
}
console.log(JSON.stringify(params))
$.ajax({
url: 'http://localhost:8888/order/alipay',
type: 'post',
headers: { "Content-Type": "application/json" },
data: JSON.stringify(params),
success: function (result) {
console.log(result)
const divForm = document.getElementsByTagName("div");
if (divForm.length) {
document.body.removeChild(divForm[0]);
}
const div = document.createElement("div");
div.innerHTML = result; // data就是接口返回的form 表单字符串
document.body.appendChild(div);
document.forms[0].setAttribute("target", "_blank"); // 新开窗口跳转
document.forms[0].submit();
},
error: function (e) {
console.log("ajax请求出现错误: ");
console.log(e);
},
});
}
</script>
</body>
</html>
success.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h3>这里是支付成功后跳转的页面</h3>
</body>
</html>
编写页面获取接口WebController:
package com.cc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* web页面提供
* 用thymeleaf的时候,不要用RestController注解
* @author cc
* @date 2021-12-02 15:03
*/
@Controller
public class WebController {
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/success")
public String success() {
return "success";
}
}
在支付接口中添加一个接收支付结果通知的notify接口:
PayController:
@RestController
public class PayController {
...
@PostMapping("/notify")
public String finishNotify(HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
System.out.println("cynsjj8596@sandbox.com");
return alipayService.finishNotify(request);
}
}
ApiService(代码源于官方demo):
/**
* 接收支付完成通知
* @author cc
* @date 2021-12-02 10:34
*/
public String finishNotify(HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
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] + ",";
}
params.put(name, valueStr);
}
//调用SDK验证签名
System.out.println(params.toString());
System.out.println(alipayProperties.toString());
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayProperties.getPublicKey(), alipayProperties.getCharset(), alipayProperties.getSignType());
System.out.println("SDK验证签名结果1:" + signVerified);
if(signVerified) { //验证成功
// 商户订单号
String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");
// 支付宝交易号
String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");
// 交易状态
String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");
System.out.println("==========");
System.out.println("out_trade_no: " + out_trade_no);
System.out.println("trade_no: " + trade_no);
System.out.println("trade_status: " + trade_status);
if(trade_status.equals("TRADE_FINISHED")){
// 判断该笔订单是否在商户网站中已经做过处理
// 如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
// 如果有做过处理,不执行商户的业务程序
// 注意:
// 退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
}else if (trade_status.equals("TRADE_SUCCESS")){
// 判断该笔订单是否在商户网站中已经做过处理
// 如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
// 如果有做过处理,不执行商户的业务程序
// 注意:
// 付款完成后,支付宝系统发送该交易状态通知
}
return "success";
}else { //验证失败
// 调试用,写文本函数记录程序运行情况是否正常
// String sWord = AlipaySignature.getSignCheckContentV1(params);
// AlipayConfig.logResult(sWord);
return "fail";
}
}
/notify接口返回success告诉支付宝成功,返回fail失败。
最后完善一下我们的application配置文件中的returnUrl和notifyUrl:
server:
port: 8888
# 支付宝沙箱支付配置
alipay:
# 应用Id
appId:
# 支付宝私钥公钥
privateKey:
publicKey:
# 支付完成后同步跳转的路径
returnUrl: http://服务器IP:19001/success
# 支付完成后异步调用的地址,返回结果中,确认收款成功的key值是 trade_status
notifyUrl: http://服务器IP:19001/notify
# 签名方式
signType: RSA2
# 编码格式
charset: utf-8
# 支付宝网关
gatewayUrl: https://openapi.alipaydev.com/gateway.do
效果
再次测试支付功能,页面在支付完成后会跳转到我们的success.html,并且会发送一个通知到/notify接口。