Skip to content

Commit 49c5db5

Browse files
authored
feat: 使用证书信任链验证通过API下载的微信支付平台证书 (#138)
1 parent 9a76cb0 commit 49c5db5

14 files changed

+672
-201
lines changed

.code.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ source:
33
# 提供产品代码库中编写的测试代码存放目录或文件名格式,以便后续代码统计环节进行排除等特殊处理
44
test_source:
55
#用于匹配文件; 匹配方式为正则表达式
6-
filepath_regex: [ ".*Test.java" ]
6+
filepath_regex: [ ".*/src/test/.*" ]
77

88
# 提供产品代码库中工具或框架自动生成的且在代码库中的代码,没有可为空。以便后续代码统计环节进行排除等特殊处理。
99
auto_generate_source:

core/build.gradle

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ dependencies {
2222

2323
testImplementation "junit:junit:${junitVersion}"
2424
testImplementation "org.mockito:mockito-inline:${mockitoInlineVersion}"
25+
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
26+
testImplementation "org.awaitility:awaitility:${awaitilityVersion}"
2527

2628
testImplementation platform("org.junit:junit-bom:${junit5Version}")
2729
testImplementation 'org.junit.jupiter:junit-jupiter'
28-
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
29-
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:${junit5Version}")
30+
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
3031

32+
testRuntimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}"
3133
}
3234

3335
test {
@@ -97,4 +99,4 @@ signing {
9799
def signingPassword = System.getenv("SIGNING_PASSWORD")
98100
useInMemoryPgpKeys(signingKey, signingPassword)
99101
sign publishing.publications.maven
100-
}
102+
}

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.wechat.pay.java.core;
22

3-
import static com.wechat.pay.java.core.cipher.Constant.HEX;
4-
53
import com.wechat.pay.java.core.auth.Credential;
64
import com.wechat.pay.java.core.auth.Validator;
75
import com.wechat.pay.java.core.auth.WechatPay2Credential;
@@ -14,6 +12,7 @@
1412
import com.wechat.pay.java.core.cipher.RSASigner;
1513
import com.wechat.pay.java.core.cipher.RSAVerifier;
1614
import com.wechat.pay.java.core.cipher.Signer;
15+
import com.wechat.pay.java.core.util.PemUtil;
1716
import java.security.PrivateKey;
1817
import java.security.cert.X509Certificate;
1918

@@ -44,7 +43,7 @@ protected AbstractRSAConfig(
4443
public PrivacyEncryptor createEncryptor() {
4544
X509Certificate certificate = certificateProvider.getAvailableCertificate();
4645
return new RSAPrivacyEncryptor(
47-
certificate.getPublicKey(), certificate.getSerialNumber().toString(HEX));
46+
certificate.getPublicKey(), PemUtil.getSerialNumber(certificate));
4847
}
4948

5049
@Override

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

+60-53
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
package com.wechat.pay.java.core.certificate;
22

3-
import static com.wechat.pay.java.core.cipher.Constant.HEX;
4-
5-
import com.wechat.pay.java.core.auth.Validator;
6-
import com.wechat.pay.java.core.auth.WechatPay2Validator;
73
import com.wechat.pay.java.core.certificate.model.Data;
84
import com.wechat.pay.java.core.certificate.model.DownloadCertificateResponse;
95
import com.wechat.pay.java.core.certificate.model.EncryptCertificate;
106
import com.wechat.pay.java.core.cipher.AeadCipher;
11-
import com.wechat.pay.java.core.exception.ValidationException;
127
import com.wechat.pay.java.core.http.Constant;
138
import com.wechat.pay.java.core.http.HttpClient;
149
import com.wechat.pay.java.core.http.HttpMethod;
1510
import com.wechat.pay.java.core.http.HttpRequest;
1611
import com.wechat.pay.java.core.http.HttpResponse;
17-
import com.wechat.pay.java.core.http.JsonResponseBody;
1812
import com.wechat.pay.java.core.http.MediaType;
13+
import com.wechat.pay.java.core.util.PemUtil;
1914
import java.nio.charset.StandardCharsets;
2015
import java.security.cert.X509Certificate;
21-
import java.util.ArrayList;
2216
import java.util.Base64;
2317
import java.util.HashMap;
2418
import java.util.List;
@@ -30,19 +24,19 @@
3024
/** 自动更新平台证书提供器抽象类 */
3125
public abstract class AbstractAutoCertificateProvider implements CertificateProvider {
3226
private static final Logger log = LoggerFactory.getLogger(AbstractAutoCertificateProvider.class);
33-
protected static final int UPDATE_INTERVAL_MINUTE = 60; // 定时更新时间,1小时
27+
protected static final int UPDATE_INTERVAL_MINUTE = 60; // 定时更新时间,60分钟,即1小时
3428
protected final SafeSingleScheduleExecutor executor =
3529
SafeSingleScheduleExecutor.getInstance(); // 安全的单线程定时执行器实例
36-
protected String requestUrl; // 请求URl
30+
3731
protected String merchantId; // 商户号
3832

3933
protected CertificateHandler certificateHandler; // 证书处理器
4034
protected AeadCipher aeadCipher; // 解密平台证书的aeadCipher;
4135
protected HttpClient httpClient; // 下载平台证书的httpClient
4236
private final HttpRequest httpRequest; // http请求
43-
private Validator validator; // 验证器
4437

45-
private int updateTime; // 自动更新次数
38+
private int updateCount; // 自动更新次数
39+
private int succeedCount; // 成功次数
4640
private final Map<String, Map<String, X509Certificate>> certificateMap; // 证书map
4741

4842
protected AbstractAutoCertificateProvider(
@@ -52,6 +46,24 @@ protected AbstractAutoCertificateProvider(
5246
HttpClient httpClient,
5347
String merchantId,
5448
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap) {
49+
this(
50+
requestUrl,
51+
certificateHandler,
52+
aeadCipher,
53+
httpClient,
54+
merchantId,
55+
wechatPayCertificateMap,
56+
UPDATE_INTERVAL_MINUTE * 60);
57+
}
58+
59+
protected AbstractAutoCertificateProvider(
60+
String requestUrl,
61+
CertificateHandler certificateHandler,
62+
AeadCipher aeadCipher,
63+
HttpClient httpClient,
64+
String merchantId,
65+
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap,
66+
int updateInterval) {
5567
this.merchantId = merchantId;
5668
synchronized (AbstractAutoCertificateProvider.class) {
5769
if (!wechatPayCertificateMap.containsKey(merchantId)) {
@@ -61,7 +73,7 @@ protected AbstractAutoCertificateProvider(
6173
"The corresponding provider for the merchant already exists.");
6274
}
6375
}
64-
this.requestUrl = requestUrl;
76+
6577
this.certificateHandler = certificateHandler;
6678
this.aeadCipher = aeadCipher;
6779
this.httpClient = httpClient;
@@ -73,42 +85,49 @@ protected AbstractAutoCertificateProvider(
7385
.addHeader(Constant.ACCEPT, " */*")
7486
.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue())
7587
.build();
88+
// 下载证书,如果失败会抛出异常
7689
downloadAndUpdate(wechatPayCertificateMap);
90+
7791
Runnable runnable =
7892
() -> {
7993
log.info(
80-
"Begin update Certificates.merchantId:{},total updates:{}", merchantId, updateTime);
81-
downloadAndUpdate(wechatPayCertificateMap);
94+
"Begin update Certificates.merchantId:{},total updates:{}", merchantId, updateCount);
95+
try {
96+
updateCount++;
97+
downloadAndUpdate(wechatPayCertificateMap);
98+
succeedCount++;
99+
} catch (Exception e) {
100+
// 已经有证书了,失败暂时忽略
101+
log.error("Download and update WechatPay certificates failed.", e);
102+
}
103+
82104
log.info(
83-
"Finish update Certificates.merchantId:{},total updates:{}", merchantId, updateTime);
105+
"Finish update Certificates.merchantId:{},total updates:{}, succeed updates:{}",
106+
merchantId,
107+
updateCount,
108+
succeedCount);
84109
};
85-
executor.scheduleAtFixedRate(
86-
runnable, UPDATE_INTERVAL_MINUTE, UPDATE_INTERVAL_MINUTE, TimeUnit.MINUTES);
110+
executor.scheduleAtFixedRate(runnable, updateInterval, updateInterval, TimeUnit.SECONDS);
87111
}
88112

89-
/** 下载和更新证书 */
113+
/**
114+
* 下载并更新证书
115+
*
116+
* @param wechatPayCertificateMap 存放多商户对应证书的Map
117+
*/
90118
protected void downloadAndUpdate(
91119
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap) {
92-
try {
93-
HttpResponse<DownloadCertificateResponse> httpResponse = downloadCertificate(httpClient);
94-
validateCertificate(httpResponse);
95-
updateCertificate(httpResponse, wechatPayCertificateMap);
96-
validator =
97-
new WechatPay2Validator(
98-
certificateHandler.generateVerifier(
99-
new ArrayList<>(wechatPayCertificateMap.get(merchantId).values())));
100-
updateTime++;
101-
} catch (Exception e) {
102-
if (validator == null) {
103-
throw e;
104-
}
105-
log.error("Download and update WechatPay certificates failed.", e);
106-
}
120+
HttpResponse<DownloadCertificateResponse> httpResponse = downloadCertificate(httpClient);
121+
122+
Map<String, X509Certificate> downloaded = decryptCertificate(httpResponse);
123+
validateCertificate(downloaded);
124+
wechatPayCertificateMap.put(merchantId, downloaded);
107125
}
108126

109127
/**
110128
* 下载证书
111129
*
130+
* @param httpClient 下载使用的HttpClient
112131
* @return httpResponse
113132
*/
114133
protected HttpResponse<DownloadCertificateResponse> downloadCertificate(HttpClient httpClient) {
@@ -117,30 +136,18 @@ protected HttpResponse<DownloadCertificateResponse> downloadCertificate(HttpClie
117136
return httpResponse;
118137
}
119138

120-
/**
121-
* 校验下载证书
122-
*
123-
* @param httpResponse httpResponse
124-
*/
125-
protected void validateCertificate(HttpResponse<DownloadCertificateResponse> httpResponse) {
126-
JsonResponseBody responseBody = (JsonResponseBody) (httpResponse.getBody());
127-
if (validator != null
128-
&& !validator.validate(httpResponse.getHeaders(), responseBody.getBody())) {
129-
throw new ValidationException(
130-
String.format(
131-
"Validate response failed,the WechatPay signature is incorrect.responseHeader[%s]\tresponseBody[%.1024s]",
132-
httpResponse.getHeaders(), httpResponse.getServiceResponse()));
133-
}
139+
protected void validateCertificate(Map<String, X509Certificate> certificates) {
140+
certificates.forEach((serialNo, cert) -> certificateHandler.validateCertPath(cert));
134141
}
135142

136143
/**
137-
* 更新证书
144+
* 从应答报文中解密证书
138145
*
139146
* @param httpResponse httpResponse
147+
* @return 应答报文解密后,生成X.509证书对象的Map
140148
*/
141-
protected void updateCertificate(
142-
HttpResponse<DownloadCertificateResponse> httpResponse,
143-
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap) {
149+
protected Map<String, X509Certificate> decryptCertificate(
150+
HttpResponse<DownloadCertificateResponse> httpResponse) {
144151
List<Data> dataList = httpResponse.getServiceResponse().getData();
145152
Map<String, X509Certificate> downloadCertMap = new HashMap<>();
146153
for (Data data : dataList) {
@@ -152,9 +159,9 @@ protected void updateCertificate(
152159
encryptCertificate.getNonce().getBytes(StandardCharsets.UTF_8),
153160
Base64.getDecoder().decode(encryptCertificate.getCiphertext()));
154161
certificate = certificateHandler.generateCertificate(decryptCertificate);
155-
downloadCertMap.put(certificate.getSerialNumber().toString(HEX).toUpperCase(), certificate);
162+
downloadCertMap.put(PemUtil.getSerialNumber(certificate), certificate);
156163
}
157-
wechatPayCertificateMap.put(merchantId, downloadCertMap);
164+
return downloadCertMap;
158165
}
159166

160167
public X509Certificate getAvailableCertificate(Map<String, X509Certificate> certificateMap) {
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.wechat.pay.java.core.certificate;
22

3-
import com.wechat.pay.java.core.cipher.Verifier;
43
import java.security.cert.X509Certificate;
5-
import java.util.List;
64

75
/** 证书处理器 */
86
public interface CertificateHandler {
@@ -16,10 +14,10 @@ public interface CertificateHandler {
1614
X509Certificate generateCertificate(String certificate);
1715

1816
/**
19-
* 使用微信支付平台证书生成Verifier
17+
* * 验证证书链
2018
*
21-
* @param certificateList 微信支付平台证书列表
22-
* @return verifier
19+
* @param certificate 微信支付平台证书
20+
* @throws com.wechat.pay.java.core.exception.ValidationException 证书验证失败
2321
*/
24-
Verifier generateVerifier(List<X509Certificate> certificateList);
22+
void validateCertPath(X509Certificate certificate);
2523
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,69 @@
11
package com.wechat.pay.java.core.certificate;
22

3-
import com.wechat.pay.java.core.cipher.RSAVerifier;
4-
import com.wechat.pay.java.core.cipher.Verifier;
3+
import com.wechat.pay.java.core.exception.ValidationException;
54
import com.wechat.pay.java.core.util.PemUtil;
6-
import java.security.cert.X509Certificate;
7-
import java.util.List;
5+
import java.security.cert.*;
6+
import java.util.*;
87

9-
class RSACertificateHandler implements CertificateHandler {
8+
final class RSACertificateHandler implements CertificateHandler {
9+
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)));
1041

1142
@Override
1243
public X509Certificate generateCertificate(String certificate) {
1344
return PemUtil.loadX509FromString(certificate);
1445
}
1546

1647
@Override
17-
public Verifier generateVerifier(List<X509Certificate> certificateList) {
18-
return new RSAVerifier(new InMemoryCertificateProvider(certificateList));
48+
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+
}
1968
}
2069
}

core/src/main/java/com/wechat/pay/java/core/util/PemUtil.java

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.wechat.pay.java.core.util;
22

3+
import static com.wechat.pay.java.core.cipher.Constant.HEX;
4+
35
import java.io.ByteArrayInputStream;
46
import java.io.FileInputStream;
57
import java.io.IOException;
@@ -190,4 +192,8 @@ public static X509Certificate loadX509FromString(String certificateString, Strin
190192
throw new UncheckedIOException(e);
191193
}
192194
}
195+
196+
public static String getSerialNumber(X509Certificate certificate) {
197+
return certificate.getSerialNumber().toString(HEX).toUpperCase();
198+
}
193199
}

0 commit comments

Comments
 (0)