From d5f132aa85f3f8899f8e881c209550fe0d2d5ba3 Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Thu, 23 Jul 2026 11:42:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(channel):=20=E4=B8=BB=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E4=B8=8E=E9=80=9A=E9=81=93=E5=AD=90=E5=BA=94=E7=94=A8=20HTTP?= =?UTF-8?q?=20=E6=8A=A5=E6=96=87=20AES-GCM=20=E9=80=8F=E6=98=8E=E5=8A=A0?= =?UTF-8?q?=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 强制常开传输加密,按 one/two 分别配置密钥;出站 Interceptor 加解密,异常文案走 i18n。 Co-authored-by: Cursor --- .../adapay/config/AdapayClientConfig.java | 25 +--- .../alipay/config/AlipayClientConfig.java | 24 +--- .../dougong/config/DougongClientConfig.java | 23 +--- .../douyin/config/DouyinClientConfig.java | 23 +--- .../fuyou/config/FuyouClientConfig.java | 23 +--- .../channel/hkrt/config/HkrtClientConfig.java | 23 +--- .../hmpay/config/HmpayClientConfig.java | 25 +--- .../lakala/config/LakalaClientConfig.java | 23 +--- .../leshua/config/LeshuaClientConfig.java | 23 +--- .../channel/ums/config/UmsClientConfig.java | 23 +--- .../vbill/config/VbillClientConfig.java | 23 +--- .../wechat/config/WechatClientConfig.java | 24 +--- .../yeepay/config/YeepayClientConfig.java | 23 +--- .../encrypt/ChannelAesGcmEncryptor.java | 121 +++++++++++++++++ .../properties/DaxpayChannelProperties.java | 18 ++- .../encrypt/ChannelAesGcmEncryptorTest.java | 85 ++++++++++++ .../resources/i18n/en-US/channel/error.json | 12 +- .../resources/i18n/id-ID/channel/error.json | 12 +- .../resources/i18n/ja-JP/channel/error.json | 12 +- .../resources/i18n/ko-KR/channel/error.json | 12 +- .../resources/i18n/ms-MY/channel/error.json | 12 +- .../resources/i18n/th-TH/channel/error.json | 12 +- .../resources/i18n/vi-VN/channel/error.json | 12 +- .../resources/i18n/zh-CN/channel/error.json | 12 +- .../resources/i18n/zh-HK/channel/error.json | 12 +- .../resources/i18n/zh-TW/channel/error.json | 12 +- .../channel/ChannelRestClientSupport.java | 58 ++++++++ .../ChannelTransportEncryptInterceptor.java | 124 ++++++++++++++++++ .../platform/core/code/WebHeaderCode.java | 3 + .../src/main/resources/application-dev.yml | 15 ++- .../src/main/resources/application-prod.yml | 9 +- 31 files changed, 618 insertions(+), 240 deletions(-) create mode 100644 daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptor.java create mode 100644 daxpay-platform/daxpay-platform-common/common-config/src/test/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptorTest.java create mode 100644 daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelRestClientSupport.java create mode 100644 daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptor.java diff --git a/daxpay-channel/daxpay-channel-adapay/src/main/java/cn/daxpay/open/channel/adapay/config/AdapayClientConfig.java b/daxpay-channel/daxpay-channel-adapay/src/main/java/cn/daxpay/open/channel/adapay/config/AdapayClientConfig.java index cea6b1484..4d0b7f9e1 100644 --- a/daxpay-channel/daxpay-channel-adapay/src/main/java/cn/daxpay/open/channel/adapay/config/AdapayClientConfig.java +++ b/daxpay-channel/daxpay-channel-adapay/src/main/java/cn/daxpay/open/channel/adapay/config/AdapayClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.adapay.config; import cn.daxpay.open.channel.adapay.client.AdapayChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; -/// # Adapay 通道客户端配置 +/// # 汇付天下 Adapay 通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class AdapayClientConfig { @Bean public AdapayChannelClient adapayChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(AdapayChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), AdapayChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/config/AlipayClientConfig.java b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/config/AlipayClientConfig.java index b21301256..138a11fae 100644 --- a/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/config/AlipayClientConfig.java +++ b/daxpay-channel/daxpay-channel-alipay/src/main/java/cn/daxpay/open/channel/alipay/config/AlipayClientConfig.java @@ -2,33 +2,23 @@ package cn.daxpay.open.channel.alipay.config; import cn.daxpay.open.channel.alipay.client.AlipayChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; +/// # 支付宝通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-one baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-one baseUrl 并强制挂载传输加密。 @Configuration public class AlipayClientConfig { @Bean public AlipayChannelClient alipayChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getOne().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(AlipayChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getOne(), AlipayChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/config/DougongClientConfig.java b/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/config/DougongClientConfig.java index e7e61d7be..1c6b64a38 100644 --- a/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/config/DougongClientConfig.java +++ b/daxpay-channel/daxpay-channel-dougong/src/main/java/cn/daxpay/open/channel/dougong/config/DougongClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.dougong.config; import cn.daxpay.open.channel.dougong.client.DougongChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 斗拱通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取(斗拱部署在 channel-two 子应用)。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class DougongClientConfig { @Bean public DougongChannelClient dougongChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(DougongChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), DougongChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/config/DouyinClientConfig.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/config/DouyinClientConfig.java index d2fc788e1..e967094cc 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/config/DouyinClientConfig.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/config/DouyinClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.douyin.config; import cn.daxpay.open.channel.douyin.client.DouyinChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 抖音通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-one baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-one baseUrl 并强制挂载传输加密。 @Configuration public class DouyinClientConfig { @Bean public DouyinChannelClient douyinChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getOne().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(DouyinChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getOne(), DouyinChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/config/FuyouClientConfig.java b/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/config/FuyouClientConfig.java index 3560ea5ed..be28f97ac 100644 --- a/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/config/FuyouClientConfig.java +++ b/daxpay-channel/daxpay-channel-fuyou/src/main/java/cn/daxpay/open/channel/fuyou/config/FuyouClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.fuyou.config; import cn.daxpay.open.channel.fuyou.client.FuyouChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 富友通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class FuyouClientConfig { @Bean public FuyouChannelClient fuyouChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(FuyouChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), FuyouChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java index 6babdd41e..cd2e58cd9 100644 --- a/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java +++ b/daxpay-channel/daxpay-channel-hkrt/src/main/java/cn/daxpay/open/channel/hkrt/config/HkrtClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.hkrt.config; import cn.daxpay.open.channel.hkrt.client.HkrtChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 海科融通通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class HkrtClientConfig { @Bean public HkrtChannelClient hkrtChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(HkrtChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), HkrtChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-hmpay/src/main/java/cn/daxpay/open/channel/hmpay/config/HmpayClientConfig.java b/daxpay-channel/daxpay-channel-hmpay/src/main/java/cn/daxpay/open/channel/hmpay/config/HmpayClientConfig.java index e24b8dde6..f9f4994e4 100644 --- a/daxpay-channel/daxpay-channel-hmpay/src/main/java/cn/daxpay/open/channel/hmpay/config/HmpayClientConfig.java +++ b/daxpay-channel/daxpay-channel-hmpay/src/main/java/cn/daxpay/open/channel/hmpay/config/HmpayClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.hmpay.config; import cn.daxpay.open.channel.hmpay.client.HmpayChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; -/// # 河马付通道客户端配置 +/// # 河马支付通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取(河马付部署在 channel-two 子应用)。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class HmpayClientConfig { @Bean public HmpayChannelClient hmpayChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(HmpayChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), HmpayChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-lakala/src/main/java/cn/daxpay/open/channel/lakala/config/LakalaClientConfig.java b/daxpay-channel/daxpay-channel-lakala/src/main/java/cn/daxpay/open/channel/lakala/config/LakalaClientConfig.java index 99fee1974..733902868 100644 --- a/daxpay-channel/daxpay-channel-lakala/src/main/java/cn/daxpay/open/channel/lakala/config/LakalaClientConfig.java +++ b/daxpay-channel/daxpay-channel-lakala/src/main/java/cn/daxpay/open/channel/lakala/config/LakalaClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.lakala.config; import cn.daxpay.open.channel.lakala.client.LakalaChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 拉卡拉通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class LakalaClientConfig { @Bean public LakalaChannelClient lakalaChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(LakalaChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), LakalaChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-leshua/src/main/java/cn/daxpay/open/channel/leshua/config/LeshuaClientConfig.java b/daxpay-channel/daxpay-channel-leshua/src/main/java/cn/daxpay/open/channel/leshua/config/LeshuaClientConfig.java index 8c790fd5f..25d09b06b 100644 --- a/daxpay-channel/daxpay-channel-leshua/src/main/java/cn/daxpay/open/channel/leshua/config/LeshuaClientConfig.java +++ b/daxpay-channel/daxpay-channel-leshua/src/main/java/cn/daxpay/open/channel/leshua/config/LeshuaClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.leshua.config; import cn.daxpay.open.channel.leshua.client.LeshuaChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 乐刷通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class LeshuaClientConfig { @Bean public LeshuaChannelClient leshuaChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(LeshuaChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), LeshuaChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/config/UmsClientConfig.java b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/config/UmsClientConfig.java index 0be2d23a7..a41b03c16 100644 --- a/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/config/UmsClientConfig.java +++ b/daxpay-channel/daxpay-channel-ums/src/main/java/cn/daxpay/open/channel/ums/config/UmsClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.ums.config; import cn.daxpay.open.channel.ums.client.UmsChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 银联商务通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-one baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-one baseUrl 并强制挂载传输加密。 @Configuration public class UmsClientConfig { @Bean public UmsChannelClient umsChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getOne().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(UmsChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getOne(), UmsChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java index 44c11d61a..72ba33fb4 100644 --- a/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java +++ b/daxpay-channel/daxpay-channel-vbill/src/main/java/cn/daxpay/open/channel/vbill/config/VbillClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.vbill.config; import cn.daxpay.open.channel.vbill.client.VbillChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 随行付通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class VbillClientConfig { @Bean public VbillChannelClient vbillChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(VbillChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), VbillChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java index 5ed82c7f6..b15046031 100644 --- a/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java +++ b/daxpay-channel/daxpay-channel-wechat/src/main/java/cn/daxpay/open/channel/wechat/config/WechatClientConfig.java @@ -2,33 +2,23 @@ package cn.daxpay.open.channel.wechat.config; import cn.daxpay.open.channel.wechat.client.WechatChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; +/// # 微信通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-one baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-one baseUrl 并强制挂载传输加密。 @Configuration public class WechatClientConfig { @Bean public WechatChannelClient wechatChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getOne().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(WechatChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getOne(), WechatChannelClient.class); } } diff --git a/daxpay-channel/daxpay-channel-yeepay/src/main/java/cn/daxpay/open/channel/yeepay/config/YeepayClientConfig.java b/daxpay-channel/daxpay-channel-yeepay/src/main/java/cn/daxpay/open/channel/yeepay/config/YeepayClientConfig.java index 4450b5317..53b0c6c57 100644 --- a/daxpay-channel/daxpay-channel-yeepay/src/main/java/cn/daxpay/open/channel/yeepay/config/YeepayClientConfig.java +++ b/daxpay-channel/daxpay-channel-yeepay/src/main/java/cn/daxpay/open/channel/yeepay/config/YeepayClientConfig.java @@ -2,34 +2,23 @@ package cn.daxpay.open.channel.yeepay.config; import cn.daxpay.open.channel.yeepay.client.YeepayChannelClient; import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.common.spring.channel.ChannelRestClientSupport; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; -import org.springframework.web.client.support.RestClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; /// # 易宝通道客户端配置 /// -/// 复用全局 [RestClient](由 common-spring 的 RestClientConfiguration 提供, 已预装: -/// OTel traceparent 透传 / BusinessContextInterceptor 业务上下文透传 / Apache HttpClient5 连接池), -/// 经 `mutate()` 派生出绑定 channel-two baseUrl 的实例。 -/// -/// baseUrl 从 [DaxpayChannelProperties] 统一读取(易宝部署在子应用二)。 +/// 复用全局 RestClient,经 [ChannelRestClientSupport] 绑定 channel-two baseUrl 并强制挂载传输加密。 @Configuration public class YeepayClientConfig { @Bean public YeepayChannelClient yeepayChannelClient( RestClient restClient, - DaxpayChannelProperties channelProperties) { - // mutate 派生: 继承全局 RestClient 的拦截器 / observation / requestFactory, 仅覆盖 baseUrl - RestClient channelClient = restClient - .mutate() - .baseUrl(channelProperties.getTwo().getBaseUrl()) - .build(); - return HttpServiceProxyFactory - .builderFor(RestClientAdapter.create(channelClient)) - .build() - .createClient(YeepayChannelClient.class); + DaxpayChannelProperties channelProperties, + ChannelRestClientSupport channelRestClientSupport) { + return channelRestClientSupport.createClient( + restClient, channelProperties.getTwo(), YeepayChannelClient.class); } } diff --git a/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptor.java b/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptor.java new file mode 100644 index 000000000..421049b0e --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptor.java @@ -0,0 +1,121 @@ +package cn.daxpay.open.platform.common.config.encrypt; + +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizErrorException; +import cn.daxpay.open.platform.core.exception.BizException; +import cn.daxpay.open.platform.core.exception.BizInfoException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +/// # 通道传输 AES-256-GCM 加密器(单密钥、无版本前缀) +/// +/// 用于主应用 ↔ 通道子应用 HTTP 报文透明加解密。 +/// 与存储侧 [SecureAesGcmEncryptor] 算法参数一致,但密文不落库、不分版本: +/// `Base64(IV(12) || ciphertext+tag)`。 +/// +/// 加解密失败抛 [BizException](i18n messageKey),避免传输层静默得到 null 后被 Jackson 误解析。 +public class ChannelAesGcmEncryptor { + + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 128; + /// 密钥须为恰好 32 个 UTF-8 字符(AES-256) + public static final int KEY_LENGTH = 32; + + /// 通道传输加密密钥长度非法 + public static final String MSG_KEY_INVALID = "channel.error.transportEncrypt.keyInvalid"; + /// 通道传输加密失败 + public static final String MSG_ENCRYPT_FAILED = "channel.error.transportEncrypt.encryptFailed"; + /// 通道传输密文长度非法 + public static final String MSG_CIPHERTEXT_INVALID = "channel.error.transportEncrypt.ciphertextInvalid"; + /// 通道传输解密失败 + public static final String MSG_DECRYPT_FAILED = "channel.error.transportEncrypt.decryptFailed"; + + private final SecretKey secretKey; + + /// @param key AES-256 密钥,恰好 32 字符 + public ChannelAesGcmEncryptor(String key) { + validateKey(key); + this.secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM); + } + + /// 校验密钥长度 + public static void validateKey(String key) { + if (key == null || key.length() != KEY_LENGTH) { + // 通道传输加密密钥长度非法 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, MSG_KEY_INVALID, KEY_LENGTH); + } + } + + /// 加密明文 + /// @param plaintext UTF-8 明文(通常为 JSON) + /// @return Base64(IV || ciphertext+tag) + public String encrypt(String plaintext) { + if (plaintext == null) { + return null; + } + try { + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + byte[] iv = new byte[GCM_IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec); + + byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + + byte[] combined = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length); + + return Base64.getEncoder().encodeToString(combined); + } catch (Exception e) { + // 通道传输加密失败 + BizErrorException ex = new BizErrorException(CommonErrorCode.SYSTEM_ERROR, MSG_ENCRYPT_FAILED); + ex.initCause(e); + throw ex; + } + } + + /// 解密密文 + /// @param ciphertext Base64(IV || ciphertext+tag) + /// @return UTF-8 明文 + public String decrypt(String ciphertext) { + if (ciphertext == null) { + return null; + } + try { + byte[] combined = Base64.getDecoder().decode(ciphertext); + if (combined.length <= GCM_IV_LENGTH) { + // 通道传输密文长度非法 + throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, MSG_CIPHERTEXT_INVALID); + } + + byte[] iv = new byte[GCM_IV_LENGTH]; + byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH]; + System.arraycopy(combined, 0, iv, 0, iv.length); + System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length); + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + } catch (BizException e) { + throw e; + } catch (Exception e) { + // 通道传输解密失败 + BizErrorException ex = new BizErrorException(CommonErrorCode.SYSTEM_ERROR, MSG_DECRYPT_FAILED); + ex.initCause(e); + throw ex; + } + } +} diff --git a/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/properties/DaxpayChannelProperties.java b/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/properties/DaxpayChannelProperties.java index 56704d28f..3f910dd4d 100644 --- a/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/properties/DaxpayChannelProperties.java +++ b/daxpay-platform/daxpay-platform-common/common-config/src/main/java/cn/daxpay/open/platform/common/config/properties/DaxpayChannelProperties.java @@ -6,8 +6,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /// # 通道适配子应用配置 /// -/// 统一管理各通道适配子应用(dax-pay-channel-one/two/...)的连接地址。 -/// 各通道模块通过注入本类获取对应子应用的 baseUrl。 +/// 统一管理各通道适配子应用(dax-pay-channel-one/two/...)的连接地址与传输加密密钥。 +/// 各通道模块通过注入本类获取对应子应用的 baseUrl 与 transportEncrypt。 @Data @Accessors(chain = true) @ConfigurationProperties(prefix = "daxpay.channel") @@ -29,5 +29,19 @@ public class DaxpayChannelProperties { /// 基础地址 private String baseUrl = "http://127.0.0.1:20100"; + + /// 传输加密(强制常开,key 必填) + private TransportEncrypt transportEncrypt = new TransportEncrypt(); + } + + /// # 通道 HTTP 报文传输加密配置 + /// + /// 与 `daxpay.platform.config.encrypt`(DB/缓存存储加密)密钥隔离。 + @Data + @Accessors(chain = true) + public static class TransportEncrypt { + + /// AES-256 密钥,恰好 32 个 UTF-8 字符;创建通道 Client 时强校验 + private String key; } } diff --git a/daxpay-platform/daxpay-platform-common/common-config/src/test/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptorTest.java b/daxpay-platform/daxpay-platform-common/common-config/src/test/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptorTest.java new file mode 100644 index 000000000..bfcbc498f --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-config/src/test/java/cn/daxpay/open/platform/common/config/encrypt/ChannelAesGcmEncryptorTest.java @@ -0,0 +1,85 @@ +package cn.daxpay.open.platform.common.config.encrypt; + +import cn.daxpay.open.platform.core.exception.BizErrorException; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.security.SecureRandom; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.*; + +/// # 通道传输 AES-256-GCM 加密器测试 +class ChannelAesGcmEncryptorTest { + + private ChannelAesGcmEncryptor encryptor; + + @BeforeEach + void initEncryptor() { + encryptor = new ChannelAesGcmEncryptor(generateKey(32)); + } + + @Test + @DisplayName("密钥长度非法应抛业务异常") + void shouldRejectInvalidKeyLength() { + assertThrows(BizInfoException.class, () -> new ChannelAesGcmEncryptor(null)); + assertThrows(BizInfoException.class, () -> new ChannelAesGcmEncryptor("short")); + assertThrows(BizInfoException.class, () -> new ChannelAesGcmEncryptor(generateKey(31))); + } + + @Test + @DisplayName("加密和解密往返一致") + void shouldEncryptAndDecrypt() { + String plaintext = "这是一段需要加密的敏感数据,包含中文和特殊字符!@#$%"; + + String encrypted = encryptor.encrypt(plaintext); + String decrypted = encryptor.decrypt(encrypted); + + assertEquals(plaintext, decrypted); + assertNotEquals(plaintext, encrypted); + // 无版本前缀 + assertFalse(encrypted.startsWith("v")); + } + + @Test + @DisplayName("空字符串加密测试") + void shouldEncryptAndDecryptEmptyString() { + String encrypted = encryptor.encrypt(""); + assertEquals("", encryptor.decrypt(encrypted)); + } + + @Test + @DisplayName("长文本加密测试") + void shouldEncryptAndDecryptLongText() { + String plaintext = "这是一段很长的测试数据。".repeat(100); + assertEquals(plaintext, encryptor.decrypt(encryptor.encrypt(plaintext))); + } + + @Test + @DisplayName("同一明文两次密文不同(随机 IV)") + void shouldProduceDifferentCiphertext() { + String plaintext = "{\"amount\":100}"; + String a = encryptor.encrypt(plaintext); + String b = encryptor.encrypt(plaintext); + assertNotEquals(a, b); + assertEquals(plaintext, encryptor.decrypt(a)); + assertEquals(plaintext, encryptor.decrypt(b)); + } + + @Test + @DisplayName("错误密钥解密应失败") + void shouldFailWithWrongKey() { + String encrypted = encryptor.encrypt("{\"ok\":true}"); + ChannelAesGcmEncryptor other = new ChannelAesGcmEncryptor(generateKey(32)); + assertThrows(BizErrorException.class, () -> other.decrypt(encrypted)); + } + + /// 生成指定长度的随机密钥字符串 + private static String generateKey(int length) { + byte[] keyBytes = new byte[length]; + new SecureRandom().nextBytes(keyBytes); + return Base64.getEncoder().encodeToString(keyBytes).substring(0, length); + } +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/channel/error.json index ab2965002..16c4f8f2d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "Callback signature verification failed", "cacheConcurrentConflict": "Concurrent conflict", "systemError": "Internal system error", - "validateParams": "Parameter validation failed" + "validateParams": "Parameter validation failed", + "transportEncrypt": { + "appConfigMissing": "Channel app config is missing: {0}", + "configMissing": "Channel transport encrypt config is missing: {0}", + "keyInvalid": "Channel transport encrypt key must be {0} characters", + "encryptFailed": "Channel transport encrypt failed", + "ciphertextInvalid": "Channel transport ciphertext length is invalid", + "decryptFailed": "Channel transport decrypt failed", + "responseHeaderMissing": "Channel response missing transport encrypt header {0}", + "requestHeaderMissing": "Channel transport encrypt: request missing encrypt header" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/channel/error.json index d838d2a73..ae3478ac8 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "Verifikasi tanda tangan panggilan balik gagal", "cacheConcurrentConflict": "Konflik yang terjadi bersamaan", "systemError": "Kesalahan sistem internal", - "validateParams": "Validasi parameter gagal" + "validateParams": "Validasi parameter gagal", + "transportEncrypt": { + "appConfigMissing": "Konfigurasi aplikasi saluran kosong: {0}", + "configMissing": "Konfigurasi enkripsi transport saluran hilang: {0}", + "keyInvalid": "Kunci enkripsi transport saluran harus {0} karakter", + "encryptFailed": "Enkripsi transport saluran gagal", + "ciphertextInvalid": "Panjang ciphertext transport saluran tidak valid", + "decryptFailed": "Dekripsi transport saluran gagal", + "responseHeaderMissing": "Respons saluran tidak memiliki header enkripsi transport {0}", + "requestHeaderMissing": "Enkripsi transport saluran: permintaan tanpa header enkripsi" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/channel/error.json index fe9c844fe..0b84801fc 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "コールバック署名の検証に失敗しました", "cacheConcurrentConflict": "同時競合", "systemError": "内部システムエラー", - "validateParams": "パラメータの検証に失敗しました" + "validateParams": "パラメータの検証に失敗しました", + "transportEncrypt": { + "appConfigMissing": "チャネルサブアプリ設定が空です: {0}", + "configMissing": "チャネル転送暗号化設定がありません: {0}", + "keyInvalid": "チャネル転送暗号化キーは {0} 文字である必要があります", + "encryptFailed": "チャネル転送の暗号化に失敗しました", + "ciphertextInvalid": "チャネル転送の暗号文の長さが不正です", + "decryptFailed": "チャネル転送の復号に失敗しました", + "responseHeaderMissing": "チャネル応答に転送暗号化ヘッダー {0} がありません", + "requestHeaderMissing": "チャネル転送暗号化: リクエストに暗号化ヘッダーがありません" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/channel/error.json index e9cbb3c4a..e9c8c1e43 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "콜백 서명 확인에 실패했습니다.", "cacheConcurrentConflict": "동시 충돌", "systemError": "내부 시스템 오류", - "validateParams": "매개변수 검증 실패" + "validateParams": "매개변수 검증 실패", + "transportEncrypt": { + "appConfigMissing": "채널 서브앱 설정이 비어 있습니다: {0}", + "configMissing": "채널 전송 암호화 설정이 없습니다: {0}", + "keyInvalid": "채널 전송 암호화 키 길이는 {0}자여야 합니다", + "encryptFailed": "채널 전송 암호화에 실패했습니다", + "ciphertextInvalid": "채널 전송 암호문 길이가 잘못되었습니다", + "decryptFailed": "채널 전송 복호화에 실패했습니다", + "responseHeaderMissing": "채널 응답에 전송 암호화 헤더 {0}가 없습니다", + "requestHeaderMissing": "채널 전송 암호화: 요청에 암호화 헤더가 없습니다" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/channel/error.json index 224c6b5b4..369fe74fc 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "Pengesahan tandatangan panggilan balik gagal", "cacheConcurrentConflict": "Konflik serentak", "systemError": "Ralat sistem dalaman", - "validateParams": "Pengesahan parameter gagal" + "validateParams": "Pengesahan parameter gagal", + "transportEncrypt": { + "appConfigMissing": "Konfigurasi aplikasi saluran kosong: {0}", + "configMissing": "Konfigurasi penyulitan pengangkutan saluran tiada: {0}", + "keyInvalid": "Kunci penyulitan pengangkutan saluran mestilah {0} aksara", + "encryptFailed": "Penyulitan pengangkutan saluran gagal", + "ciphertextInvalid": "Panjang ciphertext pengangkutan saluran tidak sah", + "decryptFailed": "Penyahsulitan pengangkutan saluran gagal", + "responseHeaderMissing": "Respons saluran tiada pengepala penyulitan pengangkutan {0}", + "requestHeaderMissing": "Penyulitan pengangkutan saluran: permintaan tiada pengepala penyulitan" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/channel/error.json index 3062a45f9..25ba8c695 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "การตรวจสอบลายเซ็นโทรกลับล้มเหลว", "cacheConcurrentConflict": "ความขัดแย้งที่เกิดขึ้นพร้อมกัน", "systemError": "ข้อผิดพลาดของระบบภายใน", - "validateParams": "การตรวจสอบพารามิเตอร์ล้มเหลว" + "validateParams": "การตรวจสอบพารามิเตอร์ล้มเหลว", + "transportEncrypt": { + "appConfigMissing": "การตั้งค่าแอปช่องทางว่างเปล่า: {0}", + "configMissing": "ไม่มีการตั้งค่าการเข้ารหัสการส่งช่องทาง: {0}", + "keyInvalid": "คีย์เข้ารหัสการส่งช่องทางต้องมี {0} ตัวอักษร", + "encryptFailed": "การเข้ารหัสการส่งช่องทางล้มเหลว", + "ciphertextInvalid": "ความยาวciphertext การส่งช่องทางไม่ถูกต้อง", + "decryptFailed": "การถอดรหัสการส่งช่องทางล้มเหลว", + "responseHeaderMissing": "การตอบกลับช่องทางไม่มีส่วนหัวเข้ารหัสการส่ง {0}", + "requestHeaderMissing": "การเข้ารหัสการส่งช่องทาง: คำขอไม่มีส่วนหัวเข้ารหัส" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/channel/error.json index 6844a835e..7aa0959b4 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "Xác minh chữ ký gọi lại không thành công", "cacheConcurrentConflict": "Xung đột đồng thời", "systemError": "Lỗi hệ thống nội bộ", - "validateParams": "Xác thực tham số không thành công" + "validateParams": "Xác thực tham số không thành công", + "transportEncrypt": { + "appConfigMissing": "Cấu hình ứng dụng kênh trống: {0}", + "configMissing": "Thiếu cấu hình mã hóa truyền tải kênh: {0}", + "keyInvalid": "Khóa mã hóa truyền tải kênh phải có {0} ký tự", + "encryptFailed": "Mã hóa truyền tải kênh thất bại", + "ciphertextInvalid": "Độ dài bản mã truyền tải kênh không hợp lệ", + "decryptFailed": "Giải mã truyền tải kênh thất bại", + "responseHeaderMissing": "Phản hồi kênh thiếu header mã hóa truyền tải {0}", + "requestHeaderMissing": "Mã hóa truyền tải kênh: yêu cầu thiếu header mã hóa" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/channel/error.json index 95f5bd2d1..72dd6dc1a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "回调验签失败", "cacheConcurrentConflict": "并发冲突", "systemError": "系统内部错误", - "validateParams": "参数校验失败" + "validateParams": "参数校验失败", + "transportEncrypt": { + "appConfigMissing": "通道子应用配置不能为空: {0}", + "configMissing": "缺少通道传输加密配置: {0}", + "keyInvalid": "通道传输加密密钥长度须为 {0} 个字符", + "encryptFailed": "通道传输加密失败", + "ciphertextInvalid": "通道传输密文长度非法", + "decryptFailed": "通道传输解密失败", + "responseHeaderMissing": "通道子应用响应未携带传输加密头 {0}", + "requestHeaderMissing": "通道传输加密:请求未携带加密头" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/channel/error.json index ee162ecc0..b296f2a68 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "回調驗籤失敗", "cacheConcurrentConflict": "併發衝突", "systemError": "系統內部錯誤", - "validateParams": "參數校驗失敗" + "validateParams": "參數校驗失敗", + "transportEncrypt": { + "appConfigMissing": "通道子應用配置不能為空: {0}", + "configMissing": "缺少通道傳輸加密配置: {0}", + "keyInvalid": "通道傳輸加密密鑰長度須為 {0} 個字元", + "encryptFailed": "通道傳輸加密失敗", + "ciphertextInvalid": "通道傳輸密文長度非法", + "decryptFailed": "通道傳輸解密失敗", + "responseHeaderMissing": "通道子應用回應未攜帶傳輸加密頭 {0}", + "requestHeaderMissing": "通道傳輸加密:請求未攜帶加密頭" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/channel/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/channel/error.json index 22eee03b3..2aaed942b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/channel/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/channel/error.json @@ -8,5 +8,15 @@ "callbackVerifyFailed": "回撥驗籤失敗", "cacheConcurrentConflict": "併發衝突", "systemError": "系統內部錯誤", - "validateParams": "引數校驗失敗" + "validateParams": "引數校驗失敗", + "transportEncrypt": { + "appConfigMissing": "通道子應用配置不能為空: {0}", + "configMissing": "缺少通道傳輸加密配置: {0}", + "keyInvalid": "通道傳輸加密金鑰長度須為 {0} 個字元", + "encryptFailed": "通道傳輸加密失敗", + "ciphertextInvalid": "通道傳輸密文長度非法", + "decryptFailed": "通道傳輸解密失敗", + "responseHeaderMissing": "通道子應用回應未攜帶傳輸加密頭 {0}", + "requestHeaderMissing": "通道傳輸加密:請求未攜帶加密頭" + } } diff --git a/daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelRestClientSupport.java b/daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelRestClientSupport.java new file mode 100644 index 000000000..3e510715f --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelRestClientSupport.java @@ -0,0 +1,58 @@ +package cn.daxpay.open.platform.common.spring.channel; + +import cn.daxpay.open.platform.common.config.encrypt.ChannelAesGcmEncryptor; +import cn.daxpay.open.platform.common.config.properties.DaxpayChannelProperties; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +/// # 通道 RestClient 工厂 +/// +/// 为各 HttpExchange 通道 Client 派生带 baseUrl + 传输加密拦截器的 RestClient。 +/// 传输加密强制常开:key 非法则创建 Client 时直接失败,避免带病启动后明文外泄。 +@Slf4j +@Component +public class ChannelRestClientSupport { + + /// 通道子应用配置不能为空 + public static final String MSG_APP_CONFIG_MISSING = "channel.error.transportEncrypt.appConfigMissing"; + /// 缺少通道传输加密配置 + public static final String MSG_CONFIG_MISSING = "channel.error.transportEncrypt.configMissing"; + + /// 创建声明式通道 HTTP Client + /// @param restClient 全局 RestClient(已含 OTel / 业务上下文拦截器) + /// @param app 子应用连接与传输加密配置 + /// @param clientType @HttpExchange 接口类型 + public T createClient(RestClient restClient, + DaxpayChannelProperties.ChannelApp app, + Class clientType) { + if (app == null) { + // 通道子应用配置不能为空 + throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, MSG_APP_CONFIG_MISSING, clientType.getSimpleName()); + } + var transportEncrypt = app.getTransportEncrypt(); + if (transportEncrypt == null) { + // 缺少通道传输加密配置 + throw new BizInfoException(CommonErrorCode.SYSTEM_ERROR, MSG_CONFIG_MISSING, clientType.getSimpleName()); + } + ChannelAesGcmEncryptor encryptor = new ChannelAesGcmEncryptor(transportEncrypt.getKey()); + + RestClient channelClient = restClient + .mutate() + .baseUrl(app.getBaseUrl()) + .requestInterceptor(new ChannelTransportEncryptInterceptor(encryptor)) + .build(); + + log.info("已创建通道 Client {},baseUrl={},传输加密=强制开启", + clientType.getSimpleName(), app.getBaseUrl()); + + return HttpServiceProxyFactory + .builderFor(RestClientAdapter.create(channelClient)) + .build() + .createClient(clientType); + } +} diff --git a/daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptor.java b/daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptor.java new file mode 100644 index 000000000..4a68abe58 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-spring/src/main/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptor.java @@ -0,0 +1,124 @@ +package cn.daxpay.open.platform.common.spring.channel; + +import cn.daxpay.open.platform.common.config.encrypt.ChannelAesGcmEncryptor; +import cn.daxpay.open.platform.core.code.CommonErrorCode; +import cn.daxpay.open.platform.core.exception.BizInfoException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StreamUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/// # 通道传输报文加解密拦截器 +/// +/// 主应用出站:非空请求 body 强制 AES-GCM 加密;响应强制解密后交给业务层。 +/// 对业务 HttpExchange Client 透明。 +/// +/// Header 名与 WebHeaderCode.X_DAX_PAYLOAD_ENCRYPTED 保持一致, +/// 此处硬编码以避免 common-spring → platform-core 依赖。 +@Slf4j +@RequiredArgsConstructor +public class ChannelTransportEncryptInterceptor implements ClientHttpRequestInterceptor { + + /// 通道传输报文已加密标记(与 WebHeaderCode.X_DAX_PAYLOAD_ENCRYPTED 一致) + public static final String HEADER_X_DAX_PAYLOAD_ENCRYPTED = "X-Dax-Payload-Encrypted"; + + /// 通道子应用响应未携带传输加密头 + public static final String MSG_RESPONSE_HEADER_MISSING = + "channel.error.transportEncrypt.responseHeaderMissing"; + + private static final MediaType TEXT_PLAIN_UTF8 = + new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8); + + private final ChannelAesGcmEncryptor encryptor; + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + byte[] requestBody = body; + if (body.length > 0) { + String ciphertext = encryptor.encrypt(new String(body, StandardCharsets.UTF_8)); + requestBody = ciphertext.getBytes(StandardCharsets.UTF_8); + request.getHeaders().set(HEADER_X_DAX_PAYLOAD_ENCRYPTED, "true"); + request.getHeaders().setContentType(TEXT_PLAIN_UTF8); + request.getHeaders().setContentLength(requestBody.length); + } + + ClientHttpResponse response = execution.execute(request, requestBody); + return decryptResponse(response); + } + + /// 解密响应体;无 body 原样返回;有 body 但无加密头视为协议错误 + private ClientHttpResponse decryptResponse(ClientHttpResponse response) throws IOException { + byte[] responseBytes = StreamUtils.copyToByteArray(response.getBody()); + if (responseBytes.length == 0) { + return new DecryptedClientHttpResponse(response, responseBytes, false); + } + + String encryptedFlag = response.getHeaders().getFirst(HEADER_X_DAX_PAYLOAD_ENCRYPTED); + if (!"true".equalsIgnoreCase(encryptedFlag)) { + response.close(); + // 通道子应用响应未携带传输加密头 + throw new BizInfoException( + CommonErrorCode.SYSTEM_ERROR, MSG_RESPONSE_HEADER_MISSING, HEADER_X_DAX_PAYLOAD_ENCRYPTED); + } + + String plaintext = encryptor.decrypt(new String(responseBytes, StandardCharsets.UTF_8)); + byte[] plainBytes = plaintext.getBytes(StandardCharsets.UTF_8); + return new DecryptedClientHttpResponse(response, plainBytes, true); + } + + /// 包装已解密的响应,供 RestClient/HttpExchange 按 JSON 解析 + static final class DecryptedClientHttpResponse implements ClientHttpResponse { + + private final ClientHttpResponse delegate; + private final byte[] body; + private final HttpHeaders headers; + + DecryptedClientHttpResponse(ClientHttpResponse delegate, byte[] body, boolean decrypted) { + this.delegate = delegate; + this.body = body; + this.headers = new HttpHeaders(); + this.headers.putAll(delegate.getHeaders()); + if (decrypted) { + this.headers.setContentType(MediaType.APPLICATION_JSON); + this.headers.setContentLength(body.length); + } + } + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return delegate.getStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return delegate.getStatusText(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public InputStream getBody() { + return new ByteArrayInputStream(body); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + } +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/WebHeaderCode.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/WebHeaderCode.java index 5506df7d5..b697eed7e 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/WebHeaderCode.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/code/WebHeaderCode.java @@ -22,4 +22,7 @@ public interface WebHeaderCode { /// 国际化语言 String ACCEPT_LANGUAGE = "accept-language"; + /// 通道传输报文已 AES-GCM 加密标记(主应用 ↔ 通道子应用) + String X_DAX_PAYLOAD_ENCRYPTED = "X-Dax-Payload-Encrypted"; + } diff --git a/daxpay-start/src/main/resources/application-dev.yml b/daxpay-start/src/main/resources/application-dev.yml index e2a11bde0..e99f1502d 100644 --- a/daxpay-start/src/main/resources/application-dev.yml +++ b/daxpay-start/src/main/resources/application-dev.yml @@ -171,12 +171,19 @@ daxpay: # 拆分目的: 通道 SDK 依赖隔离(避免冲突) + 独立升级 + 弹性伸缩 # 子应用编号(one/two/three)固定且不可改名, 与路由策略中的通道分配一一对应。 channel: - # 子应用1: 支付宝 + 微信支付(已启用, 本机启动的 dax-pay-channel-one 服务) + # 子应用1: 支付宝/微信支付/银联商务/(已启用, 本机启动的 dax-pay-channel-one 服务) one: base-url: http://127.0.0.1:20100 - # 子应用2: 其他支付通道(channel-two 已搭建架子, 通道待对接) + # 通道传输 AES 密钥(强制常开,恰好 32 字符;仅本地占位,勿用于生产) + transport-encrypt: + key: z0Vd8jDKB80pA6OOptGLO+qDVvWboEko + # 子应用2: 其他支付通道 two: base-url: http://127.0.0.1:20200 - # 子应用3: 抖音 + 其他通道(未来扩展, 暂未启用) + transport-encrypt: + key: z0Vd8jDKB80pA6OOptGLO+qDVvWboEko + # 子应用3: 其他通道(未来扩展, 暂未启用) # three: - # base-url: http://127.0.0.1:20300 \ No newline at end of file + # base-url: http://127.0.0.1:20300 + # transport-encrypt: + # key: ${CHANNEL_THREE_TRANSPORT_KEY} \ No newline at end of file diff --git a/daxpay-start/src/main/resources/application-prod.yml b/daxpay-start/src/main/resources/application-prod.yml index ead452ef3..38decaede 100644 --- a/daxpay-start/src/main/resources/application-prod.yml +++ b/daxpay-start/src/main/resources/application-prod.yml @@ -141,9 +141,16 @@ daxpay: # 子应用1: 支付宝 + 微信支付(已启用, Docker Compose 中 channel-one 容器) one: base-url: ${CHANNEL_ONE_BASE_URL:http://channel-one:20100} + # 通道传输 AES 密钥(强制常开,恰好 32 字符;生产必须由环境变量注入,无默认值) + transport-encrypt: + key: ${CHANNEL_ONE_TRANSPORT_KEY} # 子应用2: 其他支付通道(channel-two 已搭建架子, 通道待对接) two: base-url: ${CHANNEL_TWO_BASE_URL:http://channel-two:20200} + transport-encrypt: + key: ${CHANNEL_TWO_TRANSPORT_KEY} # 子应用3: 抖音 + 其他通道(未来扩展, 暂未启用) # three: - # base-url: ${CHANNEL_THREE_BASE_URL:http://channel-three:20300} \ No newline at end of file + # base-url: ${CHANNEL_THREE_BASE_URL:http://channel-three:20300} + # transport-encrypt: + # key: ${CHANNEL_THREE_TRANSPORT_KEY} \ No newline at end of file