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; + } + + } +}