Skip to content

增加自动更新证书功能 #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 27, 2019
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withWechatpay(wechatpayCertificates);
```

### 自动更新证书功能
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有一些注意事项需要补充说明好


可使用 AutoUpdateCertificatesVerifier 类,该类于原 CertificatesVerifier 上增加证书的**超时自动更新**(默认与上次更新时间超过一小时后自动更新),并会在首次创建时,进行证书更新。

示例代码:

```java
//...

AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
wechatpayCertificates,
new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
apiV3Key.getBytes("utf-8"));


WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient

// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
HttpClient httpClient = builder.build();

// 后面跟使用Apache HttpClient一样
HttpResponse response = httpClient.execute(...);
```

## 常见问题

### 如何下载平台证书?
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ ext {
httpclient_version = "4.5.8"
slf4j_version = "1.7.26"
junit_version = "4.12"
jackson_version = "2.9.7"
}

dependencies {
api "org.apache.httpcomponents:httpclient:$httpclient_version"
api "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
implementation "org.slf4j:slf4j-api:$slf4j_version"

testImplementation "junit:junit:$junit_version"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.wechat.pay.contrib.apache.httpclient.auth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wechat.pay.contrib.apache.httpclient.Credentials;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 在原有CertificatesVerifier基础上,增加自动更新证书功能
*/
public class AutoUpdateCertificatesVerifier implements Verifier {

private static final Logger log = LoggerFactory.getLogger(AutoUpdateCertificatesVerifier.class);

//证书下载地址
private static final String CertDownloadPath = "https://api.mch.weixin.qq.com/v3/certificates";

//上次更新时间
private volatile Instant instant;

//证书更新间隔时间,单位为分钟
private int minutesInterval;

private CertificatesVerifier verifier;

private List<X509Certificate> certList;

private Credentials credentials;

private byte[] apiV3Key;

private ReentrantLock lock = new ReentrantLock();

public AutoUpdateCertificatesVerifier(List<X509Certificate> certList, Credentials credentials,
byte[] apiV3Key) {
//默认证书更新时间为1小时
this(certList, credentials, apiV3Key, 60);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no magic number plz

}

public AutoUpdateCertificatesVerifier(List<X509Certificate> certList, Credentials credentials,
byte[] apiV3Key, int minutesInterval) {
this.certList = certList;
this.verifier = new CertificatesVerifier(certList);
this.credentials = credentials;
this.apiV3Key = apiV3Key;
this.instant = Instant.now();
this.minutesInterval = minutesInterval;
//构造时更新证书
try {
autoUpdateCert();
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}

@Override
public boolean verify(String serialNumber, byte[] message, String signature) {
if (Duration.between(Instant.now(), instant).toMinutes() >= minutesInterval) {
if (lock.tryLock()) {
try {
autoUpdateCert();
//更新时间
instant = Instant.now();
} catch (GeneralSecurityException | IOException e) {
log.warn("Auto update cert failed, exception = " + e);
} finally {
lock.unlock();
}
}
}
return verifier.verify(serialNumber, message, signature);
}

private void autoUpdateCert() throws IOException, GeneralSecurityException {
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withCredentials(credentials)
.withValidator(new WechatPay2Validator(new CertificatesVerifier(this.certList)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样商户开发者还是需要更新本地证书,自动更新只是运行时

.build();

HttpGet httpGet = new HttpGet(CertDownloadPath);
httpGet.addHeader("Accept", "application/json");

CloseableHttpResponse response = httpClient.execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode();
String body = EntityUtils.toString(response.getEntity());
if (statusCode == 200) {
List<X509Certificate> newCertList = deserializeToCerts(apiV3Key, body);
if (newCertList.isEmpty()) {
log.warn("Cert list is empty");
return;
}
this.certList = newCertList;
this.verifier = new CertificatesVerifier(certList);
} else {
log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body);
}
}


/**
* 反序列化证书并解密
*/
private List<X509Certificate> deserializeToCerts(byte[] apiV3Key, String body)
throws GeneralSecurityException, IOException {
AesUtil decryptor = new AesUtil(apiV3Key);
ObjectMapper mapper = new ObjectMapper();
JsonNode dataNode = mapper.readTree(body).get("data");
List<X509Certificate> newCertList = new ArrayList<>();
if (dataNode != null) {
for (int i = 0, count = dataNode.size(); i < count; i++) {
JsonNode encryptCertificateNode = dataNode.get(i).get("encrypt_certificate");
//解密
String cert = decryptor.decryptToString(
encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "")
.getBytes("utf-8"),
encryptCertificateNode.get("nonce").toString().replaceAll("\"", "")
.getBytes("utf-8"),
encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", ""));

X509Certificate x509Cert = PemUtil
.loadCertificate(new ByteArrayInputStream(cert.getBytes("utf-8")));
try {
x509Cert.checkValidity();
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
continue;
}
newCertList.add(x509Cert);
}
}
return newCertList;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.wechat.pay.contrib.apache.httpclient;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class AutoUpdateVerifierTest {

private static String mchId = ""; // 商户号
private static String mchSerialNo = ""; // 商户证书序列号
private static String apiV3Key = ""; // api密钥

private CloseableHttpClient httpClient;
private AutoUpdateCertificatesVerifier verifier;

// 你的商户私钥
private static String privateKey = "-----BEGIN PRIVATE KEY-----\n"
+ "-----END PRIVATE KEY-----\n";

// 你的微信支付平台证书
private static String certificate = "-----BEGIN CERTIFICATE-----\n"
+ "-----END CERTIFICATE-----";

//测试AutoUpdateCertificatesVerifier的verify方法参数
private static String serialNumber = "";
private static String message = "";
private static String signature = "";

@Before
public void setup() throws IOException {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
X509Certificate wechatpayCertificate = PemUtil.loadCertificate(
new ByteArrayInputStream(certificate.getBytes("utf-8")));

ArrayList<X509Certificate> listCertificates = new ArrayList<>();
listCertificates.add(wechatpayCertificate);

//使用自动更新的签名验证器
verifier = new AutoUpdateCertificatesVerifier(
listCertificates,
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
apiV3Key.getBytes("utf-8"), 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

传0?也建议定义一组枚举常量作为常见的选项


httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
}

@After
public void after() throws IOException {
httpClient.close();
}

@Test
public void autoUpdateVerifierTest() throws Exception {
assertTrue(verifier.verify(serialNumber, message.getBytes("utf-8"), signature));
}

@Test
public void getCertificateTest() throws Exception {
URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates");
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response1 = httpClient.execute(httpGet);
assertEquals(200, response1.getStatusLine().getStatusCode());
try {
HttpEntity entity1 = response1.getEntity();
// do something useful with the response body
// and ensure it is fully consumed
EntityUtils.consume(entity1);
} finally {
response1.close();
}
}
}