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 16c4f8f2d..efc6711da 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
@@ -17,6 +17,7 @@
"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"
+ "requestHeaderMissing": "Channel transport encrypt: request missing encrypt header",
+ "plainErrorResponse": "Channel sub-app returned plaintext error response [{0}]: {1}"
}
}
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 ae3478ac8..5882ba7a0 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
@@ -17,6 +17,7 @@
"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"
+ "requestHeaderMissing": "Enkripsi transport saluran: permintaan tanpa header enkripsi",
+ "plainErrorResponse": "Sub-aplikasi saluran mengembalikan respons kesalahan teks polos [{0}]: {1}"
}
}
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 0b84801fc..a3f84685a 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
@@ -17,6 +17,7 @@
"ciphertextInvalid": "チャネル転送の暗号文の長さが不正です",
"decryptFailed": "チャネル転送の復号に失敗しました",
"responseHeaderMissing": "チャネル応答に転送暗号化ヘッダー {0} がありません",
- "requestHeaderMissing": "チャネル転送暗号化: リクエストに暗号化ヘッダーがありません"
+ "requestHeaderMissing": "チャネル転送暗号化: リクエストに暗号化ヘッダーがありません",
+ "plainErrorResponse": "チャネル子アプリから平文エラーレスポンス[{0}]が返されました: {1}"
}
}
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 e9c8c1e43..858efe016 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
@@ -17,6 +17,7 @@
"ciphertextInvalid": "채널 전송 암호문 길이가 잘못되었습니다",
"decryptFailed": "채널 전송 복호화에 실패했습니다",
"responseHeaderMissing": "채널 응답에 전송 암호화 헤더 {0}가 없습니다",
- "requestHeaderMissing": "채널 전송 암호화: 요청에 암호화 헤더가 없습니다"
+ "requestHeaderMissing": "채널 전송 암호화: 요청에 암호화 헤더가 없습니다",
+ "plainErrorResponse": "채널 자식 앱이 평문 오류 응답[{0}]을 반환했습니다: {1}"
}
}
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 369fe74fc..cc8585dd8 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
@@ -17,6 +17,7 @@
"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"
+ "requestHeaderMissing": "Penyulitan pengangkutan saluran: permintaan tiada pengepala penyulitan",
+ "plainErrorResponse": "Sub-aplikasi saluran memulangkan respons ralat teks biasa [{0}]: {1}"
}
}
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 25ba8c695..ae82d9349 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
@@ -17,6 +17,7 @@
"ciphertextInvalid": "ความยาวciphertext การส่งช่องทางไม่ถูกต้อง",
"decryptFailed": "การถอดรหัสการส่งช่องทางล้มเหลว",
"responseHeaderMissing": "การตอบกลับช่องทางไม่มีส่วนหัวเข้ารหัสการส่ง {0}",
- "requestHeaderMissing": "การเข้ารหัสการส่งช่องทาง: คำขอไม่มีส่วนหัวเข้ารหัส"
+ "requestHeaderMissing": "การเข้ารหัสการส่งช่องทาง: คำขอไม่มีส่วนหัวเข้ารหัส",
+ "plainErrorResponse": "แอปย่อยของช่องทางส่งกลับการตอบกลับข้อผิดพลาดแบบข้อความธรรมดา [{0}]: {1}"
}
}
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 7aa0959b4..b1fd10b5b 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
@@ -17,6 +17,7 @@
"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"
+ "requestHeaderMissing": "Mã hóa truyền tải kênh: yêu cầu thiếu header mã hóa",
+ "plainErrorResponse": "Ứng dụng con kênh trả về phản hồi lỗi văn bản thuần [{0}]: {1}"
}
}
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 72dd6dc1a..f1beb0d4b 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
@@ -17,6 +17,7 @@
"ciphertextInvalid": "通道传输密文长度非法",
"decryptFailed": "通道传输解密失败",
"responseHeaderMissing": "通道子应用响应未携带传输加密头 {0}",
- "requestHeaderMissing": "通道传输加密:请求未携带加密头"
+ "requestHeaderMissing": "通道传输加密:请求未携带加密头",
+ "plainErrorResponse": "通道子应用返回明文错误响应[{0}]: {1}"
}
}
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 b296f2a68..1b26496f2 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
@@ -17,6 +17,7 @@
"ciphertextInvalid": "通道傳輸密文長度非法",
"decryptFailed": "通道傳輸解密失敗",
"responseHeaderMissing": "通道子應用回應未攜帶傳輸加密頭 {0}",
- "requestHeaderMissing": "通道傳輸加密:請求未攜帶加密頭"
+ "requestHeaderMissing": "通道傳輸加密:請求未攜帶加密頭",
+ "plainErrorResponse": "通道子應用傳回明文錯誤回應[{0}]: {1}"
}
}
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 2aaed942b..340b33e9e 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
@@ -17,6 +17,7 @@
"ciphertextInvalid": "通道傳輸密文長度非法",
"decryptFailed": "通道傳輸解密失敗",
"responseHeaderMissing": "通道子應用回應未攜帶傳輸加密頭 {0}",
- "requestHeaderMissing": "通道傳輸加密:請求未攜帶加密頭"
+ "requestHeaderMissing": "通道傳輸加密:請求未攜帶加密頭",
+ "plainErrorResponse": "通道子應用回傳明文錯誤回應[{0}]: {1}"
}
}
diff --git a/daxpay-platform/daxpay-platform-common/common-spring/pom.xml b/daxpay-platform/daxpay-platform-common/common-spring/pom.xml
index d303c00d8..05ad37083 100644
--- a/daxpay-platform/daxpay-platform-common/common-spring/pom.xml
+++ b/daxpay-platform/daxpay-platform-common/common-spring/pom.xml
@@ -46,5 +46,10 @@
common-config
${project.version}
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
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
index 4a68abe58..59a14b178 100644
--- 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
@@ -13,6 +13,9 @@ import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -37,9 +40,19 @@ public class ChannelTransportEncryptInterceptor implements ClientHttpRequestInte
public static final String MSG_RESPONSE_HEADER_MISSING =
"channel.error.transportEncrypt.responseHeaderMissing";
+ /// 通道子应用返回明文错误响应(子应用主动返回的未加密错误,如入站解密失败 400)
+ public static final String MSG_PLAIN_ERROR_RESPONSE =
+ "channel.error.transportEncrypt.plainErrorResponse";
+
private static final MediaType TEXT_PLAIN_UTF8 =
new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8);
+ /// 明文错误 body 片段最大长度,防止异常日志爆炸
+ private static final int SNIPPET_MAX_LENGTH = 200;
+
+ /// 解析子应用明文错误响应的独立 ObjectMapper(拦截器为手动 new,非 Spring Bean,无法注入)
+ private static final ObjectMapper PLAIN_ERROR_MAPPER = JsonMapper.builder().build();
+
private final ChannelAesGcmEncryptor encryptor;
@Override
@@ -58,7 +71,7 @@ public class ChannelTransportEncryptInterceptor implements ClientHttpRequestInte
return decryptResponse(response);
}
- /// 解密响应体;无 body 原样返回;有 body 但无加密头视为协议错误
+ /// 解密响应体;无 body 原样返回;有加密头则解密;无加密头按状态码区分协议违规与明文错误响应
private ClientHttpResponse decryptResponse(ClientHttpResponse response) throws IOException {
byte[] responseBytes = StreamUtils.copyToByteArray(response.getBody());
if (responseBytes.length == 0) {
@@ -66,16 +79,48 @@ public class ChannelTransportEncryptInterceptor implements ClientHttpRequestInte
}
String encryptedFlag = response.getHeaders().getFirst(HEADER_X_DAX_PAYLOAD_ENCRYPTED);
- if (!"true".equalsIgnoreCase(encryptedFlag)) {
- response.close();
+ if ("true".equalsIgnoreCase(encryptedFlag)) {
+ // 正常加密响应:解密后交给业务层
+ String plaintext = encryptor.decrypt(new String(responseBytes, StandardCharsets.UTF_8));
+ byte[] plainBytes = plaintext.getBytes(StandardCharsets.UTF_8);
+ return new DecryptedClientHttpResponse(response, plainBytes, true);
+ }
+
+ // 无加密头:子应用未按加密协议响应。区分「成功响应未加密」与「明文错误响应」
+ // 状态码须在 close 前获取,避免部分实现 close 后状态丢失
+ HttpStatusCode statusCode = response.getStatusCode();
+ response.close();
+ if (statusCode.is2xxSuccessful()) {
+ // 2xx 成功响应却未加密:真正的协议违规(子应用未加密或被劫持),按安全约束报错
// 通道子应用响应未携带传输加密头
throw new BizInfoException(
CommonErrorCode.SYSTEM_ERROR, MSG_RESPONSE_HEADER_MISSING, HEADER_X_DAX_PAYLOAD_ENCRYPTED);
}
+ // 非 2xx 明文错误响应:子应用入站解密失败/加密失败等主动返回的明文错误(如 400 解密失败)
+ // 透传子应用返回的真实错误详情,避免被「响应未携带加密头」掩盖
+ // 通道子应用返回明文错误响应
+ String detail = extractPlainErrorDetail(responseBytes);
+ throw new BizInfoException(
+ CommonErrorCode.SYSTEM_ERROR, MSG_PLAIN_ERROR_RESPONSE, statusCode.value(), detail);
+ }
- String plaintext = encryptor.decrypt(new String(responseBytes, StandardCharsets.UTF_8));
- byte[] plainBytes = plaintext.getBytes(StandardCharsets.UTF_8);
- return new DecryptedClientHttpResponse(response, plainBytes, true);
+ /// 从子应用明文错误响应体提取错误详情
+ /// 优先解析 JSON 的 msg 字段(子应用返回 DaxResult 结构 {"code":400,"msg":"..."}),解析失败回退原始 body 片段
+ private static String extractPlainErrorDetail(byte[] responseBytes) {
+ String body = new String(responseBytes, StandardCharsets.UTF_8);
+ try {
+ JsonNode node = PLAIN_ERROR_MAPPER.readTree(body);
+ String msg = node.path("msg").asText("");
+ if (!msg.isEmpty()) {
+ return msg;
+ }
+ } catch (Exception ignored) {
+ // 解析失败回退到原始 body 片段
+ }
+ // 截断防止异常日志爆炸
+ return body.length() > SNIPPET_MAX_LENGTH
+ ? body.substring(0, SNIPPET_MAX_LENGTH) + "..."
+ : body;
}
/// 包装已解密的响应,供 RestClient/HttpExchange 按 JSON 解析
diff --git a/daxpay-platform/daxpay-platform-common/common-spring/src/test/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptorTest.java b/daxpay-platform/daxpay-platform-common/common-spring/src/test/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptorTest.java
new file mode 100644
index 000000000..1175e84a5
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-common/common-spring/src/test/java/cn/daxpay/open/platform/common/spring/channel/ChannelTransportEncryptInterceptorTest.java
@@ -0,0 +1,205 @@
+package cn.daxpay.open.platform.common.spring.channel;
+
+import cn.daxpay.open.platform.common.config.encrypt.ChannelAesGcmEncryptor;
+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 org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpResponse;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/// # 通道传输报文加解密拦截器测试
+///
+/// 覆盖 [ChannelTransportEncryptInterceptor#decryptResponse] 的四种分支:
+/// 1. 正常加密响应(2xx + 加密头)应解密为明文 JSON
+/// 2. 成功响应未加密(2xx + 无加密头)应抛「响应未携带传输加密头」(协议违规)
+/// 3. 子应用明文错误响应(非 2xx + 无加密头)应抛「明文错误响应」并透传真实错误详情
+/// 4. 空 body 响应原样放行
+class ChannelTransportEncryptInterceptorTest {
+
+ private ChannelAesGcmEncryptor encryptor;
+ private ChannelTransportEncryptInterceptor interceptor;
+
+ @BeforeEach
+ void initInterceptor() {
+ encryptor = new ChannelAesGcmEncryptor(generateKey(32));
+ interceptor = new ChannelTransportEncryptInterceptor(encryptor);
+ }
+
+ @Test
+ @DisplayName("正常加密响应(200 + 加密头)应解密为明文 JSON")
+ void shouldDecryptEncryptedResponse() throws IOException {
+ // 模拟子应用返回的加密响应
+ String plaintext = "{\"code\":0,\"data\":\"ok\"}";
+ String ciphertext = encryptor.encrypt(plaintext);
+ ClientHttpResponse response = mockResponse(200, ciphertext, true);
+
+ ClientHttpResponse result = interceptor.intercept(mockRequest(), new byte[0], executionReturning(response));
+
+ // 出站请求 body 为空时不会被加密;响应应被解密为明文 JSON
+ byte[] body = result.getBody().readAllBytes();
+ assertEquals(plaintext, new String(body, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ @DisplayName("成功响应未携带加密头应抛协议违规错误")
+ void shouldThrowWhenSuccessResponseMissingEncryptedHeader() {
+ // 2xx 成功响应却未加密,属于真正的协议违规
+ ClientHttpResponse response = mockResponse(200, "{\"code\":0,\"data\":\"ok\"}", false);
+
+ BizInfoException ex = assertThrows(BizInfoException.class,
+ () -> interceptor.intercept(mockRequest(), new byte[0], executionReturning(response)));
+
+ assertEquals(ChannelTransportEncryptInterceptor.MSG_RESPONSE_HEADER_MISSING, ex.getMessageKey());
+ }
+
+ @Test
+ @DisplayName("子应用明文错误响应(400 解密失败)应透传真实错误详情")
+ void shouldUnwrapPlainErrorResponseDetail() {
+ // 模拟子应用入站解密失败返回的 400 明文 JSON
+ String plainErrorBody = "{\"code\":400,\"msg\":\"通道传输解密失败\"}";
+ ClientHttpResponse response = mockResponse(400, plainErrorBody, false);
+
+ BizInfoException ex = assertThrows(BizInfoException.class,
+ () -> interceptor.intercept(mockRequest(), new byte[0], executionReturning(response)));
+
+ // 应抛「明文错误响应」而非「响应未携带加密头」,并携带状态码与真实错误 msg
+ assertEquals(ChannelTransportEncryptInterceptor.MSG_PLAIN_ERROR_RESPONSE, ex.getMessageKey());
+ Object[] args = ex.getArgs();
+ assertEquals(400, args[0]);
+ assertEquals("通道传输解密失败", args[1]);
+ }
+
+ @Test
+ @DisplayName("明文错误 body 非 JSON 时应回退为原始片段")
+ void shouldFallbackToRawSnippetWhenBodyNotJson() {
+ // 子应用返回非 JSON 明文(如网关/代理介入)
+ ClientHttpResponse response = mockResponse(502, "Bad Gateway", false);
+
+ BizInfoException ex = assertThrows(BizInfoException.class,
+ () -> interceptor.intercept(mockRequest(), new byte[0], executionReturning(response)));
+
+ assertEquals(ChannelTransportEncryptInterceptor.MSG_PLAIN_ERROR_RESPONSE, ex.getMessageKey());
+ assertEquals(502, ex.getArgs()[0]);
+ // 回退为原始 body 片段
+ assertEquals("Bad Gateway", ex.getArgs()[1]);
+ }
+
+ @Test
+ @DisplayName("空 body 响应原样放行")
+ void shouldPassThroughEmptyBody() throws IOException {
+ ClientHttpResponse response = mockResponse(200, "", true);
+
+ ClientHttpResponse result = interceptor.intercept(mockRequest(), new byte[0], executionReturning(response));
+
+ // 空 body 不解密,原样返回空字节数组
+ assertArrayEquals(new byte[0], result.getBody().readAllBytes());
+ }
+
+ @Test
+ @DisplayName("出站非空请求 body 应被加密并携带加密头")
+ void shouldEncryptNonEmptyRequestBody() throws IOException {
+ String requestBody = "{\"channel\":\"alipay\"}";
+ byte[] body = requestBody.getBytes(StandardCharsets.UTF_8);
+ // 加密响应,确保入站不抛错
+ String ciphertext = encryptor.encrypt("{\"code\":0}");
+ ClientHttpResponse response = mockResponse(200, ciphertext, true);
+
+ // 捕获实际发给 execution 的请求 body
+ HttpRequest request = mockRequest();
+ interceptor.intercept(request, body, executionReturning(response));
+
+ // 请求应被加密并设置加密头
+ assertEquals("true", request.getHeaders().getFirst(ChannelTransportEncryptInterceptor.HEADER_X_DAX_PAYLOAD_ENCRYPTED));
+ }
+
+ /// 构造返回固定响应的 execution
+ private static ClientHttpRequestExecution executionReturning(ClientHttpResponse response) {
+ return (request, body) -> response;
+ }
+
+ /// 构造最简 HttpRequest(带可写 headers)
+ private static HttpRequest mockRequest() {
+ HttpHeaders headers = new HttpHeaders();
+ return new HttpRequest() {
+ @Override
+ public HttpMethod getMethod() {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public URI getURI() {
+ return URI.create("http://localhost/channel/test");
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return Map.of();
+ }
+ };
+ }
+
+ /// 构造 mock 响应
+ private static ClientHttpResponse mockResponse(int status, String body, boolean withEncryptedHeader) {
+ HttpHeaders headers = new HttpHeaders();
+ if (withEncryptedHeader) {
+ headers.add(ChannelTransportEncryptInterceptor.HEADER_X_DAX_PAYLOAD_ENCRYPTED, "true");
+ }
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ return new ClientHttpResponse() {
+ @Override
+ public HttpStatusCode getStatusCode() {
+ return HttpStatusCode.valueOf(status);
+ }
+
+ @Override
+ public String getStatusText() {
+ return "";
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public InputStream getBody() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+ };
+ }
+
+ /// 生成指定长度的随机密钥字符串
+ 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-start/src/main/resources/application-dev.yml b/daxpay-start/src/main/resources/application-dev.yml
index 235b8130f..f70f744de 100644
--- a/daxpay-start/src/main/resources/application-dev.yml
+++ b/daxpay-start/src/main/resources/application-dev.yml
@@ -175,13 +175,15 @@ daxpay:
one:
base-url: http://127.0.0.1:20100
# 通道传输 AES 密钥(强制常开,恰好 32 字符;仅本地占位,勿用于生产)
+ # 须与子应用一致: dax-pay-channel-one application.yml / dax-pay-channel-one-go config.yaml 的 channel.transport-encrypt.key
transport-encrypt:
- key: z0Vd8jDKB80pA6OOptGLO+qDVvWboEko
+ key: 0123456789abcdef0123456789abcdef
# 子应用2: 其他支付通道
two:
base-url: http://127.0.0.1:20200
+ # 须与子应用一致: dax-pay-channel-two application.yml 的 channel.transport-encrypt.key
transport-encrypt:
- key: z0Vd8jDKB80pA6OOptGLO+qDVvWboEko
+ key: fedcba9876543210fedcba9876543210
# 子应用3: 其他通道(未来扩展, 暂未启用)
# three:
# base-url: http://127.0.0.1:20300