Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bbab553
:art: Bump org.bouncycastle:bcpkix-jdk18on & bcprov-jdk18on from 1.78…
m1ngyuan Jul 22, 2024
3d8e8e5
:art: #3337 【视频号小店】 订单详情字段补充、售后新特性补充
lizhengwu Jul 29, 2024
26ee4c4
:new: #3339 【企业微信】增加上传临时素材的重载方法
lyfuci Jul 30, 2024
764a8d7
:new: #3340【微信支付】增加直连商户付款码支付和撤销支付订单的V3版接口实现
xxm1995 Jul 30, 2024
5c1aacc
Merge branch 'Wechat-Group:develop' into develop
shuiyihan12 Aug 20, 2024
3413c05
Merge branch 'Wechat-Group:develop' into develop
shuiyihan12 Sep 28, 2024
b9c192e
Merge branch 'Wechat-Group:develop' into develop
shuiyihan12 Oct 22, 2024
ce6ab6b
Merge branch 'Wechat-Group:develop' into develop
shuiyihan12 Nov 1, 2024
c25ae64
Merge branch 'binarywang:develop' into develop
shuiyihan12 Nov 9, 2024
c835b1f
Merge branch 'binarywang:develop' into develop
shuiyihan12 Nov 20, 2024
50801eb
Merge branch 'binarywang:develop' into develop
shuiyihan12 Nov 29, 2024
6bb0f9b
Merge branch 'binarywang:develop' into develop
shuiyihan12 Mar 23, 2025
39a2702
Merge branch 'binarywang:develop' into develop
shuiyihan12 Apr 8, 2025
6ff8774
Merge branch 'binarywang:develop' into develop
shuiyihan12 May 14, 2025
d7ecfe3
Merge branch 'binarywang:develop' into develop
shuiyihan12 Jun 27, 2025
008c1c7
Merge branch 'binarywang:develop' into develop
shuiyihan12 Jul 9, 2025
4549a1b
Merge branch 'binarywang:develop' into develop
shuiyihan12 Sep 2, 2025
6e1285c
Merge branch 'binarywang:develop' into develop
shuiyihan12 Sep 11, 2025
0fe3994
Merge branch 'binarywang:develop' into develop
shuiyihan12 Sep 15, 2025
70915d2
Merge branch 'binarywang:develop' into develop
shuiyihan12 Sep 23, 2025
e5399fe
Merge branch 'binarywang:develop' into develop
shuiyihan12 Oct 20, 2025
f027505
Merge branch 'binarywang:develop' into develop
shuiyihan12 Nov 7, 2025
44cfeb3
Merge branch 'binarywang:develop' into develop
shuiyihan12 Dec 10, 2025
0a31386
Merge branch 'binarywang:develop' into develop
shuiyihan12 Mar 31, 2026
0195f14
Merge branch 'binarywang:develop' into develop
shuiyihan12 Apr 16, 2026
2cb4273
Merge branch 'binarywang:develop' into develop
shuiyihan12 Apr 22, 2026
58b1839
:art: 【微信支付】支持 apiHostUrlPath 代理前缀并修复 V3 签名路径在配置apiHostUrl字段有前缀时验签失败的问题
shuiyihan12 Apr 22, 2026
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
2 changes: 2 additions & 0 deletions solon-plugins/wx-java-pay-solon-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:开头的类路径
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public WxPayService wxPayService() {
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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public class WxPayProperties {
*/
private String apiHostUrl;

/**
* 自定义API主机路径前缀(用于代理入口前缀)
* 例如:/api-weixin
*/
private String apiHostUrlPath;

/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ public class PayService {
| payScorePermissionNotifyUrl | 支付分授权回调地址 | 无 |
| useSandboxEnv | 是否使用沙箱环境 | false |
| apiHostUrl | 自定义API主机地址 | https://api.mch.weixin.qq.com |
| apiHostUrlPath | 自定义API主机路径前缀(代理入口前缀) | 空 |
| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | false |
| fullPublicKeyModel | 是否完全使用公钥模式 | false |
| publicKeyId | 公钥ID | 无 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ public class WxPaySingleProperties implements Serializable {
*/
private String apiHostUrl;

/**
* 自定义API主机路径前缀(用于代理入口前缀).
* 例如:/api-weixin
*/
private String apiHostUrlPath;

/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ private WxPayService buildWxPayService(WxPaySingleProperties properties) {
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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"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",
Expand Down Expand Up @@ -57,6 +59,8 @@ public void testConfiguration() {
assertNotNull(app2Config, "app2 configuration should exist");
assertEquals("wx2222222222222222", app2Config.getAppId());
assertEquals("2222222222", app2Config.getMchId());
assertEquals("http://10.0.0.1:3128", app2Config.getApiHostUrl());
assertEquals("/api-weixin", app2Config.getApiHostUrlPath());
assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key());
}

Expand All @@ -71,6 +75,7 @@ public void testGetWxPayService() {
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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:开头的类路径
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public WxPayService wxPayService() {
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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ public class WxPayProperties {
*/
private String apiHostUrl;

/**
* 自定义API主机路径前缀(用于代理入口前缀)
* 例如:/api-weixin
*/
private String apiHostUrlPath;

/**
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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;
Expand Down Expand Up @@ -118,8 +120,19 @@ private static AutoUpdateCertificatesVerifier getCertificatesVerifier(
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public class WxPayConfig {
*/
private String apiHostUrl = DEFAULT_PAY_BASE_URL;

/**
* 微信支付接口请求地址路径前缀(用于网关代理前缀).
* 例如:/api-weixin
*/
private String apiHostUrlPath;

/**
* http请求连接超时时间.
*/
Expand Down Expand Up @@ -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 路径前缀,不配置时为空字符串
*/
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;
}

return this.apiHostUrl;
/**
* 返回用于请求层拼接的基础地址:host + pathPrefix.
*
* @return 拼接后的基础地址
*/
public String getApiHostWithPathPrefix() {
return this.getApiHostUrl() + this.getApiHostUrlPath();
}

@SneakyThrows
Expand Down Expand Up @@ -391,10 +428,11 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
} 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())
Copy link
Copy Markdown

@augmentcode augmentcode Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weixin-java-pay/.../WxPayConfig.java:435 If a user keeps the legacy style apiHostUrl containing a path prefix (e.g., http://host/api-weixin) but leaves apiHostUrlPath empty, withSignUriStripPrefix stays blank and V3 requests may still sign "/api-weixin/...", so signatures can still fail behind rewrite proxies.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withValidator(new WxPayValidator(certificatesVerifier));
// 当 apiHostUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,9 @@ public String getPayBaseUrl() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
private Credentials credentials;
private Validator validator;
/**
* 签名前从请求 URI Path 中移除的前缀(用于带路径前缀的代理场景)
*/
private String signUriStripPrefix;
/**
* 额外受信任的主机列表,用于代理转发场景:对这些主机的请求也会携带微信支付 Authorization 头
*/
Expand All @@ -40,12 +44,30 @@ public static WxPayV3HttpClientBuilder create() {

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -70,7 +96,7 @@ public final String getToken(HttpRequestWrapper request) throws IOException {
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();
}
Expand All @@ -90,4 +116,18 @@ protected final String buildMessage(String nonce, long timestamp, HttpRequestWra
+ body + "\n";
}

private String stripPathPrefix(String rawPath) {
if (rawPath == null || rawPath.isEmpty() || signUriStripPrefix == null) {
return rawPath;
}
if (!rawPath.startsWith(signUriStripPrefix)) {
Copy link
Copy Markdown

@augmentcode augmentcode Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weixin-java-pay/.../WxPayCredentials.java:123 stripPathPrefix uses rawPath.startsWith(signUriStripPrefix), which can strip unintended paths when the prefix is a partial segment (e.g., prefix /api also matches /api2/...), leading to incorrect canonical URLs and signature failures.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

return rawPath;
}
Comment on lines +120 to +125
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新增的签名 Path 前缀剥离逻辑目前缺少单元测试覆盖。建议至少补充边界 case:前缀等于整个 path、前缀后必须是/才剥离、以及不会误匹配到更长的同前缀路径(如"/api"不应匹配"/api-weixin"),以避免签名回归。

Suggested change
if (rawPath == null || rawPath.isEmpty() || signUriStripPrefix == null) {
return rawPath;
}
if (!rawPath.startsWith(signUriStripPrefix)) {
return rawPath;
}
if (rawPath == null || rawPath.isEmpty() || signUriStripPrefix == null || signUriStripPrefix.isEmpty()) {
return rawPath;
}
if (!rawPath.startsWith(signUriStripPrefix)) {
return rawPath;
}
if (rawPath.length() > signUriStripPrefix.length()
&& rawPath.charAt(signUriStripPrefix.length()) != '/') {
return rawPath;
}

Copilot uses AI. Check for mistakes.
String stripped = rawPath.substring(signUriStripPrefix.length());
Comment on lines +123 to +126
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stripPathPrefix 仅用 rawPath.startsWith(signUriStripPrefix) 判断是否需要剥离前缀,可能发生“部分前缀误匹配”:例如配置前缀为 "/api" 时,请求路径 "/api-weixin/v3/..." 也会被误剥离,导致签名串的 Path 变成"/-weixin/v3/..." 从而签名必然异常。建议改为仅在 rawPath 等于前缀或以 前缀 + "/" 开头时才剥离,避免跨 segment 的误匹配。

Suggested change
if (!rawPath.startsWith(signUriStripPrefix)) {
return rawPath;
}
String stripped = rawPath.substring(signUriStripPrefix.length());
String normalizedPrefix = signUriStripPrefix;
if (normalizedPrefix.length() > 1 && normalizedPrefix.endsWith("/")) {
normalizedPrefix = normalizedPrefix.substring(0, normalizedPrefix.length() - 1);
}
boolean exactMatch = rawPath.equals(normalizedPrefix);
boolean segmentMatch = rawPath.startsWith(normalizedPrefix + "/");
if (!exactMatch && !segmentMatch) {
return rawPath;
}
String stripped = rawPath.substring(normalizedPrefix.length());

Copilot uses AI. Check for mistakes.
if (stripped.isEmpty()) {
return "/";
}
return stripped.startsWith("/") ? stripped : "/" + stripped;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;

/**
* <pre>
* Created by BinaryWang on 2017/6/18.
Expand Down Expand Up @@ -38,6 +40,15 @@ public void testHashCode() {
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");
Expand Down