diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java index 637d46e986..857b937d8e 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java @@ -6,26 +6,26 @@ import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder; import com.github.binarywang.wxpay.v3.auth.*; import com.github.binarywang.wxpay.v3.util.PemUtils; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.SneakyThrows; -import lombok.ToString; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RegExUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.ssl.SSLContexts; - -import javax.net.ssl.SSLContext; import java.io.*; import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Base64; import java.util.Optional; +import javax.net.ssl.SSLContext; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RegExUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.ssl.SSLContexts; /** * 微信支付配置 @@ -138,6 +138,25 @@ public class WxPayConfig { */ private byte[] privateCertContent; + /** + * 公钥ID + */ + private String publicKeyId; + + /** + * pub_key.pem证书base64编码 + */ + private String publicKeyString; + + /** + * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String publicKeyPath; + + /** + * pub_key.pem证书文件内容的字节数组. + */ + private byte[] publicKeyContent; /** * apiV3 秘钥值. */ @@ -241,7 +260,7 @@ public SSLContext initSSLContext() throws WxPayException { } try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(), - this.keyContent, "p12证书");) { + this.keyContent, "p12证书")) { KeyStore keystore = KeyStore.getInstance("PKCS12"); char[] partnerId2charArray = this.getMchId().toCharArray(); keystore.load(inputStream, partnerId2charArray); @@ -284,7 +303,6 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException { this.privateKeyContent, "privateKeyPath")) { merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream); } - } if (certificate == null && StringUtils.isBlank(this.getCertSerialNo())) { try (InputStream certInputStream = this.loadConfigInputStream(this.getPrivateCertString(), this.getPrivateCertPath(), @@ -293,13 +311,28 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException { } this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase(); } + PublicKey publicKey = null; + if (this.getPublicKeyString() != null || this.getPublicKeyPath() != null || this.publicKeyContent != null) { + try (InputStream pubInputStream = + this.loadConfigInputStream(this.getPublicKeyString(), this.getPublicKeyPath(), + this.publicKeyContent, "publicKeyPath")) { + publicKey = PemUtils.loadPublicKey(pubInputStream); + } + } //构造Http Proxy正向代理 WxPayHttpProxy wxPayHttpProxy = getWxPayHttpProxy(); - AutoUpdateCertificatesVerifier certificatesVerifier = new AutoUpdateCertificatesVerifier( - new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)), - this.getApiV3Key().getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime(), this.getPayBaseUrl(), wxPayHttpProxy); + Verifier certificatesVerifier; + if (publicKey == null) { + certificatesVerifier = + new AutoUpdateCertificatesVerifier( + new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)), + this.getApiV3Key().getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime(), + this.getPayBaseUrl(), wxPayHttpProxy); + } else { + certificatesVerifier = new PublicCertificateVerifier(publicKey, publicKeyId); + } WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create() .withMerchant(mchId, certSerialNo, merchantPrivateKey) @@ -422,7 +455,7 @@ private Object[] p12ToPem() { // 分解p12证书文件 try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(), - this.keyContent, "p12证书");) { + this.keyContent, "p12证书")) { KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(inputStream, key.toCharArray()); diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java new file mode 100644 index 0000000000..9344fc6f83 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java @@ -0,0 +1,39 @@ +package com.github.binarywang.wxpay.v3.auth; + +import java.security.*; +import java.security.cert.X509Certificate; +import java.util.Base64; +import me.chanjar.weixin.common.error.WxRuntimeException; + +public class PublicCertificateVerifier implements Verifier{ + + private final PublicKey publicKey; + + private final X509PublicCertificate publicCertificate; + + public PublicCertificateVerifier(PublicKey publicKey, String publicId) { + this.publicKey = publicKey; + this.publicCertificate = new X509PublicCertificate(publicKey, publicId); + } + + @Override + public boolean verify(String serialNumber, byte[] message, String signature) { + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initVerify(publicKey); + sign.update(message); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (NoSuchAlgorithmException e) { + throw new WxRuntimeException("当前Java环境不支持SHA256withRSA", e); + } catch (SignatureException e) { + throw new WxRuntimeException("签名验证过程发生了错误", e); + } catch (InvalidKeyException e) { + throw new WxRuntimeException("无效的证书", e); + } + } + + @Override + public X509Certificate getValidCertificate() { + return this.publicCertificate; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java new file mode 100644 index 0000000000..39d147c6ac --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java @@ -0,0 +1,150 @@ +package com.github.binarywang.wxpay.v3.auth; + +import java.math.BigInteger; +import java.security.*; +import java.security.cert.*; +import java.util.Collections; +import java.util.Date; +import java.util.Set; + +public class X509PublicCertificate extends X509Certificate { + + private final PublicKey publicKey; + + private final String publicId; + + public X509PublicCertificate(PublicKey publicKey, String publicId) { + this.publicKey = publicKey; + this.publicId = publicId; + } + + @Override + public PublicKey getPublicKey() { + return this.publicKey; + } + + @Override + public BigInteger getSerialNumber() { + return new BigInteger(publicId.replace("PUB_KEY_ID_", ""), 16); + } + + @Override + public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + } + + @Override + public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public Principal getIssuerDN() { + return null; + } + + @Override + public Principal getSubjectDN() { + return null; + } + + @Override + public Date getNotBefore() { + return null; + } + + @Override + public Date getNotAfter() { + return null; + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return new byte[0]; + } + + @Override + public byte[] getSignature() { + return new byte[0]; + } + + @Override + public String getSigAlgName() { + return ""; + } + + @Override + public String getSigAlgOID() { + return ""; + } + + @Override + public byte[] getSigAlgParams() { + return new byte[0]; + } + + @Override + public boolean[] getIssuerUniqueID() { + return new boolean[0]; + } + + @Override + public boolean[] getSubjectUniqueID() { + return new boolean[0]; + } + + @Override + public boolean[] getKeyUsage() { + return new boolean[0]; + } + + @Override + public int getBasicConstraints() { + return 0; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return new byte[0]; + } + + @Override + public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + public void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + public String toString() { + return ""; + } + + + @Override + public boolean hasUnsupportedCriticalExtension() { + return false; + } + + @Override + public Set getCriticalExtensionOIDs() { + return Collections.emptySet(); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return Collections.emptySet(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return new byte[0]; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java index 1983fb3387..a885ea0950 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java @@ -1,13 +1,12 @@ package com.github.binarywang.wxpay.v3.util; -import me.chanjar.weixin.common.error.WxRuntimeException; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; @@ -15,7 +14,9 @@ import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +import me.chanjar.weixin.common.error.WxRuntimeException; public class PemUtils { @@ -59,4 +60,28 @@ public static X509Certificate loadCertificate(InputStream inputStream) { throw new WxRuntimeException("无效的证书", e); } } + + public static PublicKey loadPublicKey(InputStream inputStream){ + try { + ByteArrayOutputStream array = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + array.write(buffer, 0, length); + } + + String publicKey = array.toString("utf-8") + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))); + } catch (NoSuchAlgorithmException e) { + throw new WxRuntimeException("当前Java环境不支持RSA", e); + } catch (InvalidKeySpecException e) { + throw new WxRuntimeException("无效的密钥格式"); + } catch (IOException e) { + throw new WxRuntimeException("无效的密钥"); + } + } } diff --git a/weixin-java-pay/src/test/resources/test-config.sample.xml b/weixin-java-pay/src/test/resources/test-config.sample.xml index a63cd8dc30..e9d383dd19 100644 --- a/weixin-java-pay/src/test/resources/test-config.sample.xml +++ b/weixin-java-pay/src/test/resources/test-config.sample.xml @@ -18,6 +18,7 @@ apiV3 证书序列号值 apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径. apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径. + pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径. 某个openId