Skip to content

Commit

Permalink
回调和回包验签支持平台公钥
Browse files Browse the repository at this point in the history
  • Loading branch information
wujunjiesd committed May 11, 2024
1 parent 31f42ff commit 167e9c4
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 159 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,23 @@ Config config =
.build();
```

## 使用本地平台公钥

如果你的商户可使用微信支付的公钥验证应答和回调的签名,可使用微信支付公钥和公钥ID初始化。

```java
// 可以根据实际情况使用publicKeyFromPath或publicKey加载公钥
Config config =
new RSAPublicKeyConfig.Builder()
.merchantId(merchantId)
.privateKeyFromPath(privateKeyPath)
.publicKeyFromPath(publicKeyPath)
.publicKeyId(publicKeyId)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
```

## 回调通知

首先,你需要在你的服务器上创建一个公开的 HTTP 端点,接受来自微信支付的回调通知。
Expand Down
29 changes: 29 additions & 0 deletions core/src/main/java/com/wechat/pay/java/core/AbstractRSAConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
import com.wechat.pay.java.core.cipher.Signer;
import com.wechat.pay.java.core.util.PemUtil;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;

/** RSAConfig抽象类 */
public abstract class AbstractRSAConfig implements Config {

/** 使用微信支付平台证书验签 */
protected AbstractRSAConfig(
String merchantId,
PrivateKey privateKey,
Expand All @@ -28,6 +30,23 @@ protected AbstractRSAConfig(
this.privateKey = privateKey;
this.merchantSerialNumber = merchantSerialNumber;
this.certificateProvider = certificateProvider;
this.publicKey = null;
this.publicKeyId = null;
}

/** 使用微信支付公钥验签 */
protected AbstractRSAConfig(
String merchantId,
PrivateKey privateKey,
String merchantSerialNumber,
PublicKey publicKey,
String publicKeyId) {
this.merchantId = merchantId;
this.privateKey = privateKey;
this.merchantSerialNumber = merchantSerialNumber;
this.certificateProvider = null;
this.publicKey = publicKey;
this.publicKeyId = publicKeyId;
}

/** 商户号 */
Expand All @@ -38,9 +57,16 @@ protected AbstractRSAConfig(
private final String merchantSerialNumber;
/** 微信支付平台证书Provider */
private final CertificateProvider certificateProvider;
/** 微信支付平台公钥 */
private final PublicKey publicKey;
/** 微信支付平台公钥Id */
private final String publicKeyId;

@Override
public PrivacyEncryptor createEncryptor() {
if (publicKey != null) {
return new RSAPrivacyEncryptor(publicKey, publicKeyId);
}
X509Certificate certificate = certificateProvider.getAvailableCertificate();
return new RSAPrivacyEncryptor(
certificate.getPublicKey(), PemUtil.getSerialNumber(certificate));
Expand All @@ -58,6 +84,9 @@ public Credential createCredential() {

@Override
public Validator createValidator() {
if (publicKey != null) {
return new WechatPay2Validator(new RSAVerifier(publicKey, publicKeyId));
}
return new WechatPay2Validator(new RSAVerifier(certificateProvider));
}

Expand Down
121 changes: 121 additions & 0 deletions core/src/main/java/com/wechat/pay/java/core/RSAPublicKeyConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.wechat.pay.java.core;

import static com.wechat.pay.java.core.notification.Constant.AES_CIPHER_ALGORITHM;
import static com.wechat.pay.java.core.notification.Constant.RSA_SIGN_TYPE;
import static java.util.Objects.requireNonNull;

import com.wechat.pay.java.core.cipher.AeadAesCipher;
import com.wechat.pay.java.core.cipher.AeadCipher;
import com.wechat.pay.java.core.cipher.RSAVerifier;
import com.wechat.pay.java.core.cipher.Verifier;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.util.PemUtil;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;

/** 使用微信支付平台公钥的RSA配置类。 每次构造都要求传入平台公钥以及平台公钥id,如果使用平台证书建议用RSAAutoCertificateConfig类 */
public final class RSAPublicKeyConfig extends AbstractRSAConfig implements NotificationConfig {

private final PublicKey publicKey;
private final AeadCipher aeadCipher;
private final String publicKeyId;

private RSAPublicKeyConfig(Builder builder) {
super(
builder.merchantId,
builder.privateKey,
builder.merchantSerialNumber,
builder.publicKey,
builder.publicKeyId);
this.publicKey = builder.publicKey;
this.publicKeyId = builder.publicKeyId;
this.aeadCipher = new AeadAesCipher(builder.apiV3Key);
}

/**
* 获取签名类型
*
* @return 签名类型
*/
@Override
public String getSignType() {
return RSA_SIGN_TYPE;
}

/**
* 获取认证加解密器类型
*
* @return 认证加解密器类型
*/
@Override
public String getCipherType() {
return AES_CIPHER_ALGORITHM;
}

/**
* 创建验签器
*
* @return 验签器
*/
@Override
public Verifier createVerifier() {
return new RSAVerifier(publicKey, publicKeyId);
}

/**
* 创建认证加解密器
*
* @return 认证加解密器
*/
@Override
public AeadCipher createAeadCipher() {
return aeadCipher;
}

public static class Builder extends AbstractRSAConfigBuilder<Builder> {
protected byte[] apiV3Key;
protected PublicKey publicKey;
protected String publicKeyId;

public Builder apiV3Key(String apiV3Key) {
this.apiV3Key = apiV3Key.getBytes(StandardCharsets.UTF_8);
return self();
}

public Builder publicKey(String publicKey) {
this.publicKey = PemUtil.loadPublicKeyFromString(publicKey);
return self();
}

public Builder publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return self();
}

public Builder publicKeyFromPath(String publicKeyPath) {
this.publicKey = PemUtil.loadPublicKeyFromPath(publicKeyPath);
return self();
}

public Builder publicKeyId(String publicKeyId) {
this.publicKeyId = publicKeyId;
return self();
}

@Override
protected Builder self() {
return this;
}

public RSAPublicKeyConfig build() {
requireNonNull(merchantId);
requireNonNull(publicKey);
requireNonNull(publicKeyId);
requireNonNull(privateKey);
requireNonNull(apiV3Key);
requireNonNull(merchantSerialNumber);

return new RSAPublicKeyConfig(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.wechat.pay.java.core.http.HttpRequest;
import com.wechat.pay.java.core.http.HttpResponse;
import com.wechat.pay.java.core.http.MediaType;
import com.wechat.pay.java.core.util.PemUtil;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.Base64;
Expand Down Expand Up @@ -80,13 +79,7 @@ public Map<String, X509Certificate> download() {
HttpResponse<DownloadCertificateResponse> httpResponse =
httpClient.execute(httpRequest, DownloadCertificateResponse.class);

Map<String, X509Certificate> downloaded = decryptCertificate(httpResponse);
validateCertificate(downloaded);
return downloaded;
}

private void validateCertificate(Map<String, X509Certificate> certificates) {
certificates.forEach((serialNo, cert) -> certificateHandler.validateCertPath(cert));
return decryptCertificate(httpResponse);
}

/**
Expand All @@ -109,7 +102,7 @@ private Map<String, X509Certificate> decryptCertificate(
Base64.getDecoder().decode(encryptCertificate.getCiphertext()));

certificate = certificateHandler.generateCertificate(decryptCertificate);
downloadCertMap.put(PemUtil.getSerialNumber(certificate), certificate);
downloadCertMap.put(data.getSerialNo(), certificate);
}
return downloadCertMap;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface CertificateHandler {
X509Certificate generateCertificate(String certificate);

/**
* * 验证证书链
* * 验证证书链(不推荐验证,如果证书过期不及时更换会导致验证失败,从而影响业务)
*
* @param certificate 微信支付平台证书
* @throws com.wechat.pay.java.core.exception.ValidationException 证书验证失败
Expand Down
Original file line number Diff line number Diff line change
@@ -1,69 +1,17 @@
package com.wechat.pay.java.core.certificate;

import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.util.PemUtil;
import java.security.cert.*;
import java.util.*;

final class RSACertificateHandler implements CertificateHandler {

private static final X509Certificate tenpayCACert =
PemUtil.loadX509FromString(
"-----BEGIN CERTIFICATE-----\n"
+ "MIIEcDCCA1igAwIBAgIUG9QiDlDbwEsGrTl1SYRsAcPo69IwDQYJKoZIhvcNAQEL\n"
+ "BQAwcDELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM\n"
+ "E0NoaW5hIFRydXN0IE5ldHdvcmsxLjAsBgNVBAMMJWlUcnVzQ2hpbmEgQ2xhc3Mg\n"
+ "MiBFbnRlcnByaXNlIENBIC0gRzMwHhcNMTcwODA5MDkxNTU1WhcNMzIwODA5MDkx\n"
+ "NTU1WjBeMQswCQYDVQQGEwJDTjETMBEGA1UEChMKVGVucGF5LmNvbTEdMBsGA1UE\n"
+ "CxMUVGVucGF5LmNvbSBDQSBDZW50ZXIxGzAZBgNVBAMTElRlbnBheS5jb20gUm9v\n"
+ "dCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALvnPD6k39BdPYAH\n"
+ "+6lnWPjuHH+2pcmZUf2E8cNFQFNr+ECRZylYV2iKyItCQt3I2/7VIDZl6aR9TE7n\n"
+ "sZrtSmOXCw635QOrq2yF9LTSDotAhf3ER0+216w3age/VzGcNVQpTf6gRCHCuQIk\n"
+ "8pe/oh06JagGvX0wERa+I6NfuG58ZHQY9d6RqLXKQl0Up95v73HDsG487z8k6jcn\n"
+ "qpGngmHQxdWiWRJugqxNRUD+awv2/DUsqGOffPX4jzJ6rLSJSlQXvuniDYxmaiaD\n"
+ "cK0bUbB5aM+1zMwogoHSYxWj/6B+vgcnHQCUrwGdiQR5+F+yRWzy5bO09IzaFgeO\n"
+ "PNPLPOsCAwEAAaOCARIwggEOMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/\n"
+ "BAQDAgEGMCAGA1UdEQQZMBekFTATMREwDwYDVQQDDAhzd2JlLTI2NjAdBgNVHQ4E\n"
+ "FgQUTFo4GLdm9oHX52HcWnzuL4tui2gwHwYDVR0jBBgwFoAUK1vVxWgI69vN5LA5\n"
+ "MqJf/8dPmEUwRgYDVR0gBD8wPTA7BgoqgRyG7xcBAQECMC0wKwYIKwYBBQUHAgEW\n"
+ "H2h0dHBzOi8vd3d3Lml0cnVzLmNvbS5jbi9jdG5jcHMwPgYDVR0fBDcwNTAzoDGg\n"
+ "L4YtaHR0cDovL3RvcGNhLml0cnVzLmNvbS5jbi9jcmwvaXRydXNjMmNhZzMuY3Js\n"
+ "MA0GCSqGSIb3DQEBCwUAA4IBAQBwZhL/eiOQmMyo1D0IR9mu1DPWl5J3XXhjc4R6\n"
+ "mFgsN/FCeVP9M4U9y2FJH6i5Ha5YCecKGw5pwhA0rjZr/6okWwo22GF+nzI/gQiz\n"
+ "6ugAKs5VjFbeiEb04Ncz4HT8FP1idK3tyCjqCUTkLNt0U3tR7wy26hgOqlT2wCZ9\n"
+ "X4MfT8dUMdt9nCZx4ujN5yZOzaLOCHmzoGDGxgKg91bbu0TG2Yzd2ylhrxxRtFH9\n"
+ "aZ/J1x5UoF7uwhTM8P92DuAldWC1/bX1kciOtQvQEZeAy+9y/1BtFxoBnmDxnqkX\n"
+ "+lirIUYTLDaL7HaLrOLECUlaxZCU/Nkwm3tmqQxtCh+XQBdd\n"
+ "-----END CERTIFICATE-----");

private static final Set<TrustAnchor> trustAnchor =
new LinkedHashSet<>(Collections.singletonList(new TrustAnchor(tenpayCACert, null)));

@Override
public X509Certificate generateCertificate(String certificate) {
return PemUtil.loadX509FromString(certificate);
}

@Override
public void validateCertPath(X509Certificate certificate) {
try {
PKIXParameters params = new PKIXParameters(trustAnchor);
params.setRevocationEnabled(false);

List<X509Certificate> certs = new ArrayList<>();
certs.add(certificate);

CertificateFactory cf = CertificateFactory.getInstance("X.509");
CertPath certPath = cf.generateCertPath(certs);

CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, params);
} catch (Exception e) {
throw new ValidationException(
String.format(
"certificate[%s] validation failed: %s",
PemUtil.getSerialNumber(certificate), e.getMessage()),
e);
}
// 为防止证书过期导致验签失败,从而影响业务,后续不再验证证书信任链
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
Expand All @@ -17,7 +18,8 @@ public abstract class AbstractVerifier implements Verifier {

protected static final Logger logger = LoggerFactory.getLogger(AbstractVerifier.class);
protected final CertificateProvider certificateProvider;

protected final PublicKey publicKey;
protected final String publicKeyId;
protected final String algorithmName;

/**
Expand All @@ -29,6 +31,21 @@ public abstract class AbstractVerifier implements Verifier {
protected AbstractVerifier(String algorithmName, CertificateProvider certificateProvider) {
this.certificateProvider = requireNonNull(certificateProvider);
this.algorithmName = requireNonNull(algorithmName);
this.publicKey = null;
this.publicKeyId = null;
}

/**
* AbstractVerifier 构造函数
*
* @param algorithmName 获取Signature对象时指定的算法,例如SHA256withRSA
* @param publicKey 验签使用的微信支付平台公钥,非空
*/
protected AbstractVerifier(String algorithmName, PublicKey publicKey, String publicKeyId) {
this.publicKey = requireNonNull(publicKey);
this.publicKeyId = publicKeyId;
this.algorithmName = requireNonNull(algorithmName);
this.certificateProvider = null;
}

protected boolean verify(X509Certificate certificate, String message, String signature) {
Expand All @@ -47,8 +64,34 @@ protected boolean verify(X509Certificate certificate, String message, String sig
}
}

private boolean verify(String message, String signature) {
try {
Signature sign = Signature.getInstance(algorithmName);
sign.initVerify(publicKey);
sign.update(message.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(signature));
} catch (SignatureException e) {
return false;
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("verify uses an illegal publickey.", e);
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(
"The current Java environment does not support " + algorithmName, e);
}
}

@Override
public boolean verify(String serialNumber, String message, String signature) {
// 如果公钥不为空,使用公钥验签
if (publicKey != null) {
if (serialNumber.equals(publicKeyId)) {
return verify(message, signature);
}
logger.error("publicKeyId[{}] and serialNumber[{}] are not equal", publicKeyId, serialNumber);
return false;
}
// 使用证书验签
requireNonNull(certificateProvider);
X509Certificate certificate = certificateProvider.getCertificate(serialNumber);
if (certificate == null) {
logger.error(
Expand Down
Loading

0 comments on commit 167e9c4

Please sign in to comment.