From 580686886c0dbcbe7bbd8165e027f0b6777acfd2 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 7 May 2026 15:03:47 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20HTTP=20=EC=96=B4=EB=8C=91=ED=84=B0=20?= =?UTF-8?q?=EC=95=94=EB=B3=B5=ED=98=B8=ED=99=94=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20ReloadCryptoModuleCommand=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CryptoFilter (abstract): BODY/FIELD scope 암복호화, buildRuntimeContext 추상화 - String/byte[]/JSONObject 타입 message 처리 (toBodyString) - STATIC/DYNAMIC 키 모듈 공통 지원 (CryptoModuleManager가 내부 분기) - ReloadCryptoModuleCommand: 로깅 추가, 성공 시 "success" 반환, 에러코드 정의 Co-Authored-By: Claude Sonnet 4.6 --- .../http/dynamic/filter/CryptoFilter.java | 222 ++++++++++++++++++ .../security/ReloadCryptoModuleCommand.java | 11 +- 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilter.java diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilter.java new file mode 100644 index 0000000..38ad7d1 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilter.java @@ -0,0 +1,222 @@ +package com.eactive.eai.adapter.http.dynamic.filter; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.common.security.CryptoModuleService; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.util.JsonPathUtil; + +/** + * HTTP 어댑터 요청/응답 암복호화 필터 기반 클래스. + * + * 서브클래스는 {@link #buildRuntimeContext}를 구현하여 + * DYNAMIC 키 도출에 필요한 컨텍스트 맵을 제공한다. + * STATIC 키 모듈을 사용하는 경우 빈 Map을 반환하면 된다. + * (CryptoModuleManager가 KEY_SOURCE_TYPE=STATIC이면 runtimeContext를 무시하고 고정 키를 사용한다.) + * + * 어댑터 프로퍼티 키: + * crypto.module.name 필수. DB에 등록된 암호화 모듈명. + * crypto.scope BODY | FIELD (기본 BODY) + * BODY : 메시지 전체를 암복호화 (Base64 인코딩/디코딩) + * FIELD : 특정 JSON 경로의 값만 암복호화 + * crypto.dec.enabled Y | N (기본 Y). doPreFilter(요청) 복호화 수행 여부. + * crypto.enc.enabled Y | N (기본 N). doPostFilter(응답) 암호화 수행 여부. + * + * FIELD 범위 전용: + * crypto.dec.from.path 복호화 대상 JSON 경로 (예: /encryptedData) + * crypto.dec.to.path 복호화 결과 반영 경로. "/" = 전체 body 교체. + * crypto.enc.from.path 암호화 대상 JSON 경로. "/" = 전체 body 암호화. + * crypto.enc.to.path 암호화 결과(Base64)를 넣을 JSON 경로 (예: /encryptedData) + */ +public abstract class CryptoFilter implements HttpAdapterFilter { + + protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + public static final String PROP_MODULE_NAME = "crypto.module.name"; + public static final String PROP_SCOPE = "crypto.scope"; + public static final String PROP_DEC_ENABLED = "crypto.dec.enabled"; + public static final String PROP_ENC_ENABLED = "crypto.enc.enabled"; + public static final String PROP_DEC_FROM_PATH = "crypto.dec.from.path"; + public static final String PROP_DEC_TO_PATH = "crypto.dec.to.path"; + public static final String PROP_ENC_FROM_PATH = "crypto.enc.from.path"; + public static final String PROP_ENC_TO_PATH = "crypto.enc.to.path"; + + protected static final String SCOPE_BODY = "BODY"; + protected static final String SCOPE_FIELD = "FIELD"; + protected static final String PATH_ROOT = "/"; + + // ------------------------------------------------------------------ + // 추상 메서드 + // ------------------------------------------------------------------ + + /** + * DYNAMIC 키 도출에 필요한 runtimeContext 맵을 구성한다. + * STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다. + */ + protected abstract Map buildRuntimeContext(Properties prop, HttpServletRequest request); + + // ------------------------------------------------------------------ + // doPreFilter: 요청 복호화 + // ------------------------------------------------------------------ + + @Override + public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + + if (!isEnabled(prop, PROP_DEC_ENABLED, "Y")) { + return message; + } + + String moduleName = required(prop, PROP_MODULE_NAME); + String scope = prop.getProperty(PROP_SCOPE, SCOPE_BODY).toUpperCase(); + String body = toBodyString(message); + Map runtimeCtx = buildRuntimeContext(prop, request); + + if (SCOPE_FIELD.equals(scope)) { + return decryptField(body, moduleName, runtimeCtx, + required(prop, PROP_DEC_FROM_PATH), + prop.getProperty(PROP_DEC_TO_PATH, PATH_ROOT)); + } + return decryptBody(body, moduleName, runtimeCtx); + } + + // ------------------------------------------------------------------ + // doPostFilter: 응답 암호화 + // ------------------------------------------------------------------ + + @Override + public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop, + HttpServletRequest request, HttpServletResponse response) throws Exception { + + if (!isEnabled(prop, PROP_ENC_ENABLED, "N")) { + return resultMessage; + } + + String moduleName = required(prop, PROP_MODULE_NAME); + String scope = prop.getProperty(PROP_SCOPE, SCOPE_BODY).toUpperCase(); + String body = toBodyString(resultMessage); + Map runtimeCtx = buildRuntimeContext(prop, request); + + if (SCOPE_FIELD.equals(scope)) { + return encryptField(body, moduleName, runtimeCtx, + prop.getProperty(PROP_ENC_FROM_PATH, PATH_ROOT), + required(prop, PROP_ENC_TO_PATH)); + } + return encryptBody(body, moduleName, runtimeCtx); + } + + // ------------------------------------------------------------------ + // 복호화 구현 + // ------------------------------------------------------------------ + + private String decryptBody(String body, String moduleName, Map runtimeCtx) throws Exception { + byte[] cipherBytes = Base64.getDecoder().decode(body.trim()); + byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, cipherBytes); + return new String(plainBytes, StandardCharsets.UTF_8); + } + + /** + * fromPath의 Base64 값을 복호화하여 toPath에 반영. + * toPath = "/" → 복호화된 텍스트로 body 전체 교체. + */ + private String decryptField(String body, String moduleName, Map runtimeCtx, + String fromPath, String toPath) throws Exception { + + String encBase64 = JsonPathUtil.getValueAtPath(body, fromPath); + if (StringUtils.isBlank(encBase64)) { + logger.warn("CryptoFilter] 복호화 대상 필드 없음: path=" + fromPath); + return body; + } + + byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim()); + byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, cipherBytes); + String plainText = new String(plainBytes, StandardCharsets.UTF_8); + + if (PATH_ROOT.equals(toPath)) { + return plainText; + } + return JsonPathUtil.setValueAtPath(body, toPath, plainText, false); + } + + // ------------------------------------------------------------------ + // 암호화 구현 + // ------------------------------------------------------------------ + + private String encryptBody(String body, String moduleName, Map runtimeCtx) throws Exception { + byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, + body.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(cipherBytes); + } + + /** + * fromPath 값을 암호화하여 toPath(Base64)에 반영. + * fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑. + */ + private String encryptField(String body, String moduleName, Map runtimeCtx, + String fromPath, String toPath) throws Exception { + + String plainText; + if (PATH_ROOT.equals(fromPath)) { + plainText = body; + } else { + plainText = JsonPathUtil.getValueAtPath(body, fromPath); + if (StringUtils.isBlank(plainText)) { + logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath); + return body; + } + } + + byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, + plainText.getBytes(StandardCharsets.UTF_8)); + String encBase64 = Base64.getEncoder().encodeToString(cipherBytes); + + if (PATH_ROOT.equals(fromPath)) { + String fieldName = toPath.replaceFirst("^/", ""); + return "{\"" + fieldName + "\":\"" + encBase64 + "\"}"; + } + return JsonPathUtil.setValueAtPath(body, toPath, encBase64, false); + } + + // ------------------------------------------------------------------ + // 유틸 + // ------------------------------------------------------------------ + + /** + * 필터로 전달되는 message 객체를 JSON 문자열로 변환한다. + *
    + *
  • {@code String} — 그대로 반환
  • + *
  • {@code byte[]} — UTF-8 디코딩
  • + *
  • {@code JSONObject} / 기타 — {@code toString()} 호출 + * (json-simple JSONObject는 toString()이 toJSONString()을 위임함)
  • + *
+ */ + protected String toBodyString(Object message) { + if (message instanceof String) { + return (String) message; + } + if (message instanceof byte[]) { + return new String((byte[]) message, StandardCharsets.UTF_8); + } + return message.toString(); + } + + private boolean isEnabled(Properties prop, String key, String defaultVal) { + return !"N".equalsIgnoreCase(prop.getProperty(key, defaultVal)); + } + + protected String required(Properties prop, String key) { + String v = prop.getProperty(key); + if (StringUtils.isBlank(v)) { + throw new IllegalArgumentException("CryptoFilter 필수 프로퍼티 누락: " + key); + } + return v; + } +} diff --git a/src/main/java/com/eactive/eai/agent/security/ReloadCryptoModuleCommand.java b/src/main/java/com/eactive/eai/agent/security/ReloadCryptoModuleCommand.java index 757233a..4fc294a 100644 --- a/src/main/java/com/eactive/eai/agent/security/ReloadCryptoModuleCommand.java +++ b/src/main/java/com/eactive/eai/agent/security/ReloadCryptoModuleCommand.java @@ -3,16 +3,25 @@ package com.eactive.eai.agent.security; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; import com.eactive.eai.common.security.CryptoModuleManager; +import com.eactive.eai.common.util.Logger; public class ReloadCryptoModuleCommand extends Command { + private static final long serialVersionUID = 1032982004418318100L; + @Override public Object execute() throws CommandException { + Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); try { String cryptoId = (String) args; CryptoModuleManager.getInstance().reload(cryptoId); - return null; + return "success"; } catch (Exception e) { + String rspErrorCode = "RECEAIMCM002"; + String msg = this.makeException(rspErrorCode, e); + if (logger.isError()) { + logger.error(msg, e); + } throw new CommandException(e.getMessage()); } }