From 7388a7545572a45365ed02ac02a8d93b7778d4f2 Mon Sep 17 00:00:00 2001 From: xucun Date: Wed, 11 Jun 2025 17:09:53 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=BE=AE=E4=BF=A1=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=93=8D=E5=BA=94=E9=AA=8C=E7=AD=BE=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20=E5=BE=AE=E4=BF=A1=E5=85=AC=E9=92=A5=E9=AA=8C=E7=AD=BE?= =?UTF-8?q?=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- payment-spring-boot-autoconfigure/pom.xml | 4 +- .../wechat/InMemoryWechatTenantService.java | 32 +++++++- .../payment/wechat/WechatPayProperties.java | 21 ++++++ .../payment/wechat/v3/SignatureProvider.java | 73 ++++++++++++++++++- .../wechat/v3/WeChatPublicKeyInfo.java | 26 +++++++ .../payment/wechat/v3/WechatMetaBean.java | 2 + .../payment/wechat/v3/WechatPayClient.java | 7 ++ payment-spring-boot-starter/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WeChatPublicKeyInfo.java diff --git a/payment-spring-boot-autoconfigure/pom.xml b/payment-spring-boot-autoconfigure/pom.xml index 86214da..3009ace 100644 --- a/payment-spring-boot-autoconfigure/pom.xml +++ b/payment-spring-boot-autoconfigure/pom.xml @@ -22,11 +22,11 @@ cn.felord payment-spring-boot - 1.0.20.RELEASE + 1.0.21.RELEASE payment-spring-boot-autoconfigure - 1.0.20.RELEASE + 1.0.21.RELEASE jar 4.0.0 diff --git a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/InMemoryWechatTenantService.java b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/InMemoryWechatTenantService.java index f0de581..2b361b5 100644 --- a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/InMemoryWechatTenantService.java +++ b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/InMemoryWechatTenantService.java @@ -17,15 +17,22 @@ package cn.felord.payment.wechat; -import cn.felord.payment.wechat.v3.KeyPairFactory; -import cn.felord.payment.wechat.v3.WechatMetaBean; +import cn.felord.payment.PayException; +import cn.felord.payment.wechat.v3.*; import lombok.AllArgsConstructor; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.CollectionUtils; import org.springframework.util.ResourceUtils; +import java.io.FileReader; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -62,6 +69,7 @@ public class InMemoryWechatTenantService implements WechatTenantService { WechatMetaBean wechatMetaBean = keyPairFactory.initWechatMetaBean(resource, mchId); wechatMetaBean.setV3(v3); wechatMetaBean.setTenantId(tenantId); + SignatureProvider.addWeChatPublicKey(initWeChatPublicKeyInfo(wechatMetaBean)); return wechatMetaBean; }) .collect(Collectors.toSet()); @@ -69,4 +77,24 @@ public class InMemoryWechatTenantService implements WechatTenantService { } return cache; } + + private WeChatPublicKeyInfo initWeChatPublicKeyInfo(WechatMetaBean meta) { + try { + String certPath=meta.getV3().getWeChatPayPublicKeyPath(); + Resource resource = + resourceLoader.getResource(certPath == null ? "classpath:wechat/pub_key.pem" : + certPath.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX) ? certPath : ResourceUtils.CLASSPATH_URL_PREFIX + certPath); + PemReader pemReader = new PemReader(new InputStreamReader(resource.getInputStream())); + PemObject pemObject = pemReader.readPemObject(); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pemObject.getContent()); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec); + // 生成公钥 + WeChatPublicKeyInfo keyInfo = new WeChatPublicKeyInfo(publicKey, meta.getV3().getWeChatPayPublicKeyId(), meta.getTenantId()); + return keyInfo; + }catch (Exception e){ + e.printStackTrace(); + throw new PayException("An error occurred while generating the public key,Please check the format and content of the configured public key"); + } + } } diff --git a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/WechatPayProperties.java b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/WechatPayProperties.java index 05de2d9..2d60692 100644 --- a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/WechatPayProperties.java +++ b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/WechatPayProperties.java @@ -76,5 +76,26 @@ public class WechatPayProperties { * your pay server domain */ private String domain; + + /** + * wechat pay public key id + */ + private String weChatPayPublicKeyId; + + /** + * see + * + * wechat pay public key + */ + private String weChatPayPublicKeyPath; + + /** + * + * see + * Indicate whether to switch from the platform certificate to the WeChat Pay public key + * + */ + private Boolean switchVerifySignMethod = false; + } } diff --git a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/SignatureProvider.java b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/SignatureProvider.java index b949cd6..9b1c573 100644 --- a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/SignatureProvider.java +++ b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/SignatureProvider.java @@ -27,13 +27,15 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.http.*; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; -import org.springframework.util.AlternativeJdkIdGenerator; -import org.springframework.util.Assert; -import org.springframework.util.Base64Utils; -import org.springframework.util.IdGenerator; +import org.springframework.util.*; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; @@ -44,12 +46,16 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileReader; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -85,6 +91,11 @@ public class SignatureProvider { * 微信平台证书容器 key = 序列号 value = 证书对象 */ private static final Set CERTIFICATE_SET = Collections.synchronizedSet(new HashSet<>()); + + private static final Set PUBLIC_KEY_SET = Collections.synchronizedSet(new HashSet<>()); + + private static final String PUBLIC_KYE_ID_PREFIX = "PUB_KEY_ID"; + /** * 加密算法提供方 - BouncyCastle */ @@ -117,6 +128,10 @@ public class SignatureProvider { wechatMetaContainer.getTenantIds().forEach(this::refreshCertificate); } + public static void addWeChatPublicKey(WeChatPublicKeyInfo weChatPublicKeyInfo) { + PUBLIC_KEY_SET.add(weChatPublicKeyInfo); + } + /** * 我方请求前用 SHA256withRSA 加签,使用API证书. @@ -172,7 +187,20 @@ public class SignatureProvider { */ public boolean responseSignVerify(ResponseSignVerifyParams params) { + log.debug("responseSignVerify: {}", params); + log.debug("wechatpaySerial: {}", params.getWechatpaySerial()); + + boolean verifyResult= params.getWechatpaySerial().startsWith(PUBLIC_KYE_ID_PREFIX)? + responseSignVerifyWithWeChatPublicKeyInfo(params): + responseSignVerifyWithX509WechatCertificate(params); + log.debug("responseSignVerify: {}", verifyResult); + return verifyResult; + } + + private boolean responseSignVerifyWithX509WechatCertificate(ResponseSignVerifyParams params){ + log.debug("responseSignVerifyWithX509WechatCertificate: {}", params); String wechatpaySerial = params.getWechatpaySerial(); + X509WechatCertificateInfo certificate = CERTIFICATE_SET.stream() .filter(cert -> Objects.equals(wechatpaySerial, cert.getWechatPaySerial())) .findAny() @@ -195,6 +223,31 @@ public class SignatureProvider { } } + private boolean responseSignVerifyWithWeChatPublicKeyInfo(ResponseSignVerifyParams params){ + log.debug("responseSignVerifyWithWeChatPublicKeyInfo: {}", params); + + String wechatpaySerial = params.getWechatpaySerial(); + + log.debug("wechatpaySerial: {}", wechatpaySerial); + + if (wechatpaySerial.startsWith(PUBLIC_KYE_ID_PREFIX)){ + WeChatPublicKeyInfo info = PUBLIC_KEY_SET.stream() + .filter(key -> Objects.equals(wechatpaySerial, key.getPublicKeyId())) + .findAny() + .orElseThrow(() -> new PayException("cannot obtain the public key")); + + try { + final String signatureStr = createSign(params.getWechatpayTimestamp(), params.getWechatpayNonce(), params.getBody()); + Signature signer = Signature.getInstance("SHA256withRSA", BC_PROVIDER); + signer.initVerify(info.getPublicKey()); + signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); + return signer.verify(Base64Utils.decodeFromString(params.getWechatpaySignature())); + } catch (Exception e) { + throw new PayException("An exception occurred during the response verification, the cause: " + e.getMessage()); + } + } + return false; + } /** * 当我方服务器不存在平台证书或者证书同当前响应报文中的证书序列号不一致时应当刷新 调用/v3/certificates @@ -395,4 +448,16 @@ public class SignatureProvider { .collect(Collectors.joining("\n", "", "\n")); } + public boolean isSwitchVerifySignMethod(String tenantId) { + + String publicKeyId=wechatMetaContainer.getWechatMeta(tenantId).getV3().getWeChatPayPublicKeyId(); + + Boolean switchVerifySignMethod = wechatMetaContainer.getWechatMeta(tenantId).getV3().getSwitchVerifySignMethod(); + + return switchVerifySignMethod && StringUtils.hasLength(publicKeyId); + } + + public String getWechatPublicKeyId(String tenantId) { + return wechatMetaContainer.getWechatMeta(tenantId).getV3().getWeChatPayPublicKeyId(); + } } diff --git a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WeChatPublicKeyInfo.java b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WeChatPublicKeyInfo.java new file mode 100644 index 0000000..128038d --- /dev/null +++ b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WeChatPublicKeyInfo.java @@ -0,0 +1,26 @@ +package cn.felord.payment.wechat.v3; + +import lombok.Data; + +import java.security.interfaces.RSAPublicKey; + +@Data +public class WeChatPublicKeyInfo { + + private RSAPublicKey publicKey; + + private String publicKeyId; + + private String tenantId; + + public WeChatPublicKeyInfo(RSAPublicKey publicKey, String publicKeyId, String tenantId) { + + this.publicKeyId = publicKeyId; + this.tenantId = tenantId; + this.publicKey = publicKey; + + } + + public WeChatPublicKeyInfo() { + } +} diff --git a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatMetaBean.java b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatMetaBean.java index 6e1cd8e..4ed15e5 100644 --- a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatMetaBean.java +++ b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatMetaBean.java @@ -46,5 +46,7 @@ public class WechatMetaBean { * The V3. */ private WechatPayProperties.V3 v3; + + private WeChatPublicKeyInfo publicKeyInfo; } diff --git a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatPayClient.java b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatPayClient.java index 6c987ba..1bc3f90 100644 --- a/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatPayClient.java +++ b/payment-spring-boot-autoconfigure/src/main/java/cn/felord/payment/wechat/v3/WechatPayClient.java @@ -224,6 +224,8 @@ public class WechatPayClient { String tenantId = Objects.requireNonNull(headers.get("Pay-TenantId")).get(0); String authorization = signatureProvider.requestSign(tenantId, httpMethod.name(), canonicalUrl, body); + + HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.addAll(headers); httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); @@ -232,6 +234,11 @@ public class WechatPayClient { // 避免出现因为中文导致的 HttpRetryException httpHeaders.setContentType(MediaType.parseMediaType("application/json;charset=UTF-8")); } + + if (signatureProvider.isSwitchVerifySignMethod(tenantId)){ + httpHeaders.add("Wechatpay-Serial", signatureProvider.getWechatPublicKeyId(tenantId)); + } + httpHeaders.add("Authorization", authorization); httpHeaders.add("User-Agent", "X-Pay-Service"); httpHeaders.remove("Meta-Info"); diff --git a/payment-spring-boot-starter/pom.xml b/payment-spring-boot-starter/pom.xml index 52620ab..92afff8 100644 --- a/payment-spring-boot-starter/pom.xml +++ b/payment-spring-boot-starter/pom.xml @@ -26,7 +26,7 @@ payment-spring-boot-starter - 1.0.20.RELEASE + 1.0.21.RELEASE jar 4.0.0 diff --git a/pom.xml b/pom.xml index 4c8b172..0a8671e 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> cn.felord payment-spring-boot - 1.0.20.RELEASE + 1.0.21.RELEASE pom 4.0.0