설정 기반 EncField 암복호화 필터 추가

- 기관마다 경로만 다른 필터 클래스를 만들지 않도록 경로를 PropManager 에서 읽는 공통 필터 추가
  ConfigurableEncFieldInCryptoFilter(수신), ConfigurableEncFieldOutCryptoFilter(송신)
- PropManager 그룹 CryptoFilter, 키 {어댑터그룹명}.CRYPTO_DEC_FROM_PATH / DEC_TO_PATH / ENC_FROM_PATH / ENC_TO_PATH
- 설정이 없으면 기존 EncField 필터와 같은 경로(/encryptedData, /, /, /encryptedData)
This commit is contained in:
curry772
2026-09-14 15:45:22 +09:00
parent 64bf89582a
commit c8f7bfe1b1
4 changed files with 344 additions and 0 deletions
@@ -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));
}
}
@@ -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);
}
}
}
@@ -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}".
*
* <pre>
* {어댑터그룹명}.CRYPTO_DEC_FROM_PATH = /encryptedData (기본값) 복호화할 암호문 필드
* {어댑터그룹명}.CRYPTO_DEC_TO_PATH = / (기본값) 복호화 결과 반영 위치. "/" 면 암호문 필드를 지우고 body 에 병합
* {어댑터그룹명}.CRYPTO_ENC_FROM_PATH = / (기본값) 암호화 대상. "/" 면 body 전체
* {어댑터그룹명}.CRYPTO_ENC_TO_PATH = /encryptedData (기본값) 암호문을 넣을 필드
* </pre>
*
* 필터 인스턴스는 팩토리에 캐시되어 모든 어댑터그룹이 공유하므로 설정은 호출마다 읽는다.
*
* @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;
}
}
@@ -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);
}
}