diff --git a/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalOutMessageBuilder.java b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalOutMessageBuilder.java
deleted file mode 100644
index 71e19c4..0000000
--- a/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalOutMessageBuilder.java
+++ /dev/null
@@ -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
- *
- * email egzosn@gmail.com
- * date 2021/1/17
- *
- */
-public class PayPalOutMessageBuilder extends TextBuilder {
-
-
- public PayPalOutMessageBuilder(Map message) {
- StringBuilder out = new StringBuilder();
- for (Map.Entry entry : message.entrySet()) {
- out.append(entry.getKey()).append('=').append(entry.getValue()).append("
");
- }
- super.content(out.toString());
- }
-
-
-}
diff --git a/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalPayService.java b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalPayService.java
index 5eb631d..0dae83f 100644
--- a/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalPayService.java
+++ b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalPayService.java
@@ -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 implements PayPalPayServiceInf {
+
/**
* 沙箱环境
*/
@@ -161,12 +169,18 @@ public class PayPalPayService extends BasePayService implem
@Deprecated
@Override
public boolean verify(Map 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 params = noticeParams.getBody();
Object paymentStatus = params.get("payment_status");
if (!"Completed".equals(paymentStatus)) {
@@ -177,6 +191,64 @@ public class PayPalPayService extends BasePayService implem
return "VERIFIED".equals(resp);
}
+ @Override
+ public boolean verify(NoticeParams noticeParams) {
+
+ final Map> headers = noticeParams.getHeaders();
+ if (null == headers || headers.isEmpty()) {
+ throw new PayErrorException(new PayException("failure", "校验失败,请求头不能为空"));
+ }
+
+
+ String clientCertificateLocation = noticeParams.getHeader(Constants.PAYPAL_HEADER_CERT_URL);
+ ResponseEntity clientCertificateResponseEntity = requestTemplate.getForObjectEntity(clientCertificateLocation, InputStream.class);
+ if (clientCertificateResponseEntity.getStatusCode() > 400) {
+ LOG.error("获取证书信息失败,无法进行webHook校验:{}", clientCertificateLocation);
+ return false;
+ }
+ InputStream inputStream = clientCertificateResponseEntity.getBody();
+ Collection clientCerts = PayPalUtil.getCertificateFromStream(inputStream);
+ Map 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> headers = new HashMap<>();
+ Enumeration 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 implem
@Override
public PayOutMessage getPayOutMessage(String code, String message) {
- String out = "The response from IPN was: " + code + "";
- return PayOutMessage.TEXT().content(out).build();
+
+ return PayOutMessage.TEXT().content(code).build();
}
@Override
public PayOutMessage successPayOutMessage(PayMessage payMessage) {
- Map message = payMessage.getPayMessage();
- return new PayPalOutMessageBuilder(message).build();
+
+ return PayOutMessage.TEXT().content("200").build();
}
@Override
@@ -367,16 +439,18 @@ public class PayPalPayService extends BasePayService implem
public Map close(String tradeNo, String outTradeNo) {
return null;
}
+
/**
* 交易关闭接口
*
- * @param assistOrder 关闭订单
+ * @param assistOrder 关闭订单
* @return 返回支付方交易关闭后的结果
*/
@Override
- public Map close(AssistOrder assistOrder){
+ public Map close(AssistOrder assistOrder) {
throw new UnsupportedOperationException("不支持该操作");
}
+
/**
* 注意:最好在付款成功之后回调时进行调用
* 确认订单并返回确认后订单信息
@@ -518,13 +592,11 @@ public class PayPalPayService extends BasePayService implem
JSONObject resp = getHttpRequestTemplate().getForObject(getReqUrl(PayPalTransactionType.REFUND_GET), authHeader(), JSONObject.class, refundOrder.getRefundNo());
return resp;
}
+
@Override
public Map downloadBill(Date billDate, BillType billType) {
return Collections.emptyMap();
}
-
-
-
}
diff --git a/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/bean/Constants.java b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/bean/Constants.java
new file mode 100644
index 0000000..f0819fa
--- /dev/null
+++ b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/bean/Constants.java
@@ -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";
+
+
+}
diff --git a/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/utils/PayPalUtil.java b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/utils/PayPalUtil.java
new file mode 100644
index 0000000..aaec461
--- /dev/null
+++ b/pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/utils/PayPalUtil.java
@@ -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 getCertificateFromStream(InputStream stream) {
+ if (stream == null) {
+ throw new PayErrorException(new PayException("failure", "未找到证书"));
+ }
+ try {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+ return (Collection) 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 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;
+ }
+
+ }
+}