Skip to content

Commit 17be83c

Browse files
committed
paypal v2新增webhook回调校验方式
1 parent 89beb1d commit 17be83c

4 files changed

Lines changed: 238 additions & 40 deletions

File tree

pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalOutMessageBuilder.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

pay-java-paypal/src/main/java/com/egzosn/pay/paypal/v2/api/PayPalPayService.java

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.egzosn.pay.paypal.v2.api;
22

33

4+
import java.io.IOException;
45
import java.io.InputStream;
56
import java.io.UnsupportedEncodingException;
7+
import java.security.cert.X509Certificate;
68
import java.util.ArrayList;
9+
import java.util.Collection;
710
import java.util.Collections;
811
import java.util.Date;
12+
import java.util.Enumeration;
913
import java.util.HashMap;
10-
import java.util.LinkedHashMap;
1114
import java.util.List;
1215
import java.util.Map;
1316
import java.util.UUID;
@@ -23,11 +26,11 @@
2326
import com.egzosn.pay.common.api.BasePayService;
2427
import com.egzosn.pay.common.bean.AssistOrder;
2528
import com.egzosn.pay.common.bean.BillType;
26-
2729
import com.egzosn.pay.common.bean.CurType;
2830
import com.egzosn.pay.common.bean.DefaultCurType;
2931
import com.egzosn.pay.common.bean.MethodType;
3032
import com.egzosn.pay.common.bean.NoticeParams;
33+
import com.egzosn.pay.common.bean.NoticeRequest;
3134
import com.egzosn.pay.common.bean.PayMessage;
3235
import com.egzosn.pay.common.bean.PayOrder;
3336
import com.egzosn.pay.common.bean.PayOutMessage;
@@ -38,17 +41,21 @@
3841
import com.egzosn.pay.common.exception.PayErrorException;
3942
import com.egzosn.pay.common.http.HttpHeader;
4043
import com.egzosn.pay.common.http.HttpStringEntity;
44+
import com.egzosn.pay.common.http.ResponseEntity;
4145
import com.egzosn.pay.common.http.UriVariables;
46+
import com.egzosn.pay.common.util.IOUtils;
4247
import com.egzosn.pay.common.util.Util;
4348
import com.egzosn.pay.common.util.str.StringUtils;
4449
import com.egzosn.pay.paypal.api.PayPalConfigStorage;
50+
import com.egzosn.pay.paypal.v2.bean.Constants;
4551
import com.egzosn.pay.paypal.v2.bean.PayPalRefundResult;
4652
import com.egzosn.pay.paypal.v2.bean.PayPalTransactionType;
4753
import com.egzosn.pay.paypal.v2.bean.order.ApplicationContext;
4854
import com.egzosn.pay.paypal.v2.bean.order.Money;
4955
import com.egzosn.pay.paypal.v2.bean.order.OrderRequest;
5056
import com.egzosn.pay.paypal.v2.bean.order.PurchaseUnitRequest;
5157
import com.egzosn.pay.paypal.v2.bean.order.ShippingDetail;
58+
import com.egzosn.pay.paypal.v2.utils.PayPalUtil;
5259

5360

5461
/**
@@ -61,6 +68,7 @@
6168
*/
6269
public class PayPalPayService extends BasePayService<PayPalConfigStorage> implements PayPalPayServiceInf {
6370

71+
6472
/**
6573
* 沙箱环境
6674
*/
@@ -161,12 +169,18 @@ public String getAccessToken(boolean forceRefresh) throws PayErrorException {
161169
@Deprecated
162170
@Override
163171
public boolean verify(Map<String, Object> params) {
164-
return verify(new NoticeParams(params));
172+
173+
throw new PayErrorException(new PayException("failure", "payPal V2版本不支持此校验方式"));
165174

166175
}
167176

168-
@Override
169-
public boolean verify(NoticeParams noticeParams) {
177+
178+
/**
179+
* 保留IPN的校验方式
180+
* @param noticeParams 参数
181+
* @return 结果
182+
*/
183+
public boolean verifyIpn(NoticeParams noticeParams) {
170184
final Map<String, Object> params = noticeParams.getBody();
171185
Object paymentStatus = params.get("payment_status");
172186
if (!"Completed".equals(paymentStatus)) {
@@ -177,6 +191,64 @@ public boolean verify(NoticeParams noticeParams) {
177191
return "VERIFIED".equals(resp);
178192

179193
}
194+
@Override
195+
public boolean verify(NoticeParams noticeParams) {
196+
197+
final Map<String, List<String>> headers = noticeParams.getHeaders();
198+
if (null == headers || headers.isEmpty()) {
199+
throw new PayErrorException(new PayException("failure", "校验失败,请求头不能为空"));
200+
}
201+
202+
203+
String clientCertificateLocation = noticeParams.getHeader(Constants.PAYPAL_HEADER_CERT_URL);
204+
ResponseEntity<InputStream> clientCertificateResponseEntity = requestTemplate.getForObjectEntity(clientCertificateLocation, InputStream.class);
205+
if (clientCertificateResponseEntity.getStatusCode() > 400) {
206+
LOG.error("获取证书信息失败,无法进行webHook校验:{}", clientCertificateLocation);
207+
return false;
208+
}
209+
InputStream inputStream = clientCertificateResponseEntity.getBody();
210+
Collection<X509Certificate> clientCerts = PayPalUtil.getCertificateFromStream(inputStream);
211+
Map<String, Object> body = noticeParams.getBody();
212+
String webHookId = (String) body.get(Constants.ID);
213+
String actualSignatureEncoded = noticeParams.getHeader(Constants.PAYPAL_HEADER_TRANSMISSION_SIG);
214+
String authAlgo = noticeParams.getHeader(Constants.PAYPAL_HEADER_AUTH_ALGO);
215+
String transmissionId = noticeParams.getHeader(Constants.PAYPAL_HEADER_TRANSMISSION_ID);
216+
String transmissionTime = noticeParams.getHeader(Constants.PAYPAL_HEADER_TRANSMISSION_TIME);
217+
String requestBody = noticeParams.getBodyStr();
218+
String expectedSignature = String.format("%s|%s|%s|%s", transmissionId, transmissionTime, webHookId, PayPalUtil.crc32(requestBody));
219+
boolean isDataValid = PayPalUtil.validateData(clientCerts, authAlgo, actualSignatureEncoded, expectedSignature);
220+
LOG.debug("数据校验结果: {}", isDataValid);
221+
return isDataValid;
222+
223+
}
224+
225+
/**
226+
* 将请求参数或者请求流转化为 Map
227+
*
228+
* @param request 通知请求
229+
* @return 获得回调的请求参数
230+
*/
231+
@Override
232+
public NoticeParams getNoticeParams(NoticeRequest request) {
233+
NoticeParams noticeParams = new NoticeParams();
234+
try (InputStream is = request.getInputStream()) {
235+
String body = IOUtils.toString(is);
236+
noticeParams.setBodyStr(body);
237+
noticeParams.setBody(JSON.parseObject(body));
238+
}
239+
catch (IOException e) {
240+
throw new PayErrorException(new PayException("failure", "获取回调参数异常"), e);
241+
}
242+
Map<String, List<String>> headers = new HashMap<>();
243+
Enumeration<String> headerNames = request.getHeaderNames();
244+
while (headerNames.hasMoreElements()) {
245+
String name = headerNames.nextElement();
246+
headers.put(name, Collections.list(request.getHeaders(name)));
247+
}
248+
noticeParams.setHeaders(headers);
249+
return noticeParams;
250+
}
251+
180252
/**
181253
* 获取授权请求头
182254
*
@@ -305,14 +377,14 @@ public Map<String, Object> orderInfo(PayOrder order) {
305377

306378
@Override
307379
public PayOutMessage getPayOutMessage(String code, String message) {
308-
String out = "The response from IPN was: <b>" + code + "</b>";
309-
return PayOutMessage.TEXT().content(out).build();
380+
381+
return PayOutMessage.TEXT().content(code).build();
310382
}
311383

312384
@Override
313385
public PayOutMessage successPayOutMessage(PayMessage payMessage) {
314-
Map<String, Object> message = payMessage.getPayMessage();
315-
return new PayPalOutMessageBuilder(message).build();
386+
387+
return PayOutMessage.TEXT().content("200").build();
316388
}
317389

318390
@Override
@@ -367,16 +439,18 @@ public Map<String, Object> query(AssistOrder assistOrder) {
367439
public Map<String, Object> close(String tradeNo, String outTradeNo) {
368440
return null;
369441
}
442+
370443
/**
371444
* 交易关闭接口
372445
*
373-
* @param assistOrder 关闭订单
446+
* @param assistOrder 关闭订单
374447
* @return 返回支付方交易关闭后的结果
375448
*/
376449
@Override
377-
public Map<String, Object> close(AssistOrder assistOrder){
450+
public Map<String, Object> close(AssistOrder assistOrder) {
378451
throw new UnsupportedOperationException("不支持该操作");
379452
}
453+
380454
/**
381455
* 注意:最好在付款成功之后回调时进行调用
382456
* 确认订单并返回确认后订单信息
@@ -518,13 +592,11 @@ public Map<String, Object> refundquery(RefundOrder refundOrder) {
518592
JSONObject resp = getHttpRequestTemplate().getForObject(getReqUrl(PayPalTransactionType.REFUND_GET), authHeader(), JSONObject.class, refundOrder.getRefundNo());
519593
return resp;
520594
}
595+
521596
@Override
522597
public Map<String, Object> downloadBill(Date billDate, BillType billType) {
523598
return Collections.emptyMap();
524599
}
525600

526601

527-
528-
529-
530602
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.egzosn.pay.paypal.v2.bean;
2+
3+
/**
4+
* @author Egan
5+
* @email egan@egzosn.com
6+
* @date 2023/9/12
7+
*/
8+
public final class Constants {
9+
private Constants() {
10+
}
11+
12+
/**
13+
* PayPal webhook transmission ID HTTP request header
14+
*/
15+
public static final String PAYPAL_HEADER_TRANSMISSION_ID = "PAYPAL-TRANSMISSION-ID";
16+
17+
/**
18+
* PayPal webhook transmission time HTTP request header
19+
*/
20+
public static final String PAYPAL_HEADER_TRANSMISSION_TIME = "PAYPAL-TRANSMISSION-TIME";
21+
22+
/**
23+
* PayPal webhook transmission signature HTTP request header
24+
*/
25+
public static final String PAYPAL_HEADER_TRANSMISSION_SIG = "PAYPAL-TRANSMISSION-SIG";
26+
/**
27+
* PayPal webhook certificate URL HTTP request header
28+
*/
29+
public static final String PAYPAL_HEADER_CERT_URL = "PAYPAL-CERT-URL";
30+
31+
/**
32+
* PayPal webhook authentication algorithm HTTP request header
33+
*/
34+
public static final String PAYPAL_HEADER_AUTH_ALGO = "PAYPAL-AUTH-ALGO";
35+
36+
/**
37+
* Trust Certificate Location to be used to validate webhook certificates
38+
*/
39+
public static final String PAYPAL_TRUST_CERT_URL = "webhook.trustCert";
40+
41+
42+
/**
43+
* Default Trust Certificate that comes packaged with SDK.
44+
*/
45+
public static final String PAYPAL_TRUST_DEFAULT_CERT = "DigiCertSHA2ExtendedValidationServerCA.crt";
46+
47+
/**
48+
* Webhook Id to be set for validation purposes
49+
*/
50+
public static final String PAYPAL_WEBHOOK_ID = "webhook.id";
51+
52+
/**
53+
* Webhook Id to be set for validation purposes
54+
*/
55+
public static final String PAYPAL_WEBHOOK_CERTIFICATE_AUTHTYPE = "webhook.authType";
56+
public static final String ID = "id";
57+
58+
59+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.egzosn.pay.paypal.v2.utils;
2+
3+
import java.io.InputStream;
4+
import java.nio.charset.Charset;
5+
import java.security.GeneralSecurityException;
6+
import java.security.Signature;
7+
import java.security.cert.CertificateException;
8+
import java.security.cert.CertificateFactory;
9+
import java.security.cert.X509Certificate;
10+
import java.util.Collection;
11+
import java.util.zip.CRC32;
12+
import java.util.zip.Checksum;
13+
14+
import org.apache.commons.codec.binary.Base64;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
import com.egzosn.pay.common.bean.result.PayException;
19+
import com.egzosn.pay.common.exception.PayErrorException;
20+
21+
/**
22+
* @author Egan
23+
* @email egan@egzosn.com
24+
* @date 2023/9/12
25+
*/
26+
public final class PayPalUtil {
27+
private static final Logger LOG = LoggerFactory.getLogger(PayPalUtil.class);
28+
29+
private PayPalUtil() {
30+
}
31+
32+
public static Collection<X509Certificate> getCertificateFromStream(InputStream stream) {
33+
if (stream == null) {
34+
throw new PayErrorException(new PayException("failure", "未找到证书"));
35+
}
36+
try {
37+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
38+
39+
return (Collection<X509Certificate>) cf.generateCertificates(stream);
40+
}
41+
catch (CertificateException ex) {
42+
throw new PayErrorException(new PayException("failure", "证书加载异常"), ex);
43+
}
44+
45+
}
46+
47+
/**
48+
* 生成字符串传递的CRC 32值
49+
*
50+
* @param data 字符
51+
* @return 返回长crc32输入值。-1如果string为null
52+
*/
53+
public static long crc32(String data) {
54+
if (data == null) {
55+
return -1;
56+
}
57+
byte[] bytes = data.getBytes(Charset.forName("utf-8"));
58+
Checksum checksum = new CRC32();
59+
checksum.update(bytes, 0, bytes.length);
60+
return checksum.getValue();
61+
62+
}
63+
64+
65+
/**
66+
* 基于https://developer.paypal.com/docs/integration/direct/rest-webhooks-overview/#event-signature验证Webhook签名验证,如果签名有效则返回true
67+
*
68+
* @param clientCerts 客户端证书
69+
* @param algo 服务器生成签名时使用的算法
70+
* @param actualSignatureEncoded Paypal-Transmission-Sig服务器传递的报头值
71+
* @param expectedSignature 用请求体的CRC32值格式化数据生成的签名
72+
* @return true 校验通过
73+
*/
74+
public static boolean validateData(Collection<X509Certificate> clientCerts, String algo, String actualSignatureEncoded, String expectedSignature) {
75+
// 从paypal-auth-algorithm HTTP头中获取signatureAlgorithm
76+
Signature signatureAlgorithm = null;
77+
try {
78+
signatureAlgorithm = Signature.getInstance(algo);
79+
//从HTTP头中提供的URL中获取certData并缓存它
80+
X509Certificate[] clientChain = clientCerts.toArray(new X509Certificate[0]);
81+
signatureAlgorithm.initVerify(clientChain[0].getPublicKey());
82+
signatureAlgorithm.update(expectedSignature.getBytes());
83+
// 实际的签名是base 64编码的,可以在HTTP头中找到
84+
byte[] actualSignature = Base64.decodeBase64(actualSignatureEncoded.getBytes());
85+
return signatureAlgorithm.verify(actualSignature);
86+
}
87+
catch (GeneralSecurityException e) {
88+
LOG.error("校验异常", e);
89+
return false;
90+
}
91+
92+
}
93+
}

0 commit comments

Comments
 (0)