diff --git a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiClient.java b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiClient.java index 012686b..ac4fa96 100644 --- a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiClient.java +++ b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiClient.java @@ -1,6 +1,10 @@ package com.czcb.scfs.api.core; import com.czcb.scfs.api.core.http.*; +import com.czcb.scfs.api.core.util.Extractor; +import com.czcb.scfs.api.core.util.Json; + +import java.util.Map; /** * @author wangwei @@ -41,6 +45,18 @@ public interface ApiClient { return post(url, new HttpHeaders(), body, responseClass); } + default HttpResponse post(String url, HttpHeaders headers, Map body, Class responseClass) { + return post(url, new QueryParameter(), headers, body, Extractor.extractValues(body), responseClass); + } + + default HttpResponse post(String url, QueryParameter parameters, HttpHeaders headers, Map body, Map extraParams, Class responseClass) { + JsonRequestBody requestBody = new JsonRequestBody.Builder() + .body(Json.toJson(body)) + .extraParams(extraParams) + .build(); + return post(url, parameters, headers, requestBody, responseClass); + } + default HttpResponse post(String url, HttpHeaders headers, RequestBody body, Class responseClass) { return post(url, new QueryParameter(), headers, body, responseClass); } diff --git a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiRequest.java b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiRequest.java index f5d0ed6..4f40e5d 100644 --- a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiRequest.java +++ b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/ApiRequest.java @@ -1,16 +1,10 @@ package com.czcb.scfs.api.core; import com.czcb.scfs.api.core.http.JsonRequestBody; +import com.czcb.scfs.api.core.util.Extractor; import com.czcb.scfs.api.core.util.Json; -import com.czcb.scfs.api.core.util.ObjReflect; -import com.czcb.scfs.api.core.util.Strings; import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -import static com.czcb.scfs.api.core.Constants.APP_NO; -import static com.czcb.scfs.api.core.Constants.CHANNEL_NO; /** * @author wangwei @@ -18,18 +12,9 @@ import static com.czcb.scfs.api.core.Constants.CHANNEL_NO; */ public interface ApiRequest extends Serializable { default JsonRequestBody toJsonRequest() { - final Map params = new HashMap<>(); - - String channelNoValue = ObjReflect.getValueAsString(this, "channelNo"); - if (Strings.isNotEmpty(channelNoValue)) { - params.put(CHANNEL_NO, channelNoValue); - } - - String appNoValue = ObjReflect.getValueAsString(this, "appNo"); - if (Strings.isNotEmpty(appNoValue)) { - params.put(APP_NO, appNoValue); - } - - return new JsonRequestBody.Builder().body(Json.toJson(this)).extraParams(params).build(); + return new JsonRequestBody.Builder() + .body(Json.toJson(this)) + .extraParams(Extractor.extractValues(this)) + .build(); } } diff --git a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/Constants.java b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/Constants.java index 7f279b0..a579bf9 100644 --- a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/Constants.java +++ b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/Constants.java @@ -39,6 +39,13 @@ public final class Constants { public static final String GZIP_ENCODING = "gzip"; public static final int HEX = 16; + // 序列化字段 + public static final String CHANNEL_NO_SERIALIZED_PROPERTY = "channel_no"; + public static final String APP_NO_SERIALIZED_PROPERTY = "app_no"; + // 请求字段 + public static final String CHANNEL_NO_PROPERTY = "channelNo"; + public static final String APP_NO_PROPERTY = "appNo"; + // version public static final String V_1 = "1.0"; public static final String V_2 = "2.0"; diff --git a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/cipher/AbstractPrivacyDecryptor.java b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/cipher/AbstractPrivacyDecryptor.java index fe00183..51a6291 100644 --- a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/cipher/AbstractPrivacyDecryptor.java +++ b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/cipher/AbstractPrivacyDecryptor.java @@ -17,7 +17,7 @@ import static java.util.Objects.requireNonNull; /** * @author wangwei - * @date 2024/2/2 + * @since 2024/2/2 */ public abstract class AbstractPrivacyDecryptor implements PrivacyDecryptor { protected final String transformation; diff --git a/scfs-api-core/src/main/java/com/czcb/scfs/api/core/util/Extractor.java b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/util/Extractor.java new file mode 100644 index 0000000..80386eb --- /dev/null +++ b/scfs-api-core/src/main/java/com/czcb/scfs/api/core/util/Extractor.java @@ -0,0 +1,48 @@ +package com.czcb.scfs.api.core.util; + +import com.czcb.scfs.api.core.ApiRequest; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static com.czcb.scfs.api.core.Constants.*; + +/** + * @author wangwei + * @since 2024/7/30 + */ +public class Extractor { + private Extractor() { + } + + public static Map extractValues(Map body) { + Map params = new HashMap<>(); + if (Objects.isNull(body)) { + return params; + } + + addParamValue(params, CHANNEL_NO, body.get(CHANNEL_NO_SERIALIZED_PROPERTY)); + addParamValue(params, APP_NO, body.get(APP_NO_SERIALIZED_PROPERTY)); + return params; + } + + public static Map extractValues(ApiRequest body) { + Map params = new HashMap<>(); + if (Objects.isNull(body)) { + return params; + } + + addParamValue(params, CHANNEL_NO, ObjReflect.getValueAsString(body, CHANNEL_NO_PROPERTY)); + addParamValue(params, APP_NO, ObjReflect.getValueAsString(body, APP_NO_PROPERTY)); + return params; + } + + public static void addParamValue(Map params, String key, Object value) { + if (Objects.isNull(params) || Objects.isNull(key) || Objects.isNull(value)) { + return; + } + + params.put(key, value.toString()); + } +} diff --git a/scfs-api-core/src/test/java/com/czcb/scfs/api/core/ConstantsTest.java b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/ConstantsTest.java index 10d9d7c..d9eb405 100644 --- a/scfs-api-core/src/test/java/com/czcb/scfs/api/core/ConstantsTest.java +++ b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/ConstantsTest.java @@ -26,6 +26,10 @@ class ConstantsTest { Assertions.assertEquals("gzip", Constants.GZIP_ENCODING); Assertions.assertEquals("Content-Encoding", Constants.CONTENT_ENCODING); Assertions.assertEquals("User-Agent", Constants.USER_AGENT); + Assertions.assertEquals("channelNo", Constants.CHANNEL_NO_PROPERTY); + Assertions.assertEquals("channel_no", Constants.CHANNEL_NO_SERIALIZED_PROPERTY); + Assertions.assertEquals("appNo", Constants.APP_NO_PROPERTY); + Assertions.assertEquals("app_no", Constants.APP_NO_SERIALIZED_PROPERTY); Assertions.assertEquals(16, Constants.HEX); Assertions.assertEquals("1.0", Constants.V_1); diff --git a/scfs-api-core/src/test/java/com/czcb/scfs/api/core/http/client/ApacheHttpclientMapParamTest.java b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/http/client/ApacheHttpclientMapParamTest.java new file mode 100644 index 0000000..7af4d8b --- /dev/null +++ b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/http/client/ApacheHttpclientMapParamTest.java @@ -0,0 +1,294 @@ +package com.czcb.scfs.api.core.http.client; + +import com.czcb.scfs.api.core.*; +import com.czcb.scfs.api.core.cipher.*; +import com.czcb.scfs.api.core.http.*; +import com.czcb.scfs.api.core.util.DateTimes; +import com.czcb.scfs.api.core.util.Nonce; +import com.czcb.scfs.api.core.util.Strings; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockserver.client.MockServerClient; +import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.junit.jupiter.MockServerSettings; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import static com.czcb.scfs.api.core.Constants.*; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +@ExtendWith(MockServerExtension.class) +@MockServerSettings(ports = {8888}) +class ApacheHttpclientMapParamTest { + private MockServerClient client; + + @BeforeEach + public void beforeEachLifecyleMethod(MockServerClient client) { + this.client = client; + } + + /** + * 构建配置 + */ + private Profile profile(boolean compressionEnabled) { + PrivateKey privateKey = KeyText.loadTestPrivateKeyRSA(); + X509Certificate certificate = KeyText.loadTestRSA(); + List list = new ArrayList<>(); + list.add(certificate); + CertificateProvider certificateProvider = new LocalCertificateProvider(list); + Privacy privacy = new TestPrivacy(privateKey, certificateProvider); + Signature signature = new DefaultSignature(certificateProvider, + new TestSigner(privateKey, "6CDDAA92CAD75998325027647847330C1756291"), + new TestVerifier(certificateProvider), + new DefaultSequencer()); + + return new TestProfile( + privacy, signature, new DefaultChannel.Builder() + .channelNo("0000") + .appNo("100000") + .build(), new DefaultHttpProfile.Builder() + .online(false) + .logLevel(LogLevel.BASIC) + .compressionEnabled(compressionEnabled) + .host("http://127.0.0.1:8888") + .build() + ); + } + + @Test + void doRemoteExecute() { + TestResponse mockResponse = new TestResponse(); + mockResponse.setName("123456"); + // mock 结果 + ApiClient apiClient = mock(mockResponse.toJsonResponse().getBody(), false); + + // 请求参数 + TestRequest request = new TestRequest(); + request.setAppNo("111"); + request.setChannelNo("0000"); + request.setSerialNo("00000000000000000000"); + request.setTransDate(""); + request.setTransTradeTime("22"); + + // 发起调用 + HttpResponse response = apiClient.post("/mock/xxx", new HttpHeaders(), request.getMap(), TestResponse.class); + + assertThatJson("{\"name\":\"123456\"}").isEqualTo(response.getServiceResponse().toJsonResponse().getBody()); + } + + @Test + void doRemoteExecute2() { + TestResponse mockResponse = new TestResponse(); + mockResponse.setName("123456"); + ApiClient apiClient = mock(mockResponse.toJsonResponse().getBody(), true); + + TestRequest request = new TestRequest(); + RequestBody requestBody = new JsonRequestBody.Builder() + .body(new Gson().toJson(request)) + .build(); + + HttpResponse response = apiClient.post("/mock/xxx", new HttpHeaders(), requestBody, TestResponse.class); + + assertThatJson("{\"name\":\"123456\"}").isEqualTo(response.getServiceResponse().toJsonResponse().getBody()); + } + + private ApiClient mock(String body, boolean compressionEnabled) { + ApiClient apiClient = ApiClientBuilder.custom() + .profile(profile(compressionEnabled)) + .build(); + + // 对称密钥 + byte[] secret = apiClient.getProfile().getPrivacy().getSecretCipher().getSecretKey(); + // 加密响应报文 + String responseBody = apiClient.getProfile().getPrivacy().getSecretCipher().encrypt(secret, body.getBytes(StandardCharsets.UTF_8)); + + // 加密对称密钥 + String secretKey = apiClient.getProfile().getPrivacy().getEncryptor().encrypt(Strings.toStr(secret)); + + org.mockserver.model.HttpResponse mock = response() + .withHeader(NONCE, Nonce.ofNonce()) + .withHeader(SECRET_KEY, secretKey) + .withHeader(REQUEST_ID, Nonce.ofNonce()) + .withHeader(BANK_CERTIFICATE_SERIAL, "6CDDAA92CAD75998325027647847330C1756291") + .withHeader(CHANNEL_CERTIFICATE_SERIAL, "6CDDAA92CAD75998325027647847330C1756291") + .withHeader(TIMESTAMP, DateTimes.ofTimestamp()); + + if (compressionEnabled) { + mock.withHeader("Content-Encoding", "gzip"); + mock.withBody(compress(responseBody)); + } else { + mock.withBody(responseBody); + } + + String buildAuth = NONCE + "=" + mock.getHeader(NONCE).get(0) + "," + + TIMESTAMP + "=" + mock.getHeader(TIMESTAMP).get(0) + "," + + BANK_CERTIFICATE_SERIAL + "=" + mock.getHeader(BANK_CERTIFICATE_SERIAL).get(0) + "," + + CHANNEL_CERTIFICATE_SERIAL + "=" + mock.getHeader(CHANNEL_CERTIFICATE_SERIAL).get(0) + "," + + SECRET_KEY + "=" + secretKey; + + String message = buildAuth + "\n" + responseBody + "\n"; + mock.withHeader(SIGNATURE, apiClient.getProfile().getSignature().getSigner().sign(message).getSignature()); + + client.when(request() + .withHeader("Authorization", "SCFS-SHA256withRSA X-SCFS-Channel-No=0000,X-SCFS-App-No=100000,X-SCFS-Signature=.*") + .withHeader("X-SCFS-Nonce", ".*") + .withHeader("X-SCFS-Secret-Key", ".*") + .withHeader("X-SCFS-Serial", "6CDDAA92CAD75998325027647847330C1756291") + .withHeader("X-SCFS-Channel-Serial", "6CDDAA92CAD75998325027647847330C1756291") + .withHeader("X-SCFS-Timestamp", ".*") + .withMethod(HttpMethod.POST.getUpperName()) + .withPath("/mock/xxx") + ).respond(mock); + + return apiClient; + } + + public static byte[] compress(String str) { + ByteArrayOutputStream out = null; + GZIPOutputStream gzip = null; + try { + if (str == null || str.isEmpty()) { + return null; + } + out = new ByteArrayOutputStream(); + gzip = new GZIPOutputStream(out); + gzip.write(str.getBytes(StandardCharsets.UTF_8)); + gzip.finish(); + return out.toByteArray(); + } catch (Exception e) { + return null; + } finally { + try { + if (out != null) { + out.close(); + } + if (gzip != null) { + gzip.close(); + } + } catch (Exception ignored) { + } + } + } + + @Test + void getHttpClientInfo() { + TestResponse mockResponse = new TestResponse(); + ApacheHttpclient apiClient = (ApacheHttpclient) mock(mockResponse.toJsonResponse().getBody(), false); + Assertions.assertEquals("apache-http-client-5/5.2.3", apiClient.getHttpClientInfo()); + } + + @Test + void getHttpVersion() { + TestResponse mockResponse = new TestResponse(); + ApacheHttpclient apiClient = (ApacheHttpclient) mock(mockResponse.toJsonResponse().getBody(), false); + Assertions.assertEquals("HTTP/1.1", apiClient.getHttpVersion()); + } + + public static class TestRequest implements ApiRequest { + /** + * 渠道编号 + */ + @SerializedName("channel_no") + private String channelNo; + + /** + * 应用编号 + */ + @SerializedName("app_no") + private String appNo; + + /** + * 流水号 + */ + @SerializedName("serial_no") + private String serialNo; + + /** + * 操作日期 + */ + @SerializedName("trans_date") + private String transDate; + + /** + * 操作时间 + */ + @SerializedName("trans_tradetime") + private String transTradeTime; + + public String getChannelNo() { + return channelNo; + } + + public void setChannelNo(String channelNo) { + this.channelNo = channelNo; + } + + public String getAppNo() { + return appNo; + } + + public void setAppNo(String appNo) { + this.appNo = appNo; + } + + public String getSerialNo() { + return serialNo; + } + + public void setSerialNo(String serialNo) { + this.serialNo = serialNo; + } + + public String getTransDate() { + return transDate; + } + + public void setTransDate(String transDate) { + this.transDate = transDate; + } + + public String getTransTradeTime() { + return transTradeTime; + } + + public void setTransTradeTime(String transTradeTime) { + this.transTradeTime = transTradeTime; + } + + public Map getMap() { + Map map = new HashMap<>(); + map.put("channel_no", getChannelNo()); + map.put("app_no", getAppNo()); + map.put("serial_no", getSerialNo()); + map.put("trans_date", getTransDate()); + map.put("trans_trade_time", getTransTradeTime()); + return map; + } + } + + public static class TestResponse implements ApiResponse { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} \ No newline at end of file diff --git a/scfs-api-core/src/test/java/com/czcb/scfs/api/core/util/ExtractorTest.java b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/util/ExtractorTest.java new file mode 100644 index 0000000..9d9918c --- /dev/null +++ b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/util/ExtractorTest.java @@ -0,0 +1,99 @@ +package com.czcb.scfs.api.core.util; + +import com.czcb.scfs.api.core.ApiRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +class ExtractorTest { + + @Test + void extractValues() { + Map map = null; + Map data = Extractor.extractValues(map); + Assertions.assertNotNull(data); + Assertions.assertTrue(data.isEmpty()); + + Map map2 = new HashMap<>(); + map2.put("channel_no", "444"); + map2.put("app_no", "55"); + data = Extractor.extractValues(map2); + + Assertions.assertNotNull(data); + Assertions.assertEquals("444", data.get("X-SCFS-Channel-No")); + Assertions.assertEquals("55", data.get("X-SCFS-App-No")); + + Map map3 = new HashMap<>(); + map3.put("channel_no", null); + map3.put("app_no", null); + data = Extractor.extractValues(map3); + + Assertions.assertNotNull(data); + Assertions.assertTrue(data.isEmpty()); + } + + @Test + void testExtractValues() { + TestApiRequest apiRequest = null; + Map data = Extractor.extractValues(apiRequest); + Assertions.assertNotNull(data); + Assertions.assertTrue(data.isEmpty()); + + TestApiRequest apiRequest2 = new TestApiRequest(); + apiRequest2.setAppNo("111"); + apiRequest2.setChannelNo("222"); + data = Extractor.extractValues(apiRequest2); + + Assertions.assertNotNull(data); + Assertions.assertEquals("222", data.get("X-SCFS-Channel-No")); + Assertions.assertEquals("111", data.get("X-SCFS-App-No")); + + TestApiRequest apiRequest3 = new TestApiRequest(); + apiRequest3.setAppNo(null); + apiRequest3.setChannelNo(null); + data = Extractor.extractValues(apiRequest3); + + Assertions.assertNotNull(data); + Assertions.assertTrue(data.isEmpty()); + } + + @Test + void addParamValue() { + Map map = new HashMap<>(); + Extractor.addParamValue(map, null, null); + Assertions.assertTrue(map.isEmpty()); + + Extractor.addParamValue(map, "aa", null); + Assertions.assertTrue(map.isEmpty()); + + Extractor.addParamValue(map, null, "test"); + Assertions.assertTrue(map.isEmpty()); + + Extractor.addParamValue(map, "key", "test"); + Assertions.assertFalse(map.isEmpty()); + Assertions.assertEquals("test", map.get("key")); + } + + static class TestApiRequest implements ApiRequest { + private String channelNo; + private String appNo; + + public String getChannelNo() { + return channelNo; + } + + public void setChannelNo(String channelNo) { + this.channelNo = channelNo; + } + + public String getAppNo() { + return appNo; + } + + public void setAppNo(String appNo) { + this.appNo = appNo; + } + } +} \ No newline at end of file diff --git a/scfs-api-core/src/test/java/com/czcb/scfs/api/core/util/JsonTest.java b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/util/JsonTest.java new file mode 100644 index 0000000..a79cbae --- /dev/null +++ b/scfs-api-core/src/test/java/com/czcb/scfs/api/core/util/JsonTest.java @@ -0,0 +1,59 @@ +package com.czcb.scfs.api.core.util; + +import com.czcb.scfs.api.core.ApiRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +class JsonTest { + + @Test + void toJson() { + Map map = new HashMap<>(); + map.put("key", "value"); + map.put("key2", "value2"); + Assertions.assertEquals("{\"key2\":\"value2\",\"key\":\"value\"}", Json.toJson(map)); + } + + @Test + void fromJson() { + String json = "{\"key\":\"value\",\"key2\":\"value2\",\"key\":\"value\"}"; + TestRequest request = Json.fromJson(json.getBytes(StandardCharsets.UTF_8), TestRequest.class); + Assertions.assertNotNull(request); + Assertions.assertEquals("value", request.getKey()); + Assertions.assertEquals("value2", request.getKey2()); + } + + @Test + void fromJsonObject() { + String json = "{\"key\":\"value\",\"key2\":\"value2\",\"key\":\"value\"}"; + TestRequest request = Json.fromJson(json, TestRequest.class); + Assertions.assertNotNull(request); + Assertions.assertEquals("value", request.getKey()); + Assertions.assertEquals("value2", request.getKey2()); + } + + public static class TestRequest implements ApiRequest { + private String key; + private String key2; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } + } +} \ No newline at end of file