之前開發了支付寶支付,按照官方文檔不難,最新新申請的app領導意外的配置成公鑰證書方式,由於業務的特殊性踩了不少坑,感覺有必要記錄一下整合過程,先記錄一下普通公鑰方式,也是最常用的,針對app支付,這裏主要記錄後臺服務端java版本的實現。
一、簽名
簽名方式有兩種(普通公鑰方式、公鑰證書方式),一般最常用的就是普通公鑰方式,也相對比較簡單,
申請步驟可以參照官方文檔:
參考鏈接:https://docs.open.alipay.com/291/105971/
按照文檔一步步就可以完成生成並上傳公鑰,在這裏使用開放平臺的SDK方式進行簽名及驗證,也是最簡單最高效的方式,接下來主要實現服務端代碼:
二、導入依賴
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.5.0.ALL</version>
</dependency>
三、生成APP支付訂單信息
主要有三部分:實例化客戶端、配置請求參數、發起請求。如果使用支付寶開放平臺SDK,直接用以下代碼邏輯調用即可:
//實例化客戶端
AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2");
//實例化具體API對應的request類,類名稱和接口名稱對應,當前調用接口名稱:alipay.trade.app.pay
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
//SDK已經封裝掉了公共參數,這裏只需要傳入業務參數。以下方法爲sdk的model入參方式(model和biz_content同時存在的情況下取biz_content)。
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setBody("我是測試數據");
model.setSubject("App支付測試Java");
model.setOutTradeNo(outtradeno);
model.setTimeoutExpress("30m");
model.setTotalAmount("0.01");
model.setProductCode("QUICK_MSECURITY_PAY");
request.setBizModel(model);
request.setNotifyUrl("商戶外網可以訪問的異步地址");
try {
//這裏和普通的接口調用不同,使用的是sdkExecute
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
System.out.println(response.getBody());//就是orderString 可以直接給客戶端請求,無需再做處理。
} catch (AlipayApiException e) {
e.printStackTrace();
}
調用此邏輯,返回orderString 字符串,此字符串直接發給客戶端調起支付寶支付即可,無需做其他處理。
其中需要配置回調地址,就是下面要說的處理支付寶回調的接口地址。
四、處理支付寶異步回調
客戶端調起支付並完成支付後,支付寶會異步發起回調,並返回相關數據以供後續處理,收到參數以後需要進行簽名驗證、支付狀態驗證、業務信息驗證,驗證通過後可以進行相應的業務處理。
接收參數、簽名驗證邏輯如下:
//獲取支付寶POST過來反饋信息
Map<String,String> params = new HashMap<String,String>();
Map requestParams = request.getParameterMap();
for (Iterator 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);
}
//切記alipaypublickey是支付寶的公鑰,請去open.alipay.com對應應用下查看。
//boolean AlipaySignature.rsaCheckV1(Map<String, String> params, String publicKey, String charset, String sign_type)
boolean flag = AlipaySignature.rsaCheckV1(params, alipaypublicKey, charset,"RSA2")
五、具體項目代碼
(代碼使用了springboot工程,用到lombok等,這些都可以根據實際情況自己選擇使用,這都不是重點)
這裏附上整合支付寶app支付的整體後臺服務代碼,相關參數跟業務邏輯需要根據自己業務場景跟需求進行調整或者補充,這裏只做與支付寶對接的部分:
項目整體結構
alipay文件夾是關於支付寶支付的配置及工具類,ResultStatusEnum是項目返回值狀態碼枚舉類,ResultStatus是封裝項目統一返回參數對象,PayRequest由於未摻雜業務邏輯暫時爲空
僅供參考,這都不是重點
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zrk</groupId>
<artifactId>pay-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>pay-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.4</version>
</dependency>
<!--alipay-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.5.0.ALL</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、ZrkAliPayConfig.java
package com.zrk.alipay;
/**
* @Description: 支付寶支付配置類
* @Author: zrk
* @Date: 2019/9/10
*/
public class ZrkAliPayConfig {
/**
* 支付寶公鑰
*/
public static String ALIPAY_PUBLIC_KEY = "***";
/**
* 商戶私鑰
*/
public static String APP_PRIVATE_KEY = "***";
/**
* 支付寶APPID
*/
public static String APPID = "***";
/**
* 請求網關
*/
public static String SERVERURL = "https://openapi.alipay.com/gateway.do";
/**
* 回調地址
*/
public static String ALIPAY_NOTIFY_URL = "http://zrk.com/thirdPay/aliPayNotify";
/**
* 字符集
*/
public static String CHARSET = "utf-8";
/**
* 簽名類型
*/
public static String SIGN_TYPE = "RSA2";
/**
* 格式
*/
public static String FORMAT = "json";
}
3、ZrkAliPayUtil.java
package com.zrk.alipay;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* @Description: 支付寶支付工具類
* @Author: zrk
* @Date: 2019/9/10
*/
public class ZrkAliPayUtil {
/**
* 生成訂單號
* @return
*/
public static String getOutTradeNo() {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddhhmmss");
return sdf.format(date) + getFixLengthString(5);
}
/**
* 返回長度爲【strLength】的隨機數
*/
public static String getFixLengthString(int strLength) {
Random rm = new Random();
// 獲得隨機數
double pross = (1 + rm.nextDouble()) * Math.pow(10, strLength);
// 將獲得的獲得隨機數轉化爲字符串
String fixLenthString = String.valueOf(pross);
// 返回固定的長度的隨機數
return fixLenthString.substring(1, strLength + 1);
}
}
4、ThirdPayController.java
package com.zrk.controller;
import com.zrk.model.ResultStatus;
import com.zrk.request.PayRequest;
import com.zrk.service.ThirdPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* @Description: 第三方支付接口
* @Author: zrk
* @Date: 2019/9/10
*/
@Slf4j
@RestController
@RequestMapping("thirdPay")
public class ThirdPayController {
@Resource
private ThirdPayService thirdPayService;
/**
* 支付寶下訂單接口
* 參數可根據自己的業務需求傳相應參數
* @param request
* @return
*/
@GetMapping(value = "aliPayUnifiedOrder")
public ResultStatus aliPayUnifiedOrder(PayRequest request){
return thirdPayService.aliPayUnifiedOrder(request);
}
/**
* 支付寶回調接口
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping(value = "aliPayNotify")
@ResponseBody
public String aliPayNotify(HttpServletRequest request, HttpServletResponse response) throws Exception{
//獲取支付寶POST過來反饋信息
Map requestParams = request.getParameterMap();
return thirdPayService.aliPayNotify(requestParams);
}
}
5、service
ThirdPayService.java
package com.zrk.service;
import com.zrk.model.ResultStatus;
import com.zrk.request.PayRequest;
import java.util.Map;
/**
* @Description:
* @Author: zrk
* @Date: 2019/9/10
*/
public interface ThirdPayService {
/**
* 支付寶支付統一下單接口
* @param request
* @return
*/
ResultStatus aliPayUnifiedOrder(PayRequest request);
/**
* 支付寶回調
* @param requestParams
* @return
*/
String aliPayNotify(Map requestParams);
}
ThirdPayServiceImpl.java
package com.zrk.service.impl;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.response.AlipayTradeAppPayResponse;
import com.zrk.alipay.ZrkAliPayConfig;
import com.zrk.alipay.ZrkAliPayUtil;
import com.zrk.enums.ResultStatusEnum;
import com.zrk.model.ResultStatus;
import com.zrk.request.PayRequest;
import com.zrk.service.ThirdPayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @Description:
* @Author: zrk
* @Date: 2019/9/10
*/
@Slf4j
@Service
public class ThirdPayServiceImpl implements ThirdPayService {
/**
* 分隔符
*/
private final static String attachRegex = "ZRKAPP";
@Override
public ResultStatus aliPayUnifiedOrder(PayRequest payRequest) {
/**
* 定義變量,可以根據實際需求獲取並生成相應變量,變量值僅供參考
*/
String body = "商品名稱";
String outTradeNo = ZrkAliPayUtil.getOutTradeNo();
Double totalFee = 0.01;
/**
* 拼接自己的業務參數
* 例如 用戶id + 商品id + 訂單id
* 用指定分隔符進行拼接,以便後續做業務處理
*/
String passBackParams = "123" + attachRegex + "111" + attachRegex + "222";
/**
* 以下部分可以共用,複製即可
* *********************************************************
*/
//實例化具體API對應的request類,類名稱和接口名稱對應,當前調用接口名稱:alipay.trade.app.pay
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
//SDK已經封裝掉了公共參數,這裏只需要傳入業務參數。以下方法爲sdk的model入參方式(model和biz_content同時存在的情況下取biz_content)。
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setBody(body);
model.setSubject(body);
model.setOutTradeNo(outTradeNo);
model.setTimeoutExpress("30m");
model.setTotalAmount(totalFee + "");
model.setProductCode("QUICK_MSECURITY_PAY");
model.setPassbackParams(URLEncoder.encode(passBackParams));
request.setBizModel(model);
request.setNotifyUrl(ZrkAliPayConfig.ALIPAY_NOTIFY_URL);
log.info(">>>>支付寶統一下單接口請求參數:" + model.getBody() + "," + model.getOutTradeNo() + "," + model.getTotalAmount());
/**實例化客戶端*/
AlipayClient alipayClient = new DefaultAlipayClient(
ZrkAliPayConfig.SERVERURL,
ZrkAliPayConfig.APPID,
ZrkAliPayConfig.APP_PRIVATE_KEY,
ZrkAliPayConfig.FORMAT,
ZrkAliPayConfig.CHARSET,
ZrkAliPayConfig.ALIPAY_PUBLIC_KEY,
ZrkAliPayConfig.SIGN_TYPE);
try {
//這裏和普通的接口調用不同,使用的是sdkExecute
AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
//就是orderString 可以直接給客戶端請求,無需再做處理。
log.info(">>>生成調用支付寶參數" + response.getBody());
/**************************************************************/
ResultStatus resultStatus = new ResultStatus(ResultStatusEnum.SUCCESS.getStatus());
resultStatus.setData(response.getBody());
return resultStatus;
} catch (AlipayApiException e) {
log.error(e.getMessage(), e);
}
return new ResultStatus(ResultStatusEnum.ERROR.getStatus(),"支付寶下訂單失敗");
}
@Override
public String aliPayNotify(Map requestParams) {
log.info(">>>支付寶回調參數:" + requestParams);
Map<String,String> params = new HashMap<>();
for (Iterator 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);
}
log.info(">>>支付寶回調參數解析:" + params);
try {
//切記alipaypublickey是支付寶的公鑰,請去open.alipay.com對應應用下查看。
boolean flag = AlipaySignature.rsaCheckV1(
params,
ZrkAliPayConfig.ALIPAY_PUBLIC_KEY,
ZrkAliPayConfig.CHARSET, ZrkAliPayConfig.SIGN_TYPE);
if(flag) {
log.info(">>>支付寶回調簽名認證成功");
//商戶訂單號
String out_trade_no = params.get("out_trade_no");
//交易狀態
String trade_status = params.get("trade_status");
//交易金額
String amount = params.get("total_amount");
//商戶app_id
String app_id = params.get("app_id");
if ("TRADE_SUCCESS".equals(trade_status) || "TRADE_FINISHED".equals(trade_status)) {
/**
* 自己的業務處理
*/
} else {
log.error("沒有處理支付寶回調業務,支付寶交易狀態:{},params:{}",trade_status,params);
}
} else {
log.info("支付寶回調簽名認證失敗,signVerified=false, params:{}", params);
return "failure";
}
} catch (Exception e){
log.error(e.getMessage(), e);
log.info("支付寶回調簽名認證失敗,signVerified=false, params:{}", params);
return "failure";
}
return "success";//請不要修改或刪除
}
}
6、其他(可換用自己項目邏輯)
ResultStatus.java
package com.zrk.model;
import com.alipay.api.domain.PageInfo;
import com.zrk.enums.ResultStatusEnum;
import lombok.Data;
import org.apache.commons.lang.StringUtils;
import java.io.Serializable;
/**
* 統一封裝返回值對象
* @Description: 返回對象包裝類
* @Author: zrk
* @Date: 2019/9/10
*/
@Data
public class ResultStatus implements Serializable{
private static final long serialVersionUID = -3276270216128997806L;
/**狀態碼*/
private Integer status = ResultStatusEnum.SUCCESS.getStatus();
/**信息*/
private String msg = "";
/**數據*/
private Object data;
/**分頁信息*/
private PageInfo page;
public ResultStatus(){
setStatus(ResultStatusEnum.SUCCESS.getStatus());
}
public ResultStatus(Integer status) {
setStatus(status);
this.data = null;
this.page = null;
}
public ResultStatus(Integer status, String msg) {
setStatus(status);
this.msg = msg;
this.data = null;
this.page = null;
}
private void setStatus(Integer status){
this.status = status;
try{
if(StringUtils.isNotEmpty(ResultStatusEnum.getMsgByStatus(status))){
this.msg = ResultStatusEnum.getMsgByStatus(status);
}
} catch (Exception e){
}
}
}
ResultStatusEnum.java
package com.zrk.enums;
/**
* @Description: 返回值狀態碼
* @Author: zrk
* @Date: 2019/8/3
*/
public enum ResultStatusEnum {
/**操作成功*/
SUCCESS(0,"成功"),
/**參數錯誤*/
PARAM_INVALID(4001,"參數錯誤"),
/**操作失敗*/
ERROR(5001,"操作失敗"),
/**結果爲空*/
RESULT_EMPTY(5002,"返回結果爲空")
;
private Integer status;
private String msg;
ResultStatusEnum(Integer status, String msg) {
this.status = status;
this.msg = msg;
}
public Integer getStatus() {
return status;
}
public String getMsg(){
return msg;
}
public static String getMsgByStatus(Integer status){
if(status == null){
return "";
}
for(ResultStatusEnum resultStatusEnum : values()){
if(resultStatusEnum.getStatus().equals(status)){
return resultStatusEnum.getMsg();
}
}
return "";
}
}
六、測試
支付寶下訂單接口
使用postman訪問 localhost:30040/thirdPay/aliPayUnifiedOrder,
返回結果:
{
"status": 0,
"msg": "成功",
"data": "alipay_sdk=alipay-sdk-java-4.5.0.ALL&app_id=201904****237&biz_content=%7B%22body%22%22%E5%95%86%E5%93%81%E5%90%8D%E7%A7%B0%22%2C%22out_trade_no%22%3A%222019091012230100000%22%2C%22passback_params%22%3A%22123ZRKAPP111ZRKAPP222%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%2C%22subject%22%3A%22%E5%95%86%E5%93%81%E5%90%8D%E7%A7%B0%22%2C%22timeout_express%22%3A%2230m%22%2C%22total_amount%22%3A%220.01%22%7D&charset=utf-8&format=json&method=alipay.trade.app.pay¬ify_url=http%3A%2F%2Fzrk.com%2FthirdPay%2FalipayNotify&sign=PHwxWegDutk6U41EvxatsgQF3Wyglxef1%2BxQr8X%2BkosNveCO%2By3URYwAE677HOxHyvaWTEmvQMcLvanFoXaNtOq65sdIG%2BOwtikV93FD02EZpeOrvR2ULYAOlFDUZkJnurJIMbT9TW4%2FJh4vL0sNIj1n%2BdqCGZUsFXFtYNarZmagKT1Qs7pJ3WjXzPfCn03lw%2Bp4b47YQcHnO%2B6PxNtuIrUebb23DQ9KtmQTbyGadkNwCxaakqsmhYqQCDEgfr4jYel%2BPZrdMzbiTiEQ7kjV58EPce6N8e7cz1GSU3iYacCJQ%2FOP7%2BlYKV1Q7REwSxPni4Uro94kVwDZ7yesj0qg%3D%3D&sign_type=RSA2×tamp=2019-09-10+12%3A23%3A01&version=1.0",
"page": null
}
其中data內容就是給客戶端調起支付寶支付用的,出於保密對參數進行了部分處理
支付寶回調
參數是支付寶回調返回的,方便調試就將參數一一複製到postman進行模擬調試
官方文檔有參數說明:
參考鏈接:
1、App支付服務端 DEMO & SDK :https://docs.open.alipay.com/54/106370/
2、簽名文檔:https://docs.open.alipay.com/291/105974/
3、參數說明:https://docs.open.alipay.com/204/105301/