mirror of
https://github.com/dromara/payment-spring-boot.git
synced 2026-03-13 21:33:41 +08:00
✨ 微信支付响应验签增加 微信公钥验签方式
This commit is contained in:
@@ -22,11 +22,11 @@
|
||||
<parent>
|
||||
<groupId>cn.felord</groupId>
|
||||
<artifactId>payment-spring-boot</artifactId>
|
||||
<version>1.0.20.RELEASE</version>
|
||||
<version>1.0.21.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>payment-spring-boot-autoconfigure</artifactId>
|
||||
<version>1.0.20.RELEASE</version>
|
||||
<version>1.0.21.RELEASE</version>
|
||||
<packaging>jar</packaging>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,5 +76,26 @@ public class WechatPayProperties {
|
||||
* your pay server domain
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* wechat pay public key id
|
||||
*/
|
||||
private String weChatPayPublicKeyId;
|
||||
|
||||
/**
|
||||
* see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012154180#4.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">
|
||||
* </a>
|
||||
* wechat pay public key
|
||||
*/
|
||||
private String weChatPayPublicKeyPath;
|
||||
|
||||
/**
|
||||
*
|
||||
* see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012154180">
|
||||
* Indicate whether to switch from the platform certificate to the WeChat Pay public key
|
||||
* </a>
|
||||
*/
|
||||
private Boolean switchVerifySignMethod = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<X509WechatCertificateInfo> CERTIFICATE_SET = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private static final Set<WeChatPublicKeyInfo> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,6 @@ public class WechatMetaBean {
|
||||
*/
|
||||
private WechatPayProperties.V3 v3;
|
||||
|
||||
private WeChatPublicKeyInfo publicKeyInfo;
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</parent>
|
||||
|
||||
<artifactId>payment-spring-boot-starter</artifactId>
|
||||
<version>1.0.20.RELEASE</version>
|
||||
<version>1.0.21.RELEASE</version>
|
||||
<packaging>jar</packaging>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
2
pom.xml
2
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">
|
||||
<groupId>cn.felord</groupId>
|
||||
<artifactId>payment-spring-boot</artifactId>
|
||||
<version>1.0.20.RELEASE</version>
|
||||
<version>1.0.21.RELEASE</version>
|
||||
<packaging>pom</packaging>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user