paypal v2新增webhook回调校验方式

This commit is contained in:
egan
2023-09-12 22:34:15 +08:00
parent 89beb1d5b4
commit 17be83c20f
4 changed files with 238 additions and 40 deletions

View File

@@ -1,26 +0,0 @@
package com.egzosn.pay.paypal.v2.api;
import java.util.Map;
import com.egzosn.pay.common.bean.outbuilder.TextBuilder;
/**
* @author Egan
* <pre>
* email egzosn@gmail.com
* date 2021/1/17
* </pre>
*/
public class PayPalOutMessageBuilder extends TextBuilder {
public PayPalOutMessageBuilder(Map<String, Object> message) {
StringBuilder out = new StringBuilder();
for (Map.Entry<String, Object> entry : message.entrySet()) {
out.append(entry.getKey()).append('=').append(entry.getValue()).append("<br>");
}
super.content(out.toString());
}
}

View File

@@ -1,13 +1,16 @@
package com.egzosn.pay.paypal.v2.api;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -23,11 +26,11 @@ import com.alibaba.fastjson.JSONObject;
import com.egzosn.pay.common.api.BasePayService;
import com.egzosn.pay.common.bean.AssistOrder;
import com.egzosn.pay.common.bean.BillType;
import com.egzosn.pay.common.bean.CurType;
import com.egzosn.pay.common.bean.DefaultCurType;
import com.egzosn.pay.common.bean.MethodType;
import com.egzosn.pay.common.bean.NoticeParams;
import com.egzosn.pay.common.bean.NoticeRequest;
import com.egzosn.pay.common.bean.PayMessage;
import com.egzosn.pay.common.bean.PayOrder;
import com.egzosn.pay.common.bean.PayOutMessage;
@@ -38,10 +41,13 @@ import com.egzosn.pay.common.bean.result.PayException;
import com.egzosn.pay.common.exception.PayErrorException;
import com.egzosn.pay.common.http.HttpHeader;
import com.egzosn.pay.common.http.HttpStringEntity;
import com.egzosn.pay.common.http.ResponseEntity;
import com.egzosn.pay.common.http.UriVariables;
import com.egzosn.pay.common.util.IOUtils;
import com.egzosn.pay.common.util.Util;
import com.egzosn.pay.common.util.str.StringUtils;
import com.egzosn.pay.paypal.api.PayPalConfigStorage;
import com.egzosn.pay.paypal.v2.bean.Constants;
import com.egzosn.pay.paypal.v2.bean.PayPalRefundResult;
import com.egzosn.pay.paypal.v2.bean.PayPalTransactionType;
import com.egzosn.pay.paypal.v2.bean.order.ApplicationContext;
@@ -49,6 +55,7 @@ import com.egzosn.pay.paypal.v2.bean.order.Money;
import com.egzosn.pay.paypal.v2.bean.order.OrderRequest;
import com.egzosn.pay.paypal.v2.bean.order.PurchaseUnitRequest;
import com.egzosn.pay.paypal.v2.bean.order.ShippingDetail;
import com.egzosn.pay.paypal.v2.utils.PayPalUtil;
/**
@@ -61,6 +68,7 @@ import com.egzosn.pay.paypal.v2.bean.order.ShippingDetail;
*/
public class PayPalPayService extends BasePayService<PayPalConfigStorage> implements PayPalPayServiceInf {
/**
* 沙箱环境
*/
@@ -161,12 +169,18 @@ public class PayPalPayService extends BasePayService<PayPalConfigStorage> implem
@Deprecated
@Override
public boolean verify(Map<String, Object> params) {
return verify(new NoticeParams(params));
throw new PayErrorException(new PayException("failure", "payPal V2版本不支持此校验方式"));
}
@Override
public boolean verify(NoticeParams noticeParams) {
/**
* 保留IPN的校验方式
* @param noticeParams 参数
* @return 结果
*/
public boolean verifyIpn(NoticeParams noticeParams) {
final Map<String, Object> params = noticeParams.getBody();
Object paymentStatus = params.get("payment_status");
if (!"Completed".equals(paymentStatus)) {
@@ -177,6 +191,64 @@ public class PayPalPayService extends BasePayService<PayPalConfigStorage> implem
return "VERIFIED".equals(resp);
}
@Override
public boolean verify(NoticeParams noticeParams) {
final Map<String, List<String>> headers = noticeParams.getHeaders();
if (null == headers || headers.isEmpty()) {
throw new PayErrorException(new PayException("failure", "校验失败,请求头不能为空"));
}
String clientCertificateLocation = noticeParams.getHeader(Constants.PAYPAL_HEADER_CERT_URL);
ResponseEntity<InputStream> clientCertificateResponseEntity = requestTemplate.getForObjectEntity(clientCertificateLocation, InputStream.class);
if (clientCertificateResponseEntity.getStatusCode() > 400) {
LOG.error("获取证书信息失败无法进行webHook校验:{}", clientCertificateLocation);
return false;
}
InputStream inputStream = clientCertificateResponseEntity.getBody();
Collection<X509Certificate> clientCerts = PayPalUtil.getCertificateFromStream(inputStream);
Map<String, Object> body = noticeParams.getBody();
String webHookId = (String) body.get(Constants.ID);
String actualSignatureEncoded = noticeParams.getHeader(Constants.PAYPAL_HEADER_TRANSMISSION_SIG);
String authAlgo = noticeParams.getHeader(Constants.PAYPAL_HEADER_AUTH_ALGO);
String transmissionId = noticeParams.getHeader(Constants.PAYPAL_HEADER_TRANSMISSION_ID);
String transmissionTime = noticeParams.getHeader(Constants.PAYPAL_HEADER_TRANSMISSION_TIME);
String requestBody = noticeParams.getBodyStr();
String expectedSignature = String.format("%s|%s|%s|%s", transmissionId, transmissionTime, webHookId, PayPalUtil.crc32(requestBody));
boolean isDataValid = PayPalUtil.validateData(clientCerts, authAlgo, actualSignatureEncoded, expectedSignature);
LOG.debug("数据校验结果: {}", isDataValid);
return isDataValid;
}
/**
* 将请求参数或者请求流转化为 Map
*
* @param request 通知请求
* @return 获得回调的请求参数
*/
@Override
public NoticeParams getNoticeParams(NoticeRequest request) {
NoticeParams noticeParams = new NoticeParams();
try (InputStream is = request.getInputStream()) {
String body = IOUtils.toString(is);
noticeParams.setBodyStr(body);
noticeParams.setBody(JSON.parseObject(body));
}
catch (IOException e) {
throw new PayErrorException(new PayException("failure", "获取回调参数异常"), e);
}
Map<String, List<String>> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, Collections.list(request.getHeaders(name)));
}
noticeParams.setHeaders(headers);
return noticeParams;
}
/**
* 获取授权请求头
*
@@ -305,14 +377,14 @@ public class PayPalPayService extends BasePayService<PayPalConfigStorage> implem
@Override
public PayOutMessage getPayOutMessage(String code, String message) {
String out = "The response from IPN was: <b>" + code + "</b>";
return PayOutMessage.TEXT().content(out).build();
return PayOutMessage.TEXT().content(code).build();
}
@Override
public PayOutMessage successPayOutMessage(PayMessage payMessage) {
Map<String, Object> message = payMessage.getPayMessage();
return new PayPalOutMessageBuilder(message).build();
return PayOutMessage.TEXT().content("200").build();
}
@Override
@@ -367,16 +439,18 @@ public class PayPalPayService extends BasePayService<PayPalConfigStorage> implem
public Map<String, Object> close(String tradeNo, String outTradeNo) {
return null;
}
/**
* 交易关闭接口
*
* @param assistOrder 关闭订单
* @param assistOrder 关闭订单
* @return 返回支付方交易关闭后的结果
*/
@Override
public Map<String, Object> close(AssistOrder assistOrder){
public Map<String, Object> close(AssistOrder assistOrder) {
throw new UnsupportedOperationException("不支持该操作");
}
/**
* 注意:最好在付款成功之后回调时进行调用
* 确认订单并返回确认后订单信息
@@ -518,13 +592,11 @@ public class PayPalPayService extends BasePayService<PayPalConfigStorage> implem
JSONObject resp = getHttpRequestTemplate().getForObject(getReqUrl(PayPalTransactionType.REFUND_GET), authHeader(), JSONObject.class, refundOrder.getRefundNo());
return resp;
}
@Override
public Map<String, Object> downloadBill(Date billDate, BillType billType) {
return Collections.emptyMap();
}
}

View File

@@ -0,0 +1,59 @@
package com.egzosn.pay.paypal.v2.bean;
/**
* @author Egan
* @email egan@egzosn.com
* @date 2023/9/12
*/
public final class Constants {
private Constants() {
}
/**
* PayPal webhook transmission ID HTTP request header
*/
public static final String PAYPAL_HEADER_TRANSMISSION_ID = "PAYPAL-TRANSMISSION-ID";
/**
* PayPal webhook transmission time HTTP request header
*/
public static final String PAYPAL_HEADER_TRANSMISSION_TIME = "PAYPAL-TRANSMISSION-TIME";
/**
* PayPal webhook transmission signature HTTP request header
*/
public static final String PAYPAL_HEADER_TRANSMISSION_SIG = "PAYPAL-TRANSMISSION-SIG";
/**
* PayPal webhook certificate URL HTTP request header
*/
public static final String PAYPAL_HEADER_CERT_URL = "PAYPAL-CERT-URL";
/**
* PayPal webhook authentication algorithm HTTP request header
*/
public static final String PAYPAL_HEADER_AUTH_ALGO = "PAYPAL-AUTH-ALGO";
/**
* Trust Certificate Location to be used to validate webhook certificates
*/
public static final String PAYPAL_TRUST_CERT_URL = "webhook.trustCert";
/**
* Default Trust Certificate that comes packaged with SDK.
*/
public static final String PAYPAL_TRUST_DEFAULT_CERT = "DigiCertSHA2ExtendedValidationServerCA.crt";
/**
* Webhook Id to be set for validation purposes
*/
public static final String PAYPAL_WEBHOOK_ID = "webhook.id";
/**
* Webhook Id to be set for validation purposes
*/
public static final String PAYPAL_WEBHOOK_CERTIFICATE_AUTHTYPE = "webhook.authType";
public static final String ID = "id";
}

View File

@@ -0,0 +1,93 @@
package com.egzosn.pay.paypal.v2.utils;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.Signature;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.egzosn.pay.common.bean.result.PayException;
import com.egzosn.pay.common.exception.PayErrorException;
/**
* @author Egan
* @email egan@egzosn.com
* @date 2023/9/12
*/
public final class PayPalUtil {
private static final Logger LOG = LoggerFactory.getLogger(PayPalUtil.class);
private PayPalUtil() {
}
public static Collection<X509Certificate> getCertificateFromStream(InputStream stream) {
if (stream == null) {
throw new PayErrorException(new PayException("failure", "未找到证书"));
}
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (Collection<X509Certificate>) cf.generateCertificates(stream);
}
catch (CertificateException ex) {
throw new PayErrorException(new PayException("failure", "证书加载异常"), ex);
}
}
/**
* 生成字符串传递的CRC 32值
*
* @param data 字符
* @return 返回长crc32输入值。-1如果string为null
*/
public static long crc32(String data) {
if (data == null) {
return -1;
}
byte[] bytes = data.getBytes(Charset.forName("utf-8"));
Checksum checksum = new CRC32();
checksum.update(bytes, 0, bytes.length);
return checksum.getValue();
}
/**
* 基于https://developer.paypal.com/docs/integration/direct/rest-webhooks-overview/#event-signature验证Webhook签名验证如果签名有效则返回true
*
* @param clientCerts 客户端证书
* @param algo 服务器生成签名时使用的算法
* @param actualSignatureEncoded Paypal-Transmission-Sig服务器传递的报头值
* @param expectedSignature 用请求体的CRC32值格式化数据生成的签名
* @return true 校验通过
*/
public static boolean validateData(Collection<X509Certificate> clientCerts, String algo, String actualSignatureEncoded, String expectedSignature) {
// 从paypal-auth-algorithm HTTP头中获取signatureAlgorithm
Signature signatureAlgorithm = null;
try {
signatureAlgorithm = Signature.getInstance(algo);
//从HTTP头中提供的URL中获取certData并缓存它
X509Certificate[] clientChain = clientCerts.toArray(new X509Certificate[0]);
signatureAlgorithm.initVerify(clientChain[0].getPublicKey());
signatureAlgorithm.update(expectedSignature.getBytes());
// 实际的签名是base 64编码的可以在HTTP头中找到
byte[] actualSignature = Base64.decodeBase64(actualSignatureEncoded.getBytes());
return signatureAlgorithm.verify(actualSignature);
}
catch (GeneralSecurityException e) {
LOG.error("校验异常", e);
return false;
}
}
}