-
Notifications
You must be signed in to change notification settings - Fork 254
增加自动更新证书功能 #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
增加自动更新证书功能 #3
Changes from 3 commits
31da0ea
3bbf573
3d4424f
3bd088b
6dd05da
58f5661
f0798a6
7126e23
4f2d051
1a244ba
813efa8
30bfd76
40d96f4
ae4d6ae
140b6f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
有一些注意事项需要补充说明好