Skip to content

Commit d60ea25

Browse files
authored
feat: 增加支持公钥验签 (#219)
1 parent 1fab366 commit d60ea25

18 files changed

+360
-69
lines changed

Diff for: README.md

+52-21
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44

55
[微信支付API v3](https://wechatpay-api.gitbook.io/wechatpay-api-v3/)[Apache HttpClient](https://hc.apache.org/httpcomponents-client-ga/index.html)扩展,实现了请求签名的生成和应答签名的验证。
66

7-
如果你是使用Apache HttpClient的商户开发者,可以使用它构造`HttpClient`。得到的`HttpClient`在执行请求时将自动携带身份认证信息,并检查应答的微信支付签名。
7+
> [!IMPORTANT]
8+
> 我们强烈建议你改为使用 [WechatPay-Java](https://github.com/wechatpay-apiv3/wechatpay-java),该SDK同样支持 Apache HttpClient 且提供了更完善的功能,本库未来只会进行必要的修复更新。
89
910
## 项目状态
1011

11-
当前版本`0.5.0`为测试版本。请商户的专业技术人员在使用时注意系统和软件的正确性和兼容性,以及带来的风险。
12+
当前版本`0.6.0`为测试版本。请商户的专业技术人员在使用时注意系统和软件的正确性和兼容性,以及带来的风险。
1213

1314
## 升级指引
1415

15-
若你使用的版本为`0.3.0`,升级前请参考[升级指南](UPGRADING.md)
16+
若你使用的版本为`<=0.5.0`,升级前请参考[升级指南](UPGRADING.md)
1617

1718
## 环境要求
1819

@@ -27,7 +28,7 @@
2728
在你的`build.gradle`文件中加入如下的依赖
2829

2930
```groovy
30-
implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.5.0'
31+
implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.6.0'
3132
```
3233

3334
### Maven
@@ -37,17 +38,18 @@ implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.5.0'
3738
<dependency>
3839
<groupId>com.github.wechatpay-apiv3</groupId>
3940
<artifactId>wechatpay-apache-httpclient</artifactId>
40-
<version>0.5.0</version>
41+
<version>0.6.0</version>
4142
</dependency>
4243
```
4344

4445
## 名词解释
4546

46-
+ 商户API证书,是用来证实商户身份的。证书中包含商户号、证书序列号、证书有效期等信息,由证书授权机构(Certificate Authority ,简称CA)签发,以防证书被伪造或篡改。如何获取请见[商户API证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#shang-hu-api-zheng-shu)
47-
+ 商户API私钥。商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem中。注:不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。
48-
+ 微信支付平台证书。平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行应答签名的验证。获取平台证书需通过[获取平台证书列表](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu)接口下载。
49-
+ 证书序列号。每个证书都有一个由CA颁发的唯一编号,即证书序列号。如何查看证书序列号请看[这里](https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao)
50-
+ API v3密钥。为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。API v3密钥是加密时使用的对称密钥。商户可以在【商户平台】->【API安全】的页面设置该密钥。
47+
+ **商户API证书**,是用来证实商户身份的。证书中包含商户号、证书序列号、证书有效期等信息,由证书授权机构(Certificate Authority ,简称CA)签发,以防证书被伪造或篡改。如何获取请见[商户API证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#shang-hu-api-zheng-shu)
48+
+ **商户API私钥**。商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem中。注:不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。
49+
+ **微信支付平台证书**。平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行应答签名的验证。获取平台证书需通过[获取平台证书列表](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu)接口下载。
50+
+ **微信支付公钥**。由微信支付生成,商户可以使用该公钥进行应答签名、回调签名的验证,详见:[微信支付公钥](https://pay.weixin.qq.com/doc/v3/merchant/4012153196)
51+
+ **证书序列号**。每个证书都有一个由CA颁发的唯一编号,即证书序列号。如何查看证书序列号请看[这里](https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao)
52+
+ **API v3密钥**。为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。API v3密钥是加密时使用的对称密钥。商户可以在【商户平台】->【API安全】的页面设置该密钥。
5153

5254
## 开始
5355

@@ -58,7 +60,7 @@ import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
5860
//...
5961
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
6062
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
61-
.withWechatPay(wechatPayCertificates);
63+
.withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey);
6264
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
6365

6466
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签
@@ -73,7 +75,8 @@ CloseableHttpResponse response = httpClient.execute(...);
7375
+ `merchantId`商户号。
7476
+ `merchantSerialNumber`商户API证书的证书序列号。
7577
+ `merchantPrivateKey`商户API私钥,如何加载商户API私钥请看[常见问题](#如何加载商户私钥)
76-
+ `wechatPayCertificates`微信支付平台证书列表。你也可以使用后面章节提到的“[定时更新平台证书功能](#定时更新平台证书功能)”,而不需要关心平台证书的来龙去脉。
78+
+ `wechatpayPublicKeyId`微信支付公钥ID,登录商户平台可获取,详见:[获取微信支付公钥](https://pay.weixin.qq.com/doc/v3/merchant/4013053249#1.-%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%85%AC%E9%92%A5)
79+
+ `wechatPayPublicKey`微信支付公钥,登录商户平台可获取,详见:[获取微信支付公钥](https://pay.weixin.qq.com/doc/v3/merchant/4013053249#1.-%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%85%AC%E9%92%A5)
7780

7881
### 示例:获取平台证书
7982

@@ -177,11 +180,14 @@ Credentials credentials = new WechatPay2Credentials(merchantId, new Signer() {
177180
});
178181
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
179182
.withCredentials(credentials)
180-
.withWechatPay(wechatPayCertificates);
183+
.withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey);
181184
```
182185

183186
## 定时更新平台证书功能
184187

188+
> [!IMPORTANT]
189+
> 新注册的商户使用「微信支付公钥」验签,不需要下载和更新平台证书。仅尚未完全迁移至「微信支付公钥」验签的旧商户才需要此能力。
190+
185191
版本>=`0.4.0`可使用 CertificatesManager.getVerifier(merchantId) 得到的验签器替代默认的验签器。它会定时下载和更新商户对应的[微信支付平台证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu) (默认下载间隔为UPDATE_INTERVAL_MINUTE)。
186192

187193
示例代码:
@@ -197,7 +203,7 @@ certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId
197203
verifier = certificatesManager.getVerifier(merchantId);
198204
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
199205
.withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
200-
.withValidator(new WechatPay2Validator(verifier))
206+
.withValidator(new WechatPay2Validator(verifier));
201207
// ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
202208

203209
// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新
@@ -217,13 +223,13 @@ CloseableHttpResponse response = httpClient.execute(...);
217223

218224
### 加密
219225

220-
使用` RsaCryptoUtil.encryptOAEP(String, X509Certificate)`进行公钥加密。示例代码如下。
226+
使用` RsaCryptoUtil.encryptOAEP(String, PublicKey publicKey)`进行公钥加密。示例代码如下。
221227

222228
```java
223229
// 建议从Verifier中获得微信支付平台证书,或使用预先下载到本地的平台证书文件中
224-
X509Certificate certificate = verifier.getValidCertificate();
230+
PublicKey publicKey = verifier.getValidPublicKey();
225231
try {
226-
String ciphertext = RsaCryptoUtil.encryptOAEP(text, certificate);
232+
String ciphertext = RsaCryptoUtil.encryptOAEP(text, publicKey);
227233
} catch (IllegalBlockSizeException e) {
228234
e.printStackTrace();
229235
}
@@ -277,15 +283,40 @@ try (FileInputStream ins1 = new FileInputStream(file)) {
277283
2. 使用`NotificationHandler`构造一个回调通知处理器,需设置验证器、apiV3密钥。调用`parse(request)`得到回调通知`notification`
278284

279285
示例请参考下列代码。
286+
280287
```java
281288
// 构建request,传入必要参数
282-
NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial)
289+
NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial)
283290
.withNonce(nonce)
284291
.withTimestamp(timestamp)
285292
.withSignature(signature)
286293
.withBody(body)
287294
.build();
288-
NotificationHandler handler = new NotificationHandler(verifier, apiV3Key.getBytes(StandardCharsets.UTF_8));
295+
296+
// 如果已经初始化了 NotificationHandler 则直接使用,否则根据具体情况创建一个
297+
298+
// 1. 如果你使用的是微信支付公私钥,则使用公钥初始化 Verifier 以创建 NotificationHandler
299+
NotificationHandler handler = new NotificationHandler(
300+
new PublicKeyVerifier(wechatPayPublicKeyId, wechatPayPublicKey),
301+
apiV3Key.getBytes(StandardCharsets.UTF_8)
302+
);
303+
304+
// 2. 如果你使用的事微信支付平台证书,则从 CertificatesManager 获取 Verifier 以创建 NotificationHandler
305+
NotificationHandler handler = new NotificationHandler(
306+
certificatesManager.getVerifier(merchantId),
307+
apiV3Key.getBytes(StandardCharsets.UTF_8)
308+
);
309+
310+
// 3. 如果你正在进行微信支付平台证书到微信支付公私钥的灰度切换,希望保持切换兼容,则需要使用 MixVerifier 创建 NotificationHandler
311+
Verifier mixVerifier = new MixVerifier(
312+
new PublicKeyVerifier(wechatPayPublicKeyId, wechatPayPublicKey),
313+
certificatesManager.getVerifier(merchantId)
314+
);
315+
NotificationHandler handler = new NotificationHandler(
316+
mixVerifier,
317+
apiV3Key.getBytes(StandardCharsets.UTF_8)
318+
);
319+
289320
// 验签和解析请求体
290321
Notification notification = handler.parse(request);
291322
// 从notification中获取解密报文
@@ -306,11 +337,11 @@ System.out.println(notification.getDecryptData());
306337
商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件`apiclient_key.pem`中。商户开发者可以使用方法`PemUtil.loadPrivateKey()`加载证书。
307338

308339
```java
309-
# 示例:私钥存储在文件
340+
// 示例:私钥存储在文件
310341
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
311342
new FileInputStream("/path/to/apiclient_key.pem"));
312343

313-
# 示例:私钥为String字符串
344+
// 示例:私钥为String字符串
314345
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(
315346
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
316347
```

Diff for: UPGRADING.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# 升级指南
22

3+
## 从 0.5.0 升级至 0.6.0
4+
`interface Verifier` 不再提供 `getValidCertificate` 接口,请换用 `getValidPublicKey` 接口。
5+
请注意 `getValidCertificate``getValidPublicKey` 并不能等价替换,但其返回值都可以用于调用 `RsaCryptoUtil.encryptOAEP` 实现加密。
6+
37
## 从 0.3.0 升级至 0.4.0
48

59
版本`0.4.0`提供了支持多商户号的[定时更新平台证书功能](README.md#定时更新平台证书功能),不兼容版本`0.3.0`。推荐升级方式如下:

Diff for: build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group 'com.github.wechatpay-apiv3'
8-
version '0.5.0'
8+
version '0.6.0'
99

1010
sourceCompatibility = 1.8
1111
targetCompatibility = 1.8

Diff for: src/main/java/com/wechat/pay/contrib/apache/httpclient/SignatureExec.java

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.wechat.pay.contrib.apache.httpclient;
22

3+
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;
34
import static org.apache.http.HttpHeaders.AUTHORIZATION;
45
import static org.apache.http.HttpStatus.SC_MULTIPLE_CHOICES;
56
import static org.apache.http.HttpStatus.SC_OK;
@@ -81,6 +82,7 @@ private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestW
8182
}
8283
// 添加认证信息
8384
request.addHeader(AUTHORIZATION, credentials.getSchema() + " " + credentials.getToken(request));
85+
request.addHeader(WECHAT_PAY_SERIAL, validator.getSerialNumber());
8486
// 执行
8587
CloseableHttpResponse response = mainExec.execute(route, request, context, execAware);
8688
// 对成功应答验签

Diff for: src/main/java/com/wechat/pay/contrib/apache/httpclient/Validator.java

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ public interface Validator {
1010

1111
boolean validate(CloseableHttpResponse response) throws IOException;
1212

13+
String getSerialNumber();
1314
}

Diff for: src/main/java/com/wechat/pay/contrib/apache/httpclient/WechatPayHttpClientBuilder.java

+7
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import com.wechat.pay.contrib.apache.httpclient.auth.CertificatesVerifier;
44
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
5+
import com.wechat.pay.contrib.apache.httpclient.auth.PublicKeyVerifier;
56
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
67
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
78
import java.security.PrivateKey;
9+
import java.security.PublicKey;
810
import java.security.cert.X509Certificate;
911
import java.util.List;
1012
import org.apache.http.impl.client.CloseableHttpClient;
@@ -53,6 +55,11 @@ public WechatPayHttpClientBuilder withWechatPay(List<X509Certificate> certificat
5355
return this;
5456
}
5557

58+
public WechatPayHttpClientBuilder withWechatPay(String publicKeyId, PublicKey publicKey) {
59+
this.validator = new WechatPay2Validator(new PublicKeyVerifier(publicKeyId, publicKey));
60+
return this;
61+
}
62+
5663
public WechatPayHttpClientBuilder withProxy(HttpHost proxy) {
5764
if (proxy != null) {
5865
this.setProxy(proxy);

Diff for: src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/AutoUpdateCertificatesVerifier.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import com.fasterxml.jackson.databind.JsonNode;
88
import com.fasterxml.jackson.databind.ObjectMapper;
99
import com.wechat.pay.contrib.apache.httpclient.Credentials;
10+
import com.wechat.pay.contrib.apache.httpclient.Validator;
1011
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
1112
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
1213
import java.io.ByteArrayInputStream;
1314
import java.io.IOException;
1415
import java.nio.charset.StandardCharsets;
1516
import java.security.GeneralSecurityException;
17+
import java.security.PublicKey;
1618
import java.security.cert.CertificateExpiredException;
1719
import java.security.cert.CertificateFactory;
1820
import java.security.cert.CertificateNotYetValidException;
@@ -57,6 +59,19 @@ public class AutoUpdateCertificatesVerifier implements Verifier {
5759
protected volatile Instant lastUpdateTime;
5860
protected CertificatesVerifier verifier;
5961

62+
private static final Validator emptyValidator =
63+
new Validator() {
64+
@Override
65+
public boolean validate(CloseableHttpResponse response) throws IOException {
66+
return true;
67+
};
68+
69+
@Override
70+
public String getSerialNumber() {
71+
return "";
72+
}
73+
};
74+
6075
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) {
6176
this(credentials, apiV3Key, TimeUnit.HOURS.toMinutes(1));
6277
}
@@ -94,14 +109,19 @@ public boolean verify(String serialNumber, byte[] message, String signature) {
94109
}
95110

96111
@Override
97-
public X509Certificate getValidCertificate() {
98-
return verifier.getValidCertificate();
112+
public PublicKey getValidPublicKey() {
113+
return verifier.getValidPublicKey();
114+
}
115+
116+
@Override
117+
public String getSerialNumber() {
118+
return verifier.getSerialNumber();
99119
}
100120

101121
protected void autoUpdateCert() throws IOException, GeneralSecurityException {
102122
try (CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
103123
.withCredentials(credentials)
104-
.withValidator(verifier == null ? (response) -> true : new WechatPay2Validator(verifier))
124+
.withValidator(verifier == null ? emptyValidator : new WechatPay2Validator(verifier))
105125
.build()) {
106126

107127
HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH);

Diff for: src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/CertificatesVerifier.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.math.BigInteger;
44
import java.security.InvalidKeyException;
55
import java.security.NoSuchAlgorithmException;
6+
import java.security.PublicKey;
67
import java.security.Signature;
78
import java.security.SignatureException;
89
import java.security.cert.CertificateExpiredException;
@@ -67,7 +68,6 @@ public boolean verify(String serialNumber, byte[] message, String signature) {
6768
return verify(cert, message, signature);
6869
}
6970

70-
@Override
7171
public X509Certificate getValidCertificate() {
7272
X509Certificate latestCert = null;
7373
for (X509Certificate x509Cert : certificates.values()) {
@@ -83,5 +83,16 @@ public X509Certificate getValidCertificate() {
8383
throw new NoSuchElementException("没有有效的微信支付平台证书");
8484
}
8585
}
86+
87+
88+
@Override
89+
public PublicKey getValidPublicKey() {
90+
return getValidCertificate().getPublicKey();
91+
}
92+
93+
@Override
94+
public String getSerialNumber() {
95+
return getValidCertificate().getSerialNumber().toString(16).toUpperCase();
96+
}
8697
}
8798

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.wechat.pay.contrib.apache.httpclient.auth;
2+
3+
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
7+
import java.security.PublicKey;
8+
import java.util.Objects;
9+
10+
11+
/**
12+
* MixVerifier 混合Verifier,仅用于切换平台证书与微信支付公钥时提供兼容
13+
*
14+
* 本实例需要使用一个 PublicKeyVerifier + 一个 Verifier 初始化,前者提供微信支付公钥验签,后者提供平台证书验签
15+
*/
16+
public class MixVerifier implements Verifier {
17+
private static final Logger log = LoggerFactory.getLogger(MixVerifier.class);
18+
19+
final PublicKeyVerifier publicKeyVerifier;
20+
final Verifier certificateVerifier;
21+
22+
public MixVerifier(PublicKeyVerifier publicKeyVerifier, Verifier certificateVerifier) {
23+
if (publicKeyVerifier == null) {
24+
throw new IllegalArgumentException("publicKeyVerifier cannot be null");
25+
}
26+
27+
this.publicKeyVerifier = publicKeyVerifier;
28+
this.certificateVerifier = certificateVerifier;
29+
}
30+
31+
@Override
32+
public boolean verify(String serialNumber, byte[] message, String signature) {
33+
if (Objects.equals(publicKeyVerifier.getSerialNumber(), serialNumber)) {
34+
return publicKeyVerifier.verify(serialNumber, message, signature);
35+
}
36+
37+
if (certificateVerifier != null) {
38+
return certificateVerifier.verify(serialNumber, message, signature);
39+
}
40+
41+
log.error("找不到证书序列号对应的证书,序列号:{}", serialNumber);
42+
return false;
43+
}
44+
45+
@Override
46+
public PublicKey getValidPublicKey() {
47+
return publicKeyVerifier.getValidPublicKey();
48+
}
49+
50+
@Override
51+
public String getSerialNumber() {
52+
return publicKeyVerifier.getSerialNumber();
53+
}
54+
}

0 commit comments

Comments
 (0)