Skip to content
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

回调和发包支持平台公钥验签 #264

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading