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
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# wechatpay-apache-httpclient
# wechatpay-apache-httpclient

## 概览

Expand Down Expand Up @@ -95,6 +95,37 @@ WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
.withWechatpay(wechatpayCertificates);
```

### 自动更新证书功能(可选)

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

示例代码:

```java
//不需要传入微信支付证书,将会自动更新
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
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(...);
```

#### 风险

因为不需要传入微信支付平台证书,AutoUpdateCertificatesVerifier 在首次更新证书时**不会验签**,也就无法确认应答身份,可能导致下载错误的证书。

但下载时会通过 **HTTPS**、**AES 对称加密**来保证证书安全,所以可以认为,在使用官方 JDK、且 APIv3 密钥不泄露的情况下,AutoUpdateCertificatesVerifier 是**安全**的。

## 常见问题

### 如何下载平台证书?
Expand Down
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ 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 "org.slf4j:slf4j-simple:$slf4j_version"
testImplementation "junit:junit:$junit_version"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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 Credentials credentials;

private byte[] apiV3Key;

private ReentrantLock lock = new ReentrantLock();

//时间间隔枚举,支持一小时、六小时以及十二小时
public enum TimeInterval {
OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12);

private int minutes;

TimeInterval(int minutes) {
this.minutes = minutes;
}

public int getMinutes() {
return minutes;
}
}

public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) {
this(credentials, apiV3Key, TimeInterval.OneHour.getMinutes());
}

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

@Override
public boolean verify(String serialNumber, byte[] message, String signature) {
if (instant == null || Duration.between(instant, Instant.now()).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(verifier == null ? (response) -> true : new WechatPay2Validator(verifier))
.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.verifier = new CertificatesVerifier(newCertList);
} 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
Expand Up @@ -11,7 +11,7 @@

public class WechatPay2Validator implements Validator {

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

private Verifier verifier;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 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";

//测试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")));

//使用自动更新的签名验证器,不需要传入证书
verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
apiV3Key.getBytes("utf-8"));

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();
}
}
}