Skip to content

Commit c7deb46

Browse files
z1chengxy-peng
authored andcommitted
增加自动更新证书功能 (#3)
* 增加自动更新证书功能
1 parent 63a2e77 commit c7deb46

File tree

5 files changed

+278
-3
lines changed

5 files changed

+278
-3
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# wechatpay-apache-httpclient
1+
# wechatpay-apache-httpclient
22

33
## 概览
44

@@ -95,6 +95,37 @@ WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
9595
.withWechatpay(wechatpayCertificates);
9696
```
9797

98+
### 自动更新证书功能(可选)
99+
100+
可使用 AutoUpdateCertificatesVerifier 类,该类于原 CertificatesVerifier 上增加证书的**超时自动更新**(默认与上次更新时间超过一小时后自动更新),并会在首次创建时,进行证书更新。
101+
102+
示例代码:
103+
104+
```java
105+
//不需要传入微信支付证书,将会自动更新
106+
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
107+
new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
108+
apiV3Key.getBytes("utf-8"));
109+
110+
111+
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
112+
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
113+
.withValidator(new WechatPay2Validator(verifier))
114+
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
115+
116+
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
117+
HttpClient httpClient = builder.build();
118+
119+
// 后面跟使用Apache HttpClient一样
120+
HttpResponse response = httpClient.execute(...);
121+
```
122+
123+
#### 风险
124+
125+
因为不需要传入微信支付平台证书,AutoUpdateCertificatesVerifier 在首次更新证书时**不会验签**,也就无法确认应答身份,可能导致下载错误的证书。
126+
127+
但下载时会通过 **HTTPS****AES 对称加密**来保证证书安全,所以可以认为,在使用官方 JDK、且 APIv3 密钥不泄露的情况下,AutoUpdateCertificatesVerifier 是**安全**的。
128+
98129
## 常见问题
99130

100131
### 如何下载平台证书?

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ ext {
1717
httpclient_version = "4.5.8"
1818
slf4j_version = "1.7.26"
1919
junit_version = "4.12"
20+
jackson_version = "2.9.7"
2021
}
2122

2223
dependencies {
2324
api "org.apache.httpcomponents:httpclient:$httpclient_version"
25+
api "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
2426
implementation "org.slf4j:slf4j-api:$slf4j_version"
25-
27+
testImplementation "org.slf4j:slf4j-simple:$slf4j_version"
2628
testImplementation "junit:junit:$junit_version"
2729
}
2830

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.wechat.pay.contrib.apache.httpclient.auth;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.wechat.pay.contrib.apache.httpclient.Credentials;
6+
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
7+
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
8+
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
9+
import java.io.ByteArrayInputStream;
10+
import java.io.IOException;
11+
import java.security.GeneralSecurityException;
12+
import java.security.cert.CertificateExpiredException;
13+
import java.security.cert.CertificateNotYetValidException;
14+
import java.security.cert.X509Certificate;
15+
import java.time.Duration;
16+
import java.time.Instant;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.concurrent.locks.ReentrantLock;
20+
import org.apache.http.client.methods.CloseableHttpResponse;
21+
import org.apache.http.client.methods.HttpGet;
22+
import org.apache.http.impl.client.CloseableHttpClient;
23+
import org.apache.http.util.EntityUtils;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
/**
28+
* 在原有CertificatesVerifier基础上,增加自动更新证书功能
29+
*/
30+
public class AutoUpdateCertificatesVerifier implements Verifier {
31+
32+
private static final Logger log = LoggerFactory.getLogger(AutoUpdateCertificatesVerifier.class);
33+
34+
//证书下载地址
35+
private static final String CertDownloadPath = "https://api.mch.weixin.qq.com/v3/certificates";
36+
37+
//上次更新时间
38+
private volatile Instant instant;
39+
40+
//证书更新间隔时间,单位为分钟
41+
private int minutesInterval;
42+
43+
private CertificatesVerifier verifier;
44+
45+
private Credentials credentials;
46+
47+
private byte[] apiV3Key;
48+
49+
private ReentrantLock lock = new ReentrantLock();
50+
51+
//时间间隔枚举,支持一小时、六小时以及十二小时
52+
public enum TimeInterval {
53+
OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12);
54+
55+
private int minutes;
56+
57+
TimeInterval(int minutes) {
58+
this.minutes = minutes;
59+
}
60+
61+
public int getMinutes() {
62+
return minutes;
63+
}
64+
}
65+
66+
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) {
67+
this(credentials, apiV3Key, TimeInterval.OneHour.getMinutes());
68+
}
69+
70+
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key, int minutesInterval) {
71+
this.credentials = credentials;
72+
this.apiV3Key = apiV3Key;
73+
this.minutesInterval = minutesInterval;
74+
//构造时更新证书
75+
try {
76+
autoUpdateCert();
77+
instant = Instant.now();
78+
} catch (IOException | GeneralSecurityException e) {
79+
throw new RuntimeException(e);
80+
}
81+
}
82+
83+
@Override
84+
public boolean verify(String serialNumber, byte[] message, String signature) {
85+
if (instant == null || Duration.between(instant, Instant.now()).toMinutes() >= minutesInterval) {
86+
if (lock.tryLock()) {
87+
try {
88+
autoUpdateCert();
89+
//更新时间
90+
instant = Instant.now();
91+
} catch (GeneralSecurityException | IOException e) {
92+
log.warn("Auto update cert failed, exception = " + e);
93+
} finally {
94+
lock.unlock();
95+
}
96+
}
97+
}
98+
return verifier.verify(serialNumber, message, signature);
99+
}
100+
101+
private void autoUpdateCert() throws IOException, GeneralSecurityException {
102+
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
103+
.withCredentials(credentials)
104+
.withValidator(verifier == null ? (response) -> true : new WechatPay2Validator(verifier))
105+
.build();
106+
107+
HttpGet httpGet = new HttpGet(CertDownloadPath);
108+
httpGet.addHeader("Accept", "application/json");
109+
110+
CloseableHttpResponse response = httpClient.execute(httpGet);
111+
int statusCode = response.getStatusLine().getStatusCode();
112+
String body = EntityUtils.toString(response.getEntity());
113+
if (statusCode == 200) {
114+
List<X509Certificate> newCertList = deserializeToCerts(apiV3Key, body);
115+
if (newCertList.isEmpty()) {
116+
log.warn("Cert list is empty");
117+
return;
118+
}
119+
this.verifier = new CertificatesVerifier(newCertList);
120+
} else {
121+
log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body);
122+
}
123+
}
124+
125+
126+
/**
127+
* 反序列化证书并解密
128+
*/
129+
private List<X509Certificate> deserializeToCerts(byte[] apiV3Key, String body)
130+
throws GeneralSecurityException, IOException {
131+
AesUtil decryptor = new AesUtil(apiV3Key);
132+
ObjectMapper mapper = new ObjectMapper();
133+
JsonNode dataNode = mapper.readTree(body).get("data");
134+
List<X509Certificate> newCertList = new ArrayList<>();
135+
if (dataNode != null) {
136+
for (int i = 0, count = dataNode.size(); i < count; i++) {
137+
JsonNode encryptCertificateNode = dataNode.get(i).get("encrypt_certificate");
138+
//解密
139+
String cert = decryptor.decryptToString(
140+
encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "")
141+
.getBytes("utf-8"),
142+
encryptCertificateNode.get("nonce").toString().replaceAll("\"", "")
143+
.getBytes("utf-8"),
144+
encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", ""));
145+
146+
X509Certificate x509Cert = PemUtil
147+
.loadCertificate(new ByteArrayInputStream(cert.getBytes("utf-8")));
148+
try {
149+
x509Cert.checkValidity();
150+
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
151+
continue;
152+
}
153+
newCertList.add(x509Cert);
154+
}
155+
}
156+
return newCertList;
157+
}
158+
}

src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/WechatPay2Validator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
public class WechatPay2Validator implements Validator {
1313

14-
private static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class);
14+
private static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
1515

1616
private Verifier verifier;
1717

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.wechat.pay.contrib.apache.httpclient;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
7+
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
8+
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
9+
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
10+
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.security.PrivateKey;
14+
import org.apache.http.HttpEntity;
15+
import org.apache.http.client.methods.CloseableHttpResponse;
16+
import org.apache.http.client.methods.HttpGet;
17+
import org.apache.http.client.utils.URIBuilder;
18+
import org.apache.http.impl.client.CloseableHttpClient;
19+
import org.apache.http.util.EntityUtils;
20+
import org.junit.After;
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
24+
public class AutoUpdateVerifierTest {
25+
26+
private static String mchId = ""; // 商户号
27+
private static String mchSerialNo = ""; // 商户证书序列号
28+
private static String apiV3Key = ""; // api密钥
29+
30+
private CloseableHttpClient httpClient;
31+
private AutoUpdateCertificatesVerifier verifier;
32+
33+
// 你的商户私钥
34+
private static String privateKey = "-----BEGIN PRIVATE KEY-----\n"
35+
+ "-----END PRIVATE KEY-----\n";
36+
37+
//测试AutoUpdateCertificatesVerifier的verify方法参数
38+
private static String serialNumber = "";
39+
private static String message = "";
40+
private static String signature = "";
41+
42+
@Before
43+
public void setup() throws IOException {
44+
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
45+
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
46+
47+
//使用自动更新的签名验证器,不需要传入证书
48+
verifier = new AutoUpdateCertificatesVerifier(
49+
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
50+
apiV3Key.getBytes("utf-8"));
51+
52+
httpClient = WechatPayHttpClientBuilder.create()
53+
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
54+
.withValidator(new WechatPay2Validator(verifier))
55+
.build();
56+
}
57+
58+
@After
59+
public void after() throws IOException {
60+
httpClient.close();
61+
}
62+
63+
@Test
64+
public void autoUpdateVerifierTest() throws Exception {
65+
assertTrue(verifier.verify(serialNumber, message.getBytes("utf-8"), signature));
66+
}
67+
68+
@Test
69+
public void getCertificateTest() throws Exception {
70+
URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates");
71+
HttpGet httpGet = new HttpGet(uriBuilder.build());
72+
httpGet.addHeader("Accept", "application/json");
73+
CloseableHttpResponse response1 = httpClient.execute(httpGet);
74+
assertEquals(200, response1.getStatusLine().getStatusCode());
75+
try {
76+
HttpEntity entity1 = response1.getEntity();
77+
// do something useful with the response body
78+
// and ensure it is fully consumed
79+
EntityUtils.consume(entity1);
80+
} finally {
81+
response1.close();
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)