在整合之前,需要你已经注册成为支付宝开发者。我在 jsp对接支付宝支付接口,实现网站在线支付 这篇文章中有写到接入流程,可供参考。
在整合之前,先来看一下支付宝的支付流程,如下图所示:
调用顺序如下:
商户系统请求支付宝接口 alipay.trade.page.pay,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。
用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
若由于网络等问题异步通知没有到达,商户可自行调用交易查询接口 alipay.trade.query 进行查询,根据查询接口获取交易以及支付信息(商户也可以直接调用查询接口,不需要依赖异步通知)。
支付结果有两种通知方式:
所以,由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
请见官方文档:支付宝异步通知
了解了这些,接下来就可以整合支付接口了。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alipay.sdkgroupId>
<artifactId>alipay-sdk-javaartifactId>
<version>3.7.26.ALLversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
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
@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
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;
}
}
点击付款,就可以到支付宝的支付界面了,如下:
扫码(使用沙箱环境的支付宝,不是真实的支付宝)付款即可。
通过上面的步骤,已经可以进行支付了。但是,这只是同步通知,真实的支付结果需要使用异步通知。
首先,我们需要一个提供异步通知的 URL,并且,这个 URL 必须可以通过 外网 访问(如果没有服务器,可以使用内网穿透)。这里推荐一款内网穿透工具 NATAPP。
异步通知的特性:
验签:
在异步通知中,有一个验签的过程,这个过程就是为了保证我们这个接口的调用方是支付宝的服务器,而不是其他服务器。 验签过程如下:
第一步: 在通知返回参数列表中,除去 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:
/**
* 异步通知支付结果
* @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 转为 Map
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;
}
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 转为 Map
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