Skip to content

Commit fd349c4

Browse files
Copilotbinarywang
andcommitted
修复:代理转发场景下微信支付V3 API Authorization头丢失问题
Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com>
1 parent c8bd0aa commit fd349c4

File tree

5 files changed

+228
-3
lines changed

5 files changed

+228
-3
lines changed

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
import javax.net.ssl.SSLContext;
3434
import java.io.*;
35+
import java.net.URI;
36+
import java.net.URISyntaxException;
3537
import java.net.URL;
3638
import java.nio.charset.StandardCharsets;
3739
import java.security.KeyStore;
@@ -395,6 +397,19 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
395397
WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
396398
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
397399
.withValidator(new WxPayValidator(certificatesVerifier));
400+
// 当 apiHostUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
401+
// 确保 Authorization 头能正确发送到代理服务器
402+
String apiHostUrl = this.getApiHostUrl();
403+
if (StringUtils.isNotBlank(apiHostUrl)) {
404+
try {
405+
String host = new URI(apiHostUrl).getHost();
406+
if (host != null && !host.endsWith(".mch.weixin.qq.com")) {
407+
wxPayV3HttpClientBuilder.withTrustedHost(host);
408+
}
409+
} catch (URISyntaxException e) {
410+
log.warn("解析 apiHostUrl [{}] 中的主机名失败: {}", apiHostUrl, e.getMessage());
411+
}
412+
}
398413
//初始化V3接口正向代理设置
399414
HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, wxPayHttpProxy);
400415

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,27 @@
1515
import org.apache.http.util.EntityUtils;
1616

1717
import java.io.IOException;
18+
import java.util.Collections;
19+
import java.util.Set;
1820

1921
public class SignatureExec implements ClientExecChain {
2022
final ClientExecChain mainExec;
2123
final Credentials credentials;
2224
final Validator validator;
25+
/**
26+
* 额外受信任的主机列表,这些主机(如反向代理)也需要携带微信支付 Authorization 头
27+
*/
28+
final Set<String> trustedHosts;
2329

2430
SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) {
31+
this(credentials, validator, mainExec, Collections.emptySet());
32+
}
33+
34+
SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec, Set<String> trustedHosts) {
2535
this.credentials = credentials;
2636
this.validator = validator;
2737
this.mainExec = mainExec;
38+
this.trustedHosts = trustedHosts != null ? trustedHosts : Collections.emptySet();
2839
}
2940

3041
protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException {
@@ -56,7 +67,8 @@ protected void convertToRepeatableRequestEntity(HttpRequestWrapper request) thro
5667
public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
5768
HttpClientContext context, HttpExecutionAware execAware)
5869
throws IOException, HttpException {
59-
if (request.getURI().getHost() != null && request.getURI().getHost().endsWith(".mch.weixin.qq.com")) {
70+
String host = request.getURI().getHost();
71+
if (host != null && (host.endsWith(".mch.weixin.qq.com") || trustedHosts.contains(host))) {
6072
return executeWithSignature(route, request, context, execAware);
6173
} else {
6274
return mainExec.execute(route, request, context, execAware);

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33

44
import java.security.PrivateKey;
5+
import java.util.HashSet;
6+
import java.util.Set;
57

68
import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
79
import com.github.binarywang.wxpay.v3.auth.WxPayCredentials;
@@ -12,6 +14,10 @@
1214
public class WxPayV3HttpClientBuilder extends HttpClientBuilder {
1315
private Credentials credentials;
1416
private Validator validator;
17+
/**
18+
* 额外受信任的主机列表,用于代理转发场景:对这些主机的请求也会携带微信支付 Authorization 头
19+
*/
20+
private final Set<String> trustedHosts = new HashSet<>();
1521

1622
static final String OS = System.getProperty("os.name") + "/" + System.getProperty("os.version");
1723
static final String VERSION = System.getProperty("java.version");
@@ -47,6 +53,22 @@ public WxPayV3HttpClientBuilder withValidator(Validator validator) {
4753
return this;
4854
}
4955

56+
/**
57+
* 添加受信任的主机,对该主机的请求也会携带微信支付 Authorization 头.
58+
* 适用于通过反向代理(如 Nginx)转发微信支付 API 请求的场景,
59+
* 当 apiHostUrl 配置为代理地址时,需要将代理主机加入受信任列表,
60+
* 以确保 Authorization 头能正确传递到代理服务器。
61+
*
62+
* @param host 受信任的主机名(不含端口),例如 "proxy.company.com"
63+
* @return 当前 Builder 实例
64+
*/
65+
public WxPayV3HttpClientBuilder withTrustedHost(String host) {
66+
if (host != null && !host.isEmpty()) {
67+
this.trustedHosts.add(host);
68+
}
69+
return this;
70+
}
71+
5072
@Override
5173
public CloseableHttpClient build() {
5274
if (credentials == null) {
@@ -61,6 +83,6 @@ public CloseableHttpClient build() {
6183

6284
@Override
6385
protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) {
64-
return new SignatureExec(this.credentials, this.validator, requestExecutor);
86+
return new SignatureExec(this.credentials, this.validator, requestExecutor, this.trustedHosts);
6587
}
6688
}

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import java.io.ByteArrayInputStream;
2424
import java.io.IOException;
25+
import java.net.URI;
26+
import java.net.URISyntaxException;
2527
import java.nio.charset.StandardCharsets;
2628
import java.security.GeneralSecurityException;
2729
import java.security.cert.CertificateExpiredException;
@@ -154,8 +156,21 @@ private void autoUpdateCert() throws IOException, GeneralSecurityException {
154156
.withCredentials(credentials)
155157
.withValidator(verifier == null ? response -> true : new WxPayValidator(verifier));
156158

159+
// 当 payBaseUrl 配置为自定义代理地址时,将代理主机加入受信任列表,
160+
// 确保 Authorization 头能正确发送到代理服务器
161+
if (this.payBaseUrl != null && !this.payBaseUrl.isEmpty()) {
162+
try {
163+
String host = new URI(this.payBaseUrl).getHost();
164+
if (host != null && !host.endsWith(".mch.weixin.qq.com")) {
165+
wxPayV3HttpClientBuilder.withTrustedHost(host);
166+
}
167+
} catch (URISyntaxException e) {
168+
log.warn("解析 payBaseUrl [{}] 中的主机名失败: {}", this.payBaseUrl, e.getMessage());
169+
}
170+
}
171+
157172
//调用自定义扩展设置设置HTTP PROXY对象
158-
HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder,this.wxPayHttpProxy);
173+
HttpProxyUtils.initHttpProxy(wxPayV3HttpClientBuilder, this.wxPayHttpProxy);
159174

160175
//增加自定义扩展点,子类可以设置其他构造参数
161176
this.customHttpClientBuilder(wxPayV3HttpClientBuilder);
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package com.github.binarywang.wxpay.v3;
2+
3+
import org.apache.http.HttpException;
4+
import org.apache.http.ProtocolVersion;
5+
import org.apache.http.client.methods.CloseableHttpResponse;
6+
import org.apache.http.client.methods.HttpGet;
7+
import org.apache.http.client.methods.HttpRequestWrapper;
8+
import org.apache.http.client.protocol.HttpClientContext;
9+
import org.apache.http.impl.execchain.ClientExecChain;
10+
import org.apache.http.message.BasicHttpResponse;
11+
import org.apache.http.message.BasicStatusLine;
12+
import org.testng.annotations.Test;
13+
14+
import java.io.IOException;
15+
import java.util.Collections;
16+
import java.util.HashSet;
17+
import java.util.Set;
18+
import java.util.concurrent.atomic.AtomicBoolean;
19+
20+
import static org.testng.Assert.*;
21+
22+
/**
23+
* 测试 SignatureExec 的受信任主机功能,确保在代理转发场景下正确添加 Authorization 头
24+
*
25+
* @author GitHub Copilot
26+
*/
27+
public class SignatureExecTrustedHostTest {
28+
29+
/**
30+
* 最简 CloseableHttpResponse 实现,仅用于单元测试
31+
*/
32+
private static class StubCloseableHttpResponse extends BasicHttpResponse implements CloseableHttpResponse {
33+
StubCloseableHttpResponse() {
34+
super(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
35+
}
36+
37+
@Override
38+
public void close() {
39+
}
40+
}
41+
42+
/**
43+
* 创建一个测试用的 Credentials,始终返回固定 schema 和 token
44+
*/
45+
private static Credentials createTestCredentials() {
46+
return new Credentials() {
47+
@Override
48+
public String getSchema() {
49+
return "WECHATPAY2-SHA256-RSA2048";
50+
}
51+
52+
@Override
53+
public String getToken(HttpRequestWrapper request) {
54+
return "test_token";
55+
}
56+
};
57+
}
58+
59+
/**
60+
* 创建一个 ClientExecChain,记录请求是否携带了 Authorization 头
61+
*/
62+
private static ClientExecChain trackingExec(AtomicBoolean authHeaderAdded) {
63+
return (route, request, context, execAware) -> {
64+
if (request.containsHeader("Authorization")) {
65+
authHeaderAdded.set(true);
66+
}
67+
return new StubCloseableHttpResponse();
68+
};
69+
}
70+
71+
/**
72+
* 测试:对微信官方主机(以 .mch.weixin.qq.com 结尾)的请求应该添加 Authorization 头
73+
*/
74+
@Test
75+
public void testWechatOfficialHostShouldAddAuthorizationHeader() throws IOException, HttpException {
76+
AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
77+
SignatureExec signatureExec = new SignatureExec(
78+
createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
79+
);
80+
81+
HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
82+
signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
83+
84+
assertTrue(authHeaderAdded.get(), "请求微信官方接口时应该添加 Authorization 头");
85+
}
86+
87+
/**
88+
* 测试:对非微信主机且不在受信任列表中的请求,不应该添加 Authorization 头
89+
*/
90+
@Test
91+
public void testUntrustedProxyHostShouldNotAddAuthorizationHeader() throws IOException, HttpException {
92+
AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
93+
SignatureExec signatureExec = new SignatureExec(
94+
createTestCredentials(), response -> true, trackingExec(authHeaderAdded), Collections.emptySet()
95+
);
96+
97+
HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
98+
signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
99+
100+
assertFalse(authHeaderAdded.get(), "不受信任的代理主机请求不应该添加 Authorization 头");
101+
}
102+
103+
/**
104+
* 测试:对在受信任列表中的代理主机请求,应该添加 Authorization 头.
105+
* 这是修复代理转发场景下 Authorization 头丢失问题的核心功能
106+
*/
107+
@Test
108+
public void testTrustedProxyHostShouldAddAuthorizationHeader() throws IOException, HttpException {
109+
AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
110+
Set<String> trustedHosts = new HashSet<>();
111+
trustedHosts.add("proxy.company.com");
112+
SignatureExec signatureExec = new SignatureExec(
113+
createTestCredentials(), response -> true, trackingExec(authHeaderAdded), trustedHosts
114+
);
115+
116+
HttpGet httpGet = new HttpGet("http://proxy.company.com:8080/v3/certificates");
117+
signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
118+
119+
assertTrue(authHeaderAdded.get(), "受信任的代理主机请求应该添加 Authorization 头");
120+
}
121+
122+
/**
123+
* 测试:WxPayV3HttpClientBuilder 的 withTrustedHost 方法应该正确设置受信任主机
124+
*/
125+
@Test
126+
public void testWithTrustedHostBuilderMethod() {
127+
WxPayV3HttpClientBuilder builder = WxPayV3HttpClientBuilder.create();
128+
// 方法应该支持链式调用
129+
WxPayV3HttpClientBuilder result = builder.withTrustedHost("proxy.company.com");
130+
assertSame(result, builder, "withTrustedHost 应该返回当前 Builder 实例(支持链式调用)");
131+
}
132+
133+
/**
134+
* 测试:withTrustedHost 传入空值不应该抛出异常
135+
*/
136+
@Test
137+
public void testWithTrustedHostNullOrEmptyShouldNotThrow() {
138+
WxPayV3HttpClientBuilder builder = WxPayV3HttpClientBuilder.create();
139+
// 传入 null 和空字符串不应该抛出异常
140+
builder.withTrustedHost(null);
141+
builder.withTrustedHost("");
142+
}
143+
144+
/**
145+
* 测试:SignatureExec 的旧构造函数(不带 trustedHosts)应该仍然有效
146+
*/
147+
@Test
148+
public void testBackwardCompatibilityWithOldConstructor() throws IOException, HttpException {
149+
AtomicBoolean authHeaderAdded = new AtomicBoolean(false);
150+
// 使用旧的三参数构造函数
151+
SignatureExec signatureExec = new SignatureExec(
152+
createTestCredentials(), response -> true, trackingExec(authHeaderAdded)
153+
);
154+
155+
// 微信官方主机仍然应该添加 Authorization 头
156+
HttpGet httpGet = new HttpGet("https://api.mch.weixin.qq.com/v3/certificates");
157+
signatureExec.execute(null, HttpRequestWrapper.wrap(httpGet), HttpClientContext.create(), null);
158+
159+
assertTrue(authHeaderAdded.get(), "使用旧构造函数时,请求微信官方接口仍应添加 Authorization 头");
160+
}
161+
}

0 commit comments

Comments
 (0)