前言
對微信支付的H5、JSAPI、H5、App、小程序支付方式進(jìn)行統(tǒng)一,此封裝接口適用于普通商戶模式支付,如果要進(jìn)行服務(wù)商模式支付可以結(jié)合服務(wù)商官方API進(jìn)行參數(shù)修改(未驗證可行性)。
1、引入POM
<!--微信支付SDK-->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.7</version>
</dependency>
2、配置Yaml
wxpay:
#應(yīng)用編號
appId: xxxx
#商戶號
mchId: xxx
# APIv2密鑰
apiKey: xxxx
# APIv3密鑰
apiV3Key: xxx
# 微信支付V3-url前綴
baseUrl: https://api.mch.weixin.qq.com/v3
# 支付通知回調(diào), pjm6m9.natappfree.cc 為內(nèi)網(wǎng)穿透地址
notifyUrl: http://pjm6m9.natappfree.cc/pay/payNotify
# 退款通知回調(diào), pjm6m9.natappfree.cc 為內(nèi)網(wǎng)穿透地址
refundNotifyUrl: http://pjm6m9.natappfree.cc/pay/refundNotify
# 密鑰路徑,resources根目錄下
keyPemPath: apiclient_key.pem
#商戶證書序列號
serialNo: xxxxx
3、配置密鑰文件
在商戶/服務(wù)商平臺的”賬戶中心" => “API安全” 進(jìn)行API證書、密鑰的設(shè)置,API證書主要用于獲取“商戶證書序列號”以及“p12”、“key.pem”、”cert.pem“證書文件,j將獲取的apiclient_key.pem
文件放在項目的resources
目錄下。
4、配置PayConfig
WechatPayConfig:
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
/**
* @Author:
* @Description:
**/
@Component
@Data
@Slf4j
@ConfigurationProperties(prefix = "wxpay")
public class WechatPayConfig {
/**
* 應(yīng)用編號
*/
private String appId;
/**
* 商戶號
*/
private String mchId;
/**
* 服務(wù)商商戶號
*/
private String slMchId;
/**
* APIv2密鑰
*/
private String apiKey;
/**
* APIv3密鑰
*/
private String apiV3Key;
/**
* 支付通知回調(diào)地址
*/
private String notifyUrl;
/**
* 退款回調(diào)地址
*/
private String refundNotifyUrl;
/**
* API 證書中的 key.pem
*/
private String keyPemPath;
/**
* 商戶序列號
*/
private String serialNo;
/**
* 微信支付V3-url前綴
*/
private String baseUrl;
/**
* 獲取商戶的私鑰文件
* @param keyPemPath
* @return
*/
public PrivateKey getPrivateKey(String keyPemPath){
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
if(inputStream==null){
throw new RuntimeException("私鑰文件不存在");
}
return PemUtil.loadPrivateKey(inputStream);
}
/**
* 獲取證書管理器實例
* @return
*/
@Bean
public Verifier getVerifier() throws GeneralSecurityException, IOException, HttpCodeException, NotFoundException {
log.info("獲取證書管理器實例");
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(keyPemPath);
//私鑰簽名對象
PrivateKeySigner privateKeySigner = new PrivateKeySigner(serialNo, privateKey);
//身份認(rèn)證對象
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 使用定時更新的簽名驗證器,不需要傳入證書
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(mchId,wechatPay2Credentials,apiV3Key.getBytes(StandardCharsets.UTF_8));
return certificatesManager.getVerifier(mchId);
}
/**
* 獲取支付http請求對象
* @param verifier
* @return
*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(Verifier verifier) {
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(keyPemPath);
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, serialNo, privateKey)
.withValidator(new WechatPay2Validator(verifier));
// 通過WechatPayHttpClientBuilder構(gòu)造的HttpClient,會自動的處理簽名和驗簽,并進(jìn)行證書自動更新
return builder.build();
}
/**
* 獲取HttpClient,無需進(jìn)行應(yīng)答簽名驗證,跳過驗簽的流程
*/
@Bean(name = "wxPayNoSignClient")
public CloseableHttpClient getWxPayNoSignClient(){
//獲取商戶私鑰
PrivateKey privateKey = getPrivateKey(keyPemPath);
//用于構(gòu)造HttpClient
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
//設(shè)置商戶信息
.withMerchant(mchId, serialNo, privateKey)
//無需進(jìn)行簽名驗證、通過withValidator((response) -> true)實現(xiàn)
.withValidator((response) -> true);
// 通過WechatPayHttpClientBuilder構(gòu)造的HttpClient,會自動的處理簽名和驗簽,并進(jìn)行證書自動更新
return builder.build();
}
}
6、回調(diào)校驗器
用于對微信支付成功后的回調(diào)數(shù)據(jù)進(jìn)行簽名驗證,保證數(shù)據(jù)的安全性與真實性。
WechatPayValidator:
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
* @Author:
* @Description:
**/
@Slf4j
public class WechatPayValidator {
/**
* 應(yīng)答超時時間,單位為分鐘
*/
private static final long RESPONSE_EXPIRED_MINUTES = 5;
private final Verifier verifier;
private final String requestId;
private final String body;
public WechatPayValidator(Verifier verifier, String requestId, String body) {
this.verifier = verifier;
this.requestId = requestId;
this.body = body;
}
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
protected static IllegalArgumentException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
public final boolean validate(HttpServletRequest request) {
try {
//處理請求參數(shù)
validateParameters(request);
//構(gòu)造驗簽名串
String message = buildMessage(request);
String serial = request.getHeader(WECHAT_PAY_SERIAL);
String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
//驗簽
if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature, requestId);
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
private void validateParameters(HttpServletRequest request) {
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
String header = null;
for (String headerName : headers) {
header = request.getHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
//判斷請求是否過期
String timestampStr = header;
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒絕過期請求
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
private String buildMessage(HttpServletRequest request) {
String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
String nonce = request.getHeader(WECHAT_PAY_NONCE);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
private String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
/**
* 對稱解密,異步通知的加密數(shù)據(jù)
* @param resource 加密數(shù)據(jù)
* @param apiV3Key apiV3密鑰
* @param type 1-支付,2-退款
* @return
*/
public static Map<String, Object> decryptFromResource(String resource,String apiV3Key,Integer type) {
String msg = type==1?"支付成功":"退款成功";
log.info(msg+",回調(diào)通知,密文解密");
try {
//通知數(shù)據(jù)
Map<String, String> resourceMap = JSONObject.parseObject(resource, new TypeReference<Map<String, Object>>() {
});
//數(shù)據(jù)密文
String ciphertext = resourceMap.get("ciphertext");
//隨機(jī)串
String nonce = resourceMap.get("nonce");
//附加數(shù)據(jù)
String associatedData = resourceMap.get("associated_data");
log.info("密文: {}", ciphertext);
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
String resourceStr = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
log.info(msg+",回調(diào)通知,解密結(jié)果 : {}", resourceStr);
return JSONObject.parseObject(resourceStr, new TypeReference<Map<String, Object>>(){});
}catch (Exception e){
throw new RuntimeException("回調(diào)參數(shù),解密失?。?);
}
}
}
7、回調(diào)Body內(nèi)容處理
HttpUtils:
public class HttpUtils {
/**
* 將通知參數(shù)轉(zhuǎn)化為字符串
* @param request
* @return
*/
public static String readData(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
6、支付/退款回調(diào)通知
支付/退款回調(diào)通知:在進(jìn)行支付或退款操作后,支付平臺向商戶發(fā)送的異步通知,用于告知支付或退款的結(jié)果和相關(guān)信息。這種回調(diào)通知是實現(xiàn)支付系統(tǒng)與商戶系統(tǒng)之間數(shù)據(jù)同步和交互的重要方式。
支付/退款回調(diào)通知包含一些關(guān)鍵信息,如訂單號、交易金額、支付/退款狀態(tài)、支付平臺的交易流水號等。
在回調(diào)后,通過解析回調(diào)數(shù)據(jù),一定需要驗證簽名的合法性。
NotifyController:文章來源:http://www.zghlxwxcb.cn/news/detail-717500.html
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.lhz.demo.pay.WechatPayConfig;
import com.lhz.demo.pay.WechatPayValidator;
import com.lhz.demo.utils.HttpUtils;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author:
* @Description:
**/
@Api(tags = "回調(diào)接口(API3)")
@RestController
@Slf4j
public class NotifyController {
@Resource
private WechatPayConfig wechatPayConfig;
@Resource
private Verifier verifier;
private final ReentrantLock lock = new ReentrantLock();
@ApiOperation(value = "支付回調(diào)", notes = "支付回調(diào)")
@ApiOperationSupport(order = 5)
@PostMapping("/payNotify")
public Map<String, String> payNotify(HttpServletRequest request, HttpServletResponse response) {
log.info("支付回調(diào)");
// 處理通知參數(shù)
Map<String,Object> bodyMap = getNotifyBody(request);
if(bodyMap==null){
return falseMsg(response);
}
log.warn("=========== 在對業(yè)務(wù)數(shù)據(jù)進(jìn)行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進(jìn)行并發(fā)控制,以避免函數(shù)重入造成的數(shù)據(jù)混亂 ===========");
if(lock.tryLock()) {
try {
// 解密resource中的通知數(shù)據(jù)
String resource = bodyMap.get("resource").toString();
Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),1);
String orderNo = resourceMap.get("out_trade_no").toString();
String transactionId = resourceMap.get("transaction_id").toString();
// TODO 根據(jù)訂單號,做冪等處理,并且在對業(yè)務(wù)數(shù)據(jù)進(jìn)行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進(jìn)行并發(fā)控制,以避免函數(shù)重入造成的數(shù)據(jù)混亂
log.warn("=========== 根據(jù)訂單號,做冪等處理 ===========");
} finally {
//要主動釋放鎖
lock.unlock();
}
}
//成功應(yīng)答
return trueMsg(response);
}
@ApiOperation(value = "退款回調(diào)", notes = "退款回調(diào)")
@ApiOperationSupport(order = 5)
@PostMapping("/refundNotify")
public Map<String, String> refundNotify(HttpServletRequest request, HttpServletResponse response) {
log.info("退款回調(diào)");
// 處理通知參數(shù)
Map<String,Object> bodyMap = getNotifyBody(request);
if(bodyMap==null){
return falseMsg(response);
}
log.warn("=========== 在對業(yè)務(wù)數(shù)據(jù)進(jìn)行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進(jìn)行并發(fā)控制,以避免函數(shù)重入造成的數(shù)據(jù)混亂 ===========");
if(lock.tryLock()) {
try {
// 解密resource中的通知數(shù)據(jù)
String resource = bodyMap.get("resource").toString();
Map<String, Object> resourceMap = WechatPayValidator.decryptFromResource(resource, wechatPayConfig.getApiV3Key(),2);
String orderNo = resourceMap.get("out_trade_no").toString();
String transactionId = resourceMap.get("transaction_id").toString();
// TODO 根據(jù)訂單號,做冪等處理,并且在對業(yè)務(wù)數(shù)據(jù)進(jìn)行狀態(tài)檢查和處理之前,要采用數(shù)據(jù)鎖進(jìn)行并發(fā)控制,以避免函數(shù)重入造成的數(shù)據(jù)混亂
log.warn("=========== 根據(jù)訂單號,做冪等處理 ===========");
} finally {
//要主動釋放鎖
lock.unlock();
}
}
//成功應(yīng)答
return trueMsg(response);
}
private Map<String,Object> getNotifyBody(HttpServletRequest request){
//處理通知參數(shù)
String body = HttpUtils.readData(request);
log.info("退款回調(diào)參數(shù):{}",body);
// 轉(zhuǎn)換為Map
Map<String, Object> bodyMap = JSONObject.parseObject(body, new TypeReference<Map<String, Object>>(){});
// 微信的通知ID(通知的唯一ID)
String notifyId = bodyMap.get("id").toString();
// 驗證簽名信息
WechatPayValidator wechatPayValidator
= new WechatPayValidator(verifier, notifyId, body);
if(!wechatPayValidator.validate(request)){
log.error("通知驗簽失敗");
return null;
}
log.info("通知驗簽成功");
return bodyMap;
}
private Map<String, String> falseMsg(HttpServletResponse response){
Map<String, String> resMap = new HashMap<>(8);
//失敗應(yīng)答
response.setStatus(500);
resMap.put("code", "ERROR");
resMap.put("message", "通知驗簽失敗");
return resMap;
}
private Map<String, String> trueMsg(HttpServletResponse response){
Map<String, String> resMap = new HashMap<>(8);
//成功應(yīng)答
response.setStatus(200);
resMap.put("code", "SUCCESS");
resMap.put("message", "成功");
return resMap;
}
}
文章來源地址http://www.zghlxwxcb.cn/news/detail-717500.html
到了這里,關(guān)于微信支付APIV3統(tǒng)一回調(diào)接口封裝(H5、JSAPI、App、小程序)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!