🎨 #3968【微信支付】修复微信支付api-host-url配置反向代理路径前缀时会导致v3签名异常的问题
This commit is contained in:
@@ -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:开头的类路径
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -113,6 +113,12 @@ public class WxPayProperties {
|
||||
*/
|
||||
private String apiHostUrl;
|
||||
|
||||
/**
|
||||
* 自定义API主机路径前缀(用于代理入口前缀)
|
||||
* 例如:/api-weixin
|
||||
*/
|
||||
private String apiHostUrlPath;
|
||||
|
||||
/**
|
||||
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
|
||||
*/
|
||||
|
||||
@@ -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 | 无 |
|
||||
|
||||
@@ -112,6 +112,12 @@ public class WxPaySingleProperties implements Serializable {
|
||||
*/
|
||||
private String apiHostUrl;
|
||||
|
||||
/**
|
||||
* 自定义API主机路径前缀(用于代理入口前缀).
|
||||
* 例如:/api-weixin
|
||||
*/
|
||||
private String apiHostUrlPath;
|
||||
|
||||
/**
|
||||
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加.
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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:开头的类路径
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -111,6 +111,12 @@ public class WxPayProperties {
|
||||
*/
|
||||
private String apiHostUrl;
|
||||
|
||||
/**
|
||||
* 自定义API主机路径前缀(用于代理入口前缀)
|
||||
* 例如:/api-weixin
|
||||
*/
|
||||
private String apiHostUrlPath;
|
||||
|
||||
/**
|
||||
* 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 配置为自定义代理地址时,将代理主机加入受信任列表,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user