Skip to content

Commit 9177e33

Browse files
wujunjiesdxy-peng
andauthored
回调和回包验签支持平台公钥 (#272)
* 回调和回包验签支持使用平台公钥 --------- Co-authored-by: Xiaoyu PENG <peng.xiaoyu@gmail.com>
1 parent 36e70ff commit 9177e33

21 files changed

Lines changed: 827 additions & 164 deletions

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,23 @@ Config config =
233233
.build();
234234
```
235235

236+
## 使用本地平台公钥
237+
238+
如果你的商户可使用微信支付的公钥验证应答和回调的签名,可使用微信支付公钥和公钥ID初始化。
239+
240+
```java
241+
// 可以根据实际情况使用publicKeyFromPath或publicKey加载公钥
242+
Config config =
243+
new RSAPublicKeyConfig.Builder()
244+
.merchantId(merchantId)
245+
.privateKeyFromPath(privateKeyPath)
246+
.publicKeyFromPath(publicKeyPath)
247+
.publicKeyId(publicKeyId)
248+
.merchantSerialNumber(merchantSerialNumber)
249+
.apiV3Key(apiV3Key)
250+
.build();
251+
```
252+
236253
## 回调通知
237254

238255
首先,你需要在你的服务器上创建一个公开的 HTTP 端点,接受来自微信支付的回调通知。

core/src/main/java/com/wechat/pay/java/core/AbstractRSAConfig.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
import com.wechat.pay.java.core.cipher.Signer;
1515
import com.wechat.pay.java.core.util.PemUtil;
1616
import java.security.PrivateKey;
17+
import java.security.PublicKey;
1718
import java.security.cert.X509Certificate;
1819

1920
/** RSAConfig抽象类 */
2021
public abstract class AbstractRSAConfig implements Config {
2122

23+
/** 使用微信支付平台证书验签 */
2224
protected AbstractRSAConfig(
2325
String merchantId,
2426
PrivateKey privateKey,
@@ -28,6 +30,23 @@ protected AbstractRSAConfig(
2830
this.privateKey = privateKey;
2931
this.merchantSerialNumber = merchantSerialNumber;
3032
this.certificateProvider = certificateProvider;
33+
this.publicKey = null;
34+
this.publicKeyId = null;
35+
}
36+
37+
/** 使用微信支付公钥验签 */
38+
protected AbstractRSAConfig(
39+
String merchantId,
40+
PrivateKey privateKey,
41+
String merchantSerialNumber,
42+
PublicKey publicKey,
43+
String publicKeyId) {
44+
this.merchantId = merchantId;
45+
this.privateKey = privateKey;
46+
this.merchantSerialNumber = merchantSerialNumber;
47+
this.certificateProvider = null;
48+
this.publicKey = publicKey;
49+
this.publicKeyId = publicKeyId;
3150
}
3251

3352
/** 商户号 */
@@ -42,8 +61,17 @@ protected AbstractRSAConfig(
4261
/** 微信支付平台证书Provider */
4362
private final CertificateProvider certificateProvider;
4463

64+
/** 微信支付平台公钥 */
65+
private final PublicKey publicKey;
66+
67+
/** 微信支付平台公钥Id */
68+
private final String publicKeyId;
69+
4570
@Override
4671
public PrivacyEncryptor createEncryptor() {
72+
if (publicKey != null) {
73+
return new RSAPrivacyEncryptor(publicKey, publicKeyId);
74+
}
4775
X509Certificate certificate = certificateProvider.getAvailableCertificate();
4876
return new RSAPrivacyEncryptor(
4977
certificate.getPublicKey(), PemUtil.getSerialNumber(certificate));
@@ -61,6 +89,9 @@ public Credential createCredential() {
6189

6290
@Override
6391
public Validator createValidator() {
92+
if (publicKey != null) {
93+
return new WechatPay2Validator(new RSAVerifier(publicKey, publicKeyId));
94+
}
6495
return new WechatPay2Validator(new RSAVerifier(certificateProvider));
6596
}
6697

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.wechat.pay.java.core;
2+
3+
import static com.wechat.pay.java.core.notification.Constant.AES_CIPHER_ALGORITHM;
4+
import static com.wechat.pay.java.core.notification.Constant.RSA_SIGN_TYPE;
5+
import static java.util.Objects.requireNonNull;
6+
7+
import com.wechat.pay.java.core.cipher.AeadAesCipher;
8+
import com.wechat.pay.java.core.cipher.AeadCipher;
9+
import com.wechat.pay.java.core.cipher.RSAVerifier;
10+
import com.wechat.pay.java.core.cipher.Verifier;
11+
import com.wechat.pay.java.core.notification.NotificationConfig;
12+
import com.wechat.pay.java.core.util.PemUtil;
13+
import java.nio.charset.StandardCharsets;
14+
import java.security.PublicKey;
15+
16+
/** 使用微信支付平台公钥的RSA配置类。 每次构造都要求传入平台公钥以及平台公钥id,如果使用平台证书建议用RSAAutoCertificateConfig类 */
17+
public final class RSAPublicKeyConfig extends AbstractRSAConfig implements NotificationConfig {
18+
19+
private final PublicKey publicKey;
20+
private final AeadCipher aeadCipher;
21+
private final String publicKeyId;
22+
23+
private RSAPublicKeyConfig(Builder builder) {
24+
super(
25+
builder.merchantId,
26+
builder.privateKey,
27+
builder.merchantSerialNumber,
28+
builder.publicKey,
29+
builder.publicKeyId);
30+
this.publicKey = builder.publicKey;
31+
this.publicKeyId = builder.publicKeyId;
32+
this.aeadCipher = new AeadAesCipher(builder.apiV3Key);
33+
}
34+
35+
/**
36+
* 获取签名类型
37+
*
38+
* @return 签名类型
39+
*/
40+
@Override
41+
public String getSignType() {
42+
return RSA_SIGN_TYPE;
43+
}
44+
45+
/**
46+
* 获取认证加解密器类型
47+
*
48+
* @return 认证加解密器类型
49+
*/
50+
@Override
51+
public String getCipherType() {
52+
return AES_CIPHER_ALGORITHM;
53+
}
54+
55+
/**
56+
* 创建验签器
57+
*
58+
* @return 验签器
59+
*/
60+
@Override
61+
public Verifier createVerifier() {
62+
return new RSAVerifier(publicKey, publicKeyId);
63+
}
64+
65+
/**
66+
* 创建认证加解密器
67+
*
68+
* @return 认证加解密器
69+
*/
70+
@Override
71+
public AeadCipher createAeadCipher() {
72+
return aeadCipher;
73+
}
74+
75+
public static class Builder extends AbstractRSAConfigBuilder<Builder> {
76+
protected byte[] apiV3Key;
77+
protected PublicKey publicKey;
78+
protected String publicKeyId;
79+
80+
public Builder apiV3Key(String apiV3Key) {
81+
this.apiV3Key = apiV3Key.getBytes(StandardCharsets.UTF_8);
82+
return self();
83+
}
84+
85+
public Builder publicKey(String publicKey) {
86+
this.publicKey = PemUtil.loadPublicKeyFromString(publicKey);
87+
return self();
88+
}
89+
90+
public Builder publicKey(PublicKey publicKey) {
91+
this.publicKey = publicKey;
92+
return self();
93+
}
94+
95+
public Builder publicKeyFromPath(String publicKeyPath) {
96+
this.publicKey = PemUtil.loadPublicKeyFromPath(publicKeyPath);
97+
return self();
98+
}
99+
100+
public Builder publicKeyId(String publicKeyId) {
101+
this.publicKeyId = publicKeyId;
102+
return self();
103+
}
104+
105+
@Override
106+
protected Builder self() {
107+
return this;
108+
}
109+
110+
public RSAPublicKeyConfig build() {
111+
requireNonNull(merchantId);
112+
requireNonNull(publicKey);
113+
requireNonNull(publicKeyId);
114+
requireNonNull(privateKey);
115+
requireNonNull(merchantSerialNumber);
116+
117+
return new RSAPublicKeyConfig(this);
118+
}
119+
}
120+
}

core/src/main/java/com/wechat/pay/java/core/certificate/CertificateDownloader.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
import com.wechat.pay.java.core.certificate.model.DownloadCertificateResponse;
77
import com.wechat.pay.java.core.certificate.model.EncryptCertificate;
88
import com.wechat.pay.java.core.cipher.AeadCipher;
9+
import com.wechat.pay.java.core.exception.ServiceException;
910
import com.wechat.pay.java.core.http.Constant;
1011
import com.wechat.pay.java.core.http.HttpClient;
1112
import com.wechat.pay.java.core.http.HttpMethod;
1213
import com.wechat.pay.java.core.http.HttpRequest;
1314
import com.wechat.pay.java.core.http.HttpResponse;
1415
import com.wechat.pay.java.core.http.MediaType;
15-
import com.wechat.pay.java.core.util.PemUtil;
1616
import java.nio.charset.StandardCharsets;
1717
import java.security.cert.X509Certificate;
1818
import java.util.Base64;
@@ -77,16 +77,17 @@ public Map<String, X509Certificate> download() {
7777
.addHeader(Constant.ACCEPT, " */*")
7878
.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue())
7979
.build();
80-
HttpResponse<DownloadCertificateResponse> httpResponse =
81-
httpClient.execute(httpRequest, DownloadCertificateResponse.class);
82-
83-
Map<String, X509Certificate> downloaded = decryptCertificate(httpResponse);
84-
validateCertificate(downloaded);
85-
return downloaded;
86-
}
87-
88-
private void validateCertificate(Map<String, X509Certificate> certificates) {
89-
certificates.forEach((serialNo, cert) -> certificateHandler.validateCertPath(cert));
80+
try {
81+
HttpResponse<DownloadCertificateResponse> httpResponse =
82+
httpClient.execute(httpRequest, DownloadCertificateResponse.class);
83+
return decryptCertificate(httpResponse);
84+
} catch (ServiceException e) {
85+
// 如果证书不存在,可能是切换为平台公钥,该处不报错
86+
if (e.getErrorCode().equals("NOT_FOUND")) {
87+
return new HashMap<>();
88+
}
89+
throw e;
90+
}
9091
}
9192

9293
/**
@@ -109,7 +110,7 @@ private Map<String, X509Certificate> decryptCertificate(
109110
Base64.getDecoder().decode(encryptCertificate.getCiphertext()));
110111

111112
certificate = certificateHandler.generateCertificate(decryptCertificate);
112-
downloadCertMap.put(PemUtil.getSerialNumber(certificate), certificate);
113+
downloadCertMap.put(data.getSerialNo(), certificate);
113114
}
114115
return downloadCertMap;
115116
}

core/src/main/java/com/wechat/pay/java/core/certificate/CertificateHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public interface CertificateHandler {
1414
X509Certificate generateCertificate(String certificate);
1515

1616
/**
17-
* * 验证证书链
17+
* * 验证证书链(不推荐验证,如果证书过期不及时更换会导致验证失败,从而影响业务)
1818
*
1919
* @param certificate 微信支付平台证书
2020
* @throws com.wechat.pay.java.core.exception.ValidationException 证书验证失败
Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,17 @@
11
package com.wechat.pay.java.core.certificate;
22

3-
import com.wechat.pay.java.core.exception.ValidationException;
43
import com.wechat.pay.java.core.util.PemUtil;
54
import java.security.cert.*;
6-
import java.util.*;
75

86
final class RSACertificateHandler implements CertificateHandler {
97

10-
private static final X509Certificate tenpayCACert =
11-
PemUtil.loadX509FromString(
12-
"-----BEGIN CERTIFICATE-----\n"
13-
+ "MIIEcDCCA1igAwIBAgIUG9QiDlDbwEsGrTl1SYRsAcPo69IwDQYJKoZIhvcNAQEL\n"
14-
+ "BQAwcDELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM\n"
15-
+ "E0NoaW5hIFRydXN0IE5ldHdvcmsxLjAsBgNVBAMMJWlUcnVzQ2hpbmEgQ2xhc3Mg\n"
16-
+ "MiBFbnRlcnByaXNlIENBIC0gRzMwHhcNMTcwODA5MDkxNTU1WhcNMzIwODA5MDkx\n"
17-
+ "NTU1WjBeMQswCQYDVQQGEwJDTjETMBEGA1UEChMKVGVucGF5LmNvbTEdMBsGA1UE\n"
18-
+ "CxMUVGVucGF5LmNvbSBDQSBDZW50ZXIxGzAZBgNVBAMTElRlbnBheS5jb20gUm9v\n"
19-
+ "dCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALvnPD6k39BdPYAH\n"
20-
+ "+6lnWPjuHH+2pcmZUf2E8cNFQFNr+ECRZylYV2iKyItCQt3I2/7VIDZl6aR9TE7n\n"
21-
+ "sZrtSmOXCw635QOrq2yF9LTSDotAhf3ER0+216w3age/VzGcNVQpTf6gRCHCuQIk\n"
22-
+ "8pe/oh06JagGvX0wERa+I6NfuG58ZHQY9d6RqLXKQl0Up95v73HDsG487z8k6jcn\n"
23-
+ "qpGngmHQxdWiWRJugqxNRUD+awv2/DUsqGOffPX4jzJ6rLSJSlQXvuniDYxmaiaD\n"
24-
+ "cK0bUbB5aM+1zMwogoHSYxWj/6B+vgcnHQCUrwGdiQR5+F+yRWzy5bO09IzaFgeO\n"
25-
+ "PNPLPOsCAwEAAaOCARIwggEOMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/\n"
26-
+ "BAQDAgEGMCAGA1UdEQQZMBekFTATMREwDwYDVQQDDAhzd2JlLTI2NjAdBgNVHQ4E\n"
27-
+ "FgQUTFo4GLdm9oHX52HcWnzuL4tui2gwHwYDVR0jBBgwFoAUK1vVxWgI69vN5LA5\n"
28-
+ "MqJf/8dPmEUwRgYDVR0gBD8wPTA7BgoqgRyG7xcBAQECMC0wKwYIKwYBBQUHAgEW\n"
29-
+ "H2h0dHBzOi8vd3d3Lml0cnVzLmNvbS5jbi9jdG5jcHMwPgYDVR0fBDcwNTAzoDGg\n"
30-
+ "L4YtaHR0cDovL3RvcGNhLml0cnVzLmNvbS5jbi9jcmwvaXRydXNjMmNhZzMuY3Js\n"
31-
+ "MA0GCSqGSIb3DQEBCwUAA4IBAQBwZhL/eiOQmMyo1D0IR9mu1DPWl5J3XXhjc4R6\n"
32-
+ "mFgsN/FCeVP9M4U9y2FJH6i5Ha5YCecKGw5pwhA0rjZr/6okWwo22GF+nzI/gQiz\n"
33-
+ "6ugAKs5VjFbeiEb04Ncz4HT8FP1idK3tyCjqCUTkLNt0U3tR7wy26hgOqlT2wCZ9\n"
34-
+ "X4MfT8dUMdt9nCZx4ujN5yZOzaLOCHmzoGDGxgKg91bbu0TG2Yzd2ylhrxxRtFH9\n"
35-
+ "aZ/J1x5UoF7uwhTM8P92DuAldWC1/bX1kciOtQvQEZeAy+9y/1BtFxoBnmDxnqkX\n"
36-
+ "+lirIUYTLDaL7HaLrOLECUlaxZCU/Nkwm3tmqQxtCh+XQBdd\n"
37-
+ "-----END CERTIFICATE-----");
38-
39-
private static final Set<TrustAnchor> trustAnchor =
40-
new LinkedHashSet<>(Collections.singletonList(new TrustAnchor(tenpayCACert, null)));
41-
428
@Override
439
public X509Certificate generateCertificate(String certificate) {
4410
return PemUtil.loadX509FromString(certificate);
4511
}
4612

4713
@Override
4814
public void validateCertPath(X509Certificate certificate) {
49-
try {
50-
PKIXParameters params = new PKIXParameters(trustAnchor);
51-
params.setRevocationEnabled(false);
52-
53-
List<X509Certificate> certs = new ArrayList<>();
54-
certs.add(certificate);
55-
56-
CertificateFactory cf = CertificateFactory.getInstance("X.509");
57-
CertPath certPath = cf.generateCertPath(certs);
58-
59-
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
60-
validator.validate(certPath, params);
61-
} catch (Exception e) {
62-
throw new ValidationException(
63-
String.format(
64-
"certificate[%s] validation failed: %s",
65-
PemUtil.getSerialNumber(certificate), e.getMessage()),
66-
e);
67-
}
15+
// 为防止证书过期导致验签失败,从而影响业务,后续不再验证证书信任链
6816
}
6917
}

0 commit comments

Comments
 (0)