Skip to content

Commit

Permalink
增加自动更新证书功能 (#3)
Browse files Browse the repository at this point in the history
* 增加自动更新证书功能
  • Loading branch information
z1cheng authored and xy-peng committed Aug 27, 2019
1 parent 63a2e77 commit c7deb46
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 3 deletions.
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();
}
}
}

0 comments on commit c7deb46

Please sign in to comment.