1
0
mirror of synced 2026-05-20 09:16:25 +08:00

🎨 #3968【微信支付】修复微信支付api-host-url配置反向代理路径前缀时会导致v3签名异常的问题

This commit is contained in:
水依寒
2026-05-11 20:32:42 +08:00
committed by GitHub
parent bcb3110bd7
commit 24703be583
16 changed files with 164 additions and 9 deletions

View File

@@ -23,6 +23,8 @@ wx:
pay:
appId: xxxxxxxxxxx
mchId: 15xxxxxxxxx #商户id
apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机
apiHostUrlPath: /api-weixin # 可选:代理入口前缀
apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥
certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径

View File

@@ -59,6 +59,7 @@ public class WxPayAutoConfiguration {
payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath()));
payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel());

View File

@@ -113,6 +113,12 @@ public class WxPayProperties {
*/
private String apiHostUrl;
/**
* 自定义API主机路径前缀用于代理入口前缀
* 例如:/api-weixin
*/
private String apiHostUrlPath;
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头默认添加
*/

View File

@@ -255,6 +255,7 @@ public class PayService {
| payScorePermissionNotifyUrl | 支付分授权回调地址 | 无 |
| useSandboxEnv | 是否使用沙箱环境 | false |
| apiHostUrl | 自定义API主机地址 | https://api.mch.weixin.qq.com |
| apiHostUrlPath | 自定义API主机路径前缀代理入口前缀 | 空 |
| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | true |
| fullPublicKeyModel | 是否完全使用公钥模式 | true |
| publicKeyId | 公钥ID | 无 |

View File

@@ -112,6 +112,12 @@ public class WxPaySingleProperties implements Serializable {
*/
private String apiHostUrl;
/**
* 自定义API主机路径前缀用于代理入口前缀.
* 例如:/api-weixin
*/
private String apiHostUrlPath;
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头默认添加.
*/

View File

@@ -83,6 +83,7 @@ public class WxPayMultiServicesImpl implements WxPayMultiServices {
payConfig.setPublicKeyId(StringUtils.trimToNull(properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
payConfig.setApiHostUrlPath(StringUtils.trimToNull(properties.getApiHostUrlPath()));
payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel());

View File

@@ -26,6 +26,8 @@ import static org.junit.jupiter.api.Assertions.*;
"wx.pay.configs.app1.notify-url=https://example.com/pay/notify",
"wx.pay.configs.app2.app-id=wx2222222222222222",
"wx.pay.configs.app2.mch-id=2222222222",
"wx.pay.configs.app2.api-host-url=http://10.0.0.1:3128",
"wx.pay.configs.app2.api-host-url-path=/api-weixin",
"wx.pay.configs.app2.apiv3-key=22222222222222222222222222222222",
"wx.pay.configs.app2.cert-serial-no=2222222222222222",
"wx.pay.configs.app2.private-key-path=classpath:cert/apiclient_key.pem",
@@ -57,7 +59,9 @@ public class WxPayMultiServicesTest {
assertNotNull(app2Config, "app2 configuration should exist");
assertEquals("wx2222222222222222", app2Config.getAppId());
assertEquals("2222222222", app2Config.getMchId());
assertEquals("22222222222222222222222222222222", app2Config.getApiV3Key());
assertEquals("http://10.0.0.1:3128", app2Config.getApiHostUrl());
assertEquals("/api-weixin", app2Config.getApiHostUrlPath());
assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key());
}
@Test
@@ -71,6 +75,7 @@ public class WxPayMultiServicesTest {
assertNotNull(app2Service, "Should get WxPayService for app2");
assertEquals("wx2222222222222222", app2Service.getConfig().getAppId());
assertEquals("2222222222", app2Service.getConfig().getMchId());
assertEquals("/api-weixin", app2Service.getConfig().getApiHostUrlPath());
// 测试相同key返回相同实例
WxPayService app1ServiceAgain = wxPayMultiServices.getWxPayService("app1");

View File

@@ -23,6 +23,8 @@ wx:
pay:
appId: xxxxxxxxxxx
mchId: 15xxxxxxxxx #商户id
apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机
apiHostUrlPath: /api-weixin # 可选:代理入口前缀
apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥
certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx
privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径

View File

@@ -63,6 +63,7 @@ public class WxPayAutoConfiguration {
payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath()));
payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial());
payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel());

View File

@@ -111,6 +111,12 @@ public class WxPayProperties {
*/
private String apiHostUrl;
/**
* 自定义API主机路径前缀用于代理入口前缀
* 例如:/api-weixin
*/
private String apiHostUrlPath;
/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头默认添加
*/

View File

@@ -6,6 +6,8 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
@@ -118,8 +120,19 @@ class VerifierBuilder {
String certSerialNo, String mchId, String apiV3Key, PrivateKey merchantPrivateKey,
WxPayHttpProxy wxPayHttpProxy, int certAutoUpdateTime, String payBaseUrl
) {
String signUriStripPrefix = null;
if (StringUtils.isNotBlank(payBaseUrl)) {
try {
String rawPath = new URI(payBaseUrl).getRawPath();
if (StringUtils.isNotBlank(rawPath) && !"/".equals(rawPath)) {
signUriStripPrefix = rawPath;
}
} catch (URISyntaxException ignored) {
// ignore
}
}
return new AutoUpdateCertificatesVerifier(
new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey), signUriStripPrefix),
apiV3Key.getBytes(StandardCharsets.UTF_8), certAutoUpdateTime,
payBaseUrl, wxPayHttpProxy);
}

View File

@@ -64,6 +64,12 @@ public class WxPayConfig {
*/
private String apiHostUrl = DEFAULT_PAY_BASE_URL;
/**
* 微信支付接口请求地址路径前缀(用于网关代理前缀).
* 例如:/api-weixin
*/
private String apiHostUrlPath;
/**
* http请求连接超时时间.
*/
@@ -285,11 +291,42 @@ public class WxPayConfig {
* @return 微信支付接口请求地址域名
*/
public String getApiHostUrl() {
if (StringUtils.isEmpty(this.apiHostUrl)) {
String hostUrl = StringUtils.trimToNull(this.apiHostUrl);
if (hostUrl == null) {
return DEFAULT_PAY_BASE_URL;
}
if (hostUrl.endsWith("/")) {
hostUrl = hostUrl.substring(0, hostUrl.length() - 1);
}
return hostUrl;
}
return this.apiHostUrl;
/**
* 返回所设置的微信支付接口路径前缀.
*
* @return 路径前缀,不配置时为空字符串
*/
public String getApiHostUrlPath() {
String pathPrefix = StringUtils.trimToNull(this.apiHostUrlPath);
if (pathPrefix == null || "/".equals(pathPrefix)) {
return "";
}
if (!pathPrefix.startsWith("/")) {
pathPrefix = "/" + pathPrefix;
}
if (pathPrefix.endsWith("/")) {
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
}
return pathPrefix;
}
/**
* 返回用于请求层拼接的基础地址host + pathPrefix.
*
* @return 拼接后的基础地址
*/
public String getApiHostWithPathPrefix() {
return this.getApiHostUrl() + this.getApiHostUrlPath();
}
@SneakyThrows
@@ -391,10 +428,11 @@ public class WxPayConfig {
} else {
certificatesVerifier = VerifierBuilder.build(
this.getCertSerialNo(), this.getMchId(), this.getApiV3Key(), merchantPrivateKey, wxPayHttpProxy,
this.getCertAutoUpdateTime(), this.getApiHostUrl(), this.getPublicKeyId(), publicKey);
this.getCertAutoUpdateTime(), this.getApiHostWithPathPrefix(), this.getPublicKeyId(), publicKey);
}
WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
.withSignUriStripPrefix(this.getApiHostUrlPath())
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withValidator(new WxPayValidator(certificatesVerifier));
// 当 apiHostUrl 配置为自定义代理地址时,将代理主机加入受信任列表,

View File

@@ -366,9 +366,9 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
if (StringUtils.isNotBlank(this.getConfig().getApiV3Key())) {
throw new WxRuntimeException("微信支付V3 目前不支持沙箱模式!");
}
return this.getConfig().getApiHostUrl() + "/xdc/apiv2sandbox";
return this.getConfig().getApiHostWithPathPrefix() + "/xdc/apiv2sandbox";
}
return this.getConfig().getApiHostUrl();
return this.getConfig().getApiHostWithPathPrefix();
}
@Override

View File

@@ -15,6 +15,10 @@ import org.apache.http.impl.execchain.ClientExecChain;
public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
private Credentials credentials;
private Validator validator;
/**
* 签名前从请求 URI Path 中移除的前缀(用于带路径前缀的代理场景)
*/
private String signUriStripPrefix;
/**
* 额外受信任的主机列表,用于代理转发场景:对这些主机的请求也会携带微信支付 Authorization 头
*/
@@ -40,12 +44,30 @@ public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
public WxPayV3HttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) {
this.credentials =
new WxPayCredentials(merchantId, new PrivateKeySigner(serialNo, privateKey));
new WxPayCredentials(merchantId, new PrivateKeySigner(serialNo, privateKey), this.signUriStripPrefix);
return this;
}
public WxPayV3HttpClientBuilder withCredentials(Credentials credentials) {
this.credentials = credentials;
if (this.credentials instanceof WxPayCredentials) {
((WxPayCredentials) this.credentials).setSignUriStripPrefix(this.signUriStripPrefix);
}
return this;
}
/**
* 配置签名前需要移除的 URI Path 前缀.
* 例如设置为 "/api-weixin" 时,签名串中的 Path 会从 "/api-weixin/v3/..." 调整为 "/v3/..."。
*
* @param signUriStripPrefix 需要移除的前缀
* @return 当前 Builder 实例
*/
public WxPayV3HttpClientBuilder withSignUriStripPrefix(String signUriStripPrefix) {
this.signUriStripPrefix = signUriStripPrefix;
if (this.credentials instanceof WxPayCredentials) {
((WxPayCredentials) this.credentials).setSignUriStripPrefix(signUriStripPrefix);
}
return this;
}

View File

@@ -20,16 +20,42 @@ public class WxPayCredentials implements Credentials {
private static final SecureRandom RANDOM = new SecureRandom();
protected String merchantId;
protected Signer signer;
/**
* 签名前从 URI Path 中移除的前缀(用于带路径前缀的反向代理场景)
* 例如配置为 "/api-weixin" 时,"/api-weixin/v3/pay/..." 将参与签名为 "/v3/pay/..."
*/
protected String signUriStripPrefix;
public WxPayCredentials(String merchantId, Signer signer) {
this.merchantId = merchantId;
this.signer = signer;
}
public WxPayCredentials(String merchantId, Signer signer, String signUriStripPrefix) {
this.merchantId = merchantId;
this.signer = signer;
this.setSignUriStripPrefix(signUriStripPrefix);
}
public String getMerchantId() {
return merchantId;
}
public void setSignUriStripPrefix(String signUriStripPrefix) {
if (signUriStripPrefix == null || signUriStripPrefix.trim().isEmpty()) {
this.signUriStripPrefix = null;
return;
}
String normalized = signUriStripPrefix.trim();
if (!normalized.startsWith("/")) {
normalized = "/" + normalized;
}
if (normalized.length() > 1 && normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
this.signUriStripPrefix = normalized;
}
protected long generateTimestamp() {
return System.currentTimeMillis() / 1000;
}
@@ -70,7 +96,7 @@ public class WxPayCredentials implements Credentials {
protected final String buildMessage(String nonce, long timestamp, HttpRequestWrapper request)
throws IOException {
URI uri = request.getURI();
String canonicalUrl = uri.getRawPath();
String canonicalUrl = stripPathPrefix(uri.getRawPath());
if (uri.getQuery() != null) {
canonicalUrl += "?" + uri.getRawQuery();
}
@@ -90,4 +116,18 @@ public class WxPayCredentials implements Credentials {
+ body + "\n";
}
private String stripPathPrefix(String rawPath) {
if (rawPath == null || rawPath.isEmpty() || signUriStripPrefix == null) {
return rawPath;
}
if (!rawPath.startsWith(signUriStripPrefix)) {
return rawPath;
}
String stripped = rawPath.substring(signUriStripPrefix.length());
if (stripped.isEmpty()) {
return "/";
}
return stripped.startsWith("/") ? stripped : "/" + stripped;
}
}

View File

@@ -2,6 +2,8 @@ package com.github.binarywang.wxpay.config;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
/**
* <pre>
* Created by BinaryWang on 2017/6/18.
@@ -38,6 +40,15 @@ public class WxPayConfigTest {
payConfig.hashCode();
}
@Test
public void testApiHostUrlPath() {
payConfig.setApiHostUrl("http://10.0.0.1:3128/");
payConfig.setApiHostUrlPath("api-weixin/");
assertEquals(payConfig.getApiHostUrl(), "http://10.0.0.1:3128");
assertEquals(payConfig.getApiHostUrlPath(), "/api-weixin");
assertEquals(payConfig.getApiHostWithPathPrefix(), "http://10.0.0.1:3128/api-weixin");
}
@Test
public void testInitSSLContext_base64() throws Exception {
payConfig.setMchId("123");