diff --git a/src/main/java/com/eactive/eai/adapter/http/client/impl/filter/ConfigurableEncFieldOutCryptoFilter.java b/src/main/java/com/eactive/eai/adapter/http/client/impl/filter/ConfigurableEncFieldOutCryptoFilter.java new file mode 100644 index 0000000..698d01c --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/client/impl/filter/ConfigurableEncFieldOutCryptoFilter.java @@ -0,0 +1,55 @@ +package com.eactive.eai.adapter.http.client.impl.filter; + +import java.util.Properties; + +import com.eactive.eai.adapter.http.client.impl.HttpClient5AdapterServiceRest; +import com.eactive.eai.adapter.http.filter.CryptoFilterPathConfig; +import com.eactive.eai.common.util.JacksonUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * 어댑터그룹별 암복호화 JSON 경로를 PropManager "CryptoFilter" 그룹에서 읽는 송신 암복호화 필터. + * + * 외부기관마다 경로만 다른 필터 클래스를 만드는 대신 이 필터 + 프로퍼티 설정으로 대체한다. + * 경로 키와 기본값은 {@link CryptoFilterPathConfig} 참조. + * OutCryptoFilter 는 경로를 tempProp 에서 읽으므로 경로는 tempProp 에 반영한다. + * + * doPreFilter : 송신 요청 암호화. HEADER_GROUP 으로 지정된 헤더 그룹은 암호화 대상에서 빼고 암호화 후 다시 붙인다. + * doPostFilter : 수신 응답 복호화 + * + * 필터 타입 등록 (FQCN): + * com.eactive.eai.adapter.http.client.impl.filter.ConfigurableEncFieldOutCryptoFilter + */ +public class ConfigurableEncFieldOutCryptoFilter implements HttpClientAdapterFilter { + + private final OutCryptoFilter filter = new OutCryptoFilter(); + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, + Properties tempProp) throws Exception { + String headerGroupName = prop.getProperty(HttpClient5AdapterServiceRest.HEADER_GROUP); + + ObjectNode root = (ObjectNode) JacksonUtil.readTree(message); + JsonNode header = null; + if (headerGroupName != null) { + header = root.get(headerGroupName); + root.remove(headerGroupName); + } + + Object enc = filter.doPreFilter(adptGrpName, adptName, prop, root, + CryptoFilterPathConfig.apply(adptGrpName, tempProp)); + ObjectNode encJson = (ObjectNode) JacksonUtil.readTree(enc); + if (header != null) { + encJson.set(headerGroupName, header); + } + return encJson; + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message, + Properties tempProp) throws Exception { + return filter.doPostFilter(adptGrpName, adptName, prop, message, + CryptoFilterPathConfig.apply(adptGrpName, tempProp)); + } +} diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/ConfigurableEncFieldInCryptoFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/ConfigurableEncFieldInCryptoFilter.java new file mode 100644 index 0000000..6734ed6 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/ConfigurableEncFieldInCryptoFilter.java @@ -0,0 +1,64 @@ +package com.eactive.eai.adapter.http.dynamic.filter; + +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; + +import com.eactive.eai.adapter.http.dynamic.impl.HttpAdapterServiceRest; +import com.eactive.eai.adapter.http.filter.CryptoFilterPathConfig; +import com.eactive.eai.common.util.JacksonUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * 어댑터그룹별 암복호화 JSON 경로를 PropManager "CryptoFilter" 그룹에서 읽는 수신 암복호화 필터. + * + * 외부기관마다 경로만 다른 필터 클래스를 만드는 대신 이 필터 + 프로퍼티 설정으로 대체한다. + * 경로 키와 기본값은 {@link CryptoFilterPathConfig} 참조. + * + * doPreFilter : 수신 요청 복호화 + * doPostFilter : 송신 응답 암호화. HEADER_GROUP 으로 지정된 헤더 그룹은 암호화 대상에서 빼고 암호화 후 다시 붙인다. + * + * 필터 타입 등록 (FQCN): + * com.eactive.eai.adapter.http.dynamic.filter.ConfigurableEncFieldInCryptoFilter + */ +public class ConfigurableEncFieldInCryptoFilter implements HttpAdapterFilter { + + private final InCryptoFilter filter = new InCryptoFilter(); + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + return filter.doPreFilter(adptGrpName, adptName, message, CryptoFilterPathConfig.apply(adptGrpName, prop), + request, response); + } + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + ObjectNode root = (ObjectNode) JacksonUtil.readTree(resultMessage); + String headerGroupName = prop.getProperty(HttpAdapterServiceRest.HEADER_GROUP); + JsonNode header = null; + if (headerGroupName != null) { + header = root.get(headerGroupName); + root.remove(headerGroupName); + } + + Object enc = filter.doPostFilter(adptGrpName, adptName, root, + CryptoFilterPathConfig.apply(adptGrpName, prop), request, response); + ObjectNode encJson = (ObjectNode) JacksonUtil.readTree(enc); + if (header != null) { + encJson.set(headerGroupName, header); + } + return encJson; + + } catch (Exception e) { + throw new FilterCryptoException("암호화 오류", InCryptoFilter.ERROR_ENC_FAIL, + HttpStatus.INTERNAL_SERVER_ERROR.value(), e); + } + } +} diff --git a/src/main/java/com/eactive/eai/adapter/http/filter/CryptoFilterPathConfig.java b/src/main/java/com/eactive/eai/adapter/http/filter/CryptoFilterPathConfig.java new file mode 100644 index 0000000..9d77d31 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/filter/CryptoFilterPathConfig.java @@ -0,0 +1,70 @@ +package com.eactive.eai.adapter.http.filter; + +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.common.property.PropManager; + +/** + * 설정 기반 암복호화 필터의 JSON 경로를 어댑터그룹별로 PropManager 에서 읽어 반영한다. + * + * 프로퍼티 그룹 "CryptoFilter", 키는 "{어댑터그룹명}.{CRYPTO_*_PATH}". + * + *
+ * {어댑터그룹명}.CRYPTO_DEC_FROM_PATH = /encryptedData (기본값) 복호화할 암호문 필드
+ * {어댑터그룹명}.CRYPTO_DEC_TO_PATH = / (기본값) 복호화 결과 반영 위치. "/" 면 암호문 필드를 지우고 body 에 병합
+ * {어댑터그룹명}.CRYPTO_ENC_FROM_PATH = / (기본값) 암호화 대상. "/" 면 body 전체
+ * {어댑터그룹명}.CRYPTO_ENC_TO_PATH = /encryptedData (기본값) 암호문을 넣을 필드
+ *
+ *
+ * 필터 인스턴스는 팩토리에 캐시되어 모든 어댑터그룹이 공유하므로 설정은 호출마다 읽는다.
+ *
+ * @see com.eactive.eai.adapter.http.dynamic.filter.ConfigurableEncFieldInCryptoFilter
+ * @see com.eactive.eai.adapter.http.client.impl.filter.ConfigurableEncFieldOutCryptoFilter
+ */
+public final class CryptoFilterPathConfig {
+
+ public static final String PROP_GROUP = "CryptoFilter";
+
+ static final String DEFAULT_DEC_FROM_PATH = "/encryptedData";
+ static final String DEFAULT_DEC_TO_PATH = AbstractCryptoFilter.PATH_ROOT;
+ static final String DEFAULT_ENC_FROM_PATH = AbstractCryptoFilter.PATH_ROOT;
+ static final String DEFAULT_ENC_TO_PATH = "/encryptedData";
+
+ private static final String[] PATH_KEYS = { AbstractCryptoFilter.PROP_DEC_FROM_PATH,
+ AbstractCryptoFilter.PROP_DEC_TO_PATH, AbstractCryptoFilter.PROP_ENC_FROM_PATH,
+ AbstractCryptoFilter.PROP_ENC_TO_PATH };
+
+ private CryptoFilterPathConfig() {
+ }
+
+ /**
+ * target 에 경로 프로퍼티 4개를 반영한다. (수신 필터는 prop, 송신 필터는 tempProp)
+ * 기본값을 먼저 넣고 CryptoFilter 그룹에 설정된 값으로 덮어쓴다. 값이 비어 있는 키는 기본값을 유지한다.
+ *
+ * @param adptGrpName 어댑터그룹명
+ * @param target 경로를 반영할 Properties
+ * @return target
+ */
+ public static Properties apply(String adptGrpName, Properties target) {
+ target.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, DEFAULT_DEC_FROM_PATH);
+ target.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, DEFAULT_DEC_TO_PATH);
+ target.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, DEFAULT_ENC_FROM_PATH);
+ target.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, DEFAULT_ENC_TO_PATH);
+
+ // 그룹이 없을 때 getProperties() 는 예외를 던지므로 먼저 확인한다.
+ PropManager propManager = PropManager.getInstance();
+ if (StringUtils.isBlank(adptGrpName) || !propManager.isContainProperties(PROP_GROUP)) {
+ return target;
+ }
+
+ for (String key : PATH_KEYS) {
+ String value = propManager.getProperty(PROP_GROUP, adptGrpName + "." + key, null);
+ if (StringUtils.isNotBlank(value)) {
+ target.setProperty(key, value.trim());
+ }
+ }
+ return target;
+ }
+}
diff --git a/src/test/java/com/eactive/eai/adapter/http/filter/CryptoFilterPathConfigTest.java b/src/test/java/com/eactive/eai/adapter/http/filter/CryptoFilterPathConfigTest.java
new file mode 100644
index 0000000..35b8d79
--- /dev/null
+++ b/src/test/java/com/eactive/eai/adapter/http/filter/CryptoFilterPathConfigTest.java
@@ -0,0 +1,155 @@
+package com.eactive.eai.adapter.http.filter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import java.lang.reflect.Field;
+import java.util.Properties;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.context.ApplicationContext;
+
+import com.eactive.eai.common.property.PropManager;
+import com.eactive.eai.common.util.ApplicationContextProvider;
+
+/**
+ * CryptoFilterPathConfig 단위테스트
+ *
+ * PropManager 는 ApplicationContextProvider 의 context 에 mock 을 리플렉션으로 주입해 격리한다.
+ */
+class CryptoFilterPathConfigTest {
+
+ private static final String GROUP = "_TST_IN_RST_SyS";
+
+ private PropManager propManager;
+
+ private Field contextField;
+
+ private Object previousContext;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ propManager = Mockito.mock(PropManager.class);
+
+ ApplicationContext ctx = Mockito.mock(ApplicationContext.class);
+ Mockito.when(ctx.getBean(PropManager.class)).thenReturn(propManager);
+
+ contextField = ApplicationContextProvider.class.getDeclaredField("context");
+ contextField.setAccessible(true);
+ previousContext = contextField.get(null);
+ contextField.set(null, ctx);
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ contextField.set(null, previousContext);
+ }
+
+ /** CryptoFilter 그룹이 있고 어댑터그룹의 key 에 value 가 설정된 상태로 만든다. */
+ private void configure(String adapterGroupName, String key, String value) {
+ Mockito.when(propManager.isContainProperties(CryptoFilterPathConfig.PROP_GROUP)).thenReturn(true);
+ Mockito.when(propManager.getProperty(CryptoFilterPathConfig.PROP_GROUP, adapterGroupName + "." + key, null))
+ .thenReturn(value);
+ }
+
+ private void assertDefaults(Properties p) {
+ assertEquals("/encryptedData", p.getProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH));
+ assertEquals("/", p.getProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH));
+ assertEquals("/", p.getProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH));
+ assertEquals("/encryptedData", p.getProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH));
+ }
+
+ @Test
+ @DisplayName("1. CryptoFilter 그룹이 없으면 4개 경로 모두 기본값")
+ void 그룹없음_기본값() {
+ Properties p = new Properties();
+
+ Properties result = CryptoFilterPathConfig.apply(GROUP, p);
+
+ assertSame(p, result);
+ assertDefaults(p);
+ Mockito.verify(propManager, Mockito.never()).getProperty(anyString(), anyString(), any());
+ }
+
+ @Test
+ @DisplayName("2. 그룹은 있지만 어댑터그룹 키가 없으면 기본값")
+ void 키없음_기본값() {
+ Mockito.when(propManager.isContainProperties(CryptoFilterPathConfig.PROP_GROUP)).thenReturn(true);
+ Properties p = new Properties();
+
+ CryptoFilterPathConfig.apply(GROUP, p);
+
+ assertDefaults(p);
+ }
+
+ @Test
+ @DisplayName("3. 4개 키가 모두 설정되면 모두 반영")
+ void 네개키_반영() {
+ configure(GROUP, AbstractCryptoFilter.PROP_DEC_FROM_PATH, "/encrypted_data");
+ configure(GROUP, AbstractCryptoFilter.PROP_DEC_TO_PATH, "/data");
+ configure(GROUP, AbstractCryptoFilter.PROP_ENC_FROM_PATH, "/data");
+ configure(GROUP, AbstractCryptoFilter.PROP_ENC_TO_PATH, "/encrypted_data");
+ Properties p = new Properties();
+
+ CryptoFilterPathConfig.apply(GROUP, p);
+
+ assertEquals("/encrypted_data", p.getProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH));
+ assertEquals("/data", p.getProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH));
+ assertEquals("/data", p.getProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH));
+ assertEquals("/encrypted_data", p.getProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH));
+ }
+
+ @Test
+ @DisplayName("4. 빈 값은 무시하고 기본값 유지, 앞뒤 공백은 제거")
+ void 빈값무시_공백제거() {
+ configure(GROUP, AbstractCryptoFilter.PROP_DEC_FROM_PATH, " ");
+ configure(GROUP, AbstractCryptoFilter.PROP_ENC_TO_PATH, " /encrypted_data ");
+ Properties p = new Properties();
+
+ CryptoFilterPathConfig.apply(GROUP, p);
+
+ assertEquals("/encryptedData", p.getProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH));
+ assertEquals("/encrypted_data", p.getProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH));
+ }
+
+ @Test
+ @DisplayName("5. 다른 어댑터그룹의 키는 반영하지 않는다")
+ void 다른그룹_미반영() {
+ configure("_OTHER_IN_RST_SyS", AbstractCryptoFilter.PROP_DEC_FROM_PATH, "/encrypted_data");
+ Properties p = new Properties();
+
+ CryptoFilterPathConfig.apply(GROUP, p);
+
+ assertDefaults(p);
+ }
+
+ @Test
+ @DisplayName("6. 어댑터그룹명이 없으면 PropManager 를 조회하지 않고 기본값")
+ void 그룹명없음_기본값() {
+ Mockito.when(propManager.isContainProperties(CryptoFilterPathConfig.PROP_GROUP)).thenReturn(true);
+ Properties p = new Properties();
+
+ CryptoFilterPathConfig.apply(null, p);
+
+ assertDefaults(p);
+ Mockito.verify(propManager, Mockito.never()).getProperty(anyString(), anyString(), any());
+ }
+
+ @Test
+ @DisplayName("7. 앞 단계에서 들어온 경로 값은 기본값/설정값으로 덮어쓴다")
+ void 기존값_덮어씀() {
+ Properties p = new Properties();
+ p.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, "/leftover");
+ p.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, "/leftover");
+
+ CryptoFilterPathConfig.apply(GROUP, p);
+
+ assertDefaults(p);
+ }
+}