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); + } +}