-
Notifications
You must be signed in to change notification settings - Fork 255
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 增加自动更新证书功能
- Loading branch information
Showing
5 changed files
with
278 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
...in/java/com/wechat/pay/contrib/apache/httpclient/auth/AutoUpdateCertificatesVerifier.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
src/test/java/com/wechat/pay/contrib/apache/httpclient/AutoUpdateVerifierTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |