Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eedac51f7 | |||
| 8d63586749 | |||
| 7b0699fdb9 | |||
| b6a5dce74a | |||
| 265a30ccf3 | |||
| d458d1b008 | |||
| 29b8b7eaac | |||
| 74302df5a2 | |||
| 9a15b5506f | |||
| 0b68b8166f | |||
| 8db66f8bef | |||
| c4a250db08 | |||
| db7a728344 | |||
| 813bb99952 | |||
| becd8612e8 | |||
| e208a2d64b | |||
| 444b7ad595 | |||
| 085dd024d8 | |||
| 3b6b3f6e4c | |||
| f4c28e8a27 | |||
| 3cda2873cc | |||
| c0efdd89d4 | |||
| 3eac4eeaa6 | |||
| c0c3f45d18 | |||
| 2686448791 | |||
| 3d4e5762c8 | |||
| f7859a77dc | |||
| c4010147db | |||
| 5994befbb6 | |||
| 07569e62da | |||
| e5638f22bb | |||
| e64319e7d2 | |||
| 43548a90c4 | |||
| 8900966d71 | |||
| 32a7723fa4 | |||
| 580686886c | |||
| bf1135f9ad | |||
| 8f70451477 | |||
| 437961e7d5 | |||
| 496bdd1468 | |||
| 2bfc3d0bde | |||
| 9589bc635a | |||
| ca8be26583 | |||
| 5641baff35 | |||
| f36f78d63c | |||
| 06f0d389a6 | |||
| 00735f6614 |
@@ -82,6 +82,8 @@ dependencies {
|
||||
|
||||
api 'com.nimbusds:nimbus-jose-jwt:9.24.3'
|
||||
|
||||
api 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
|
||||
api "io.undertow:undertow-servlet:${undertowVersion}"
|
||||
api 'org.java-websocket:Java-WebSocket:1.3.9'
|
||||
api 'javax.cache:cache-api:1.1.1'
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.eactive.eai.adapter.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.message.StandardItem;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
|
||||
/**
|
||||
* 템플릿 기반 어댑터 에러 메시지 핸들러.
|
||||
*
|
||||
* [프로퍼티 설정]
|
||||
* AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서
|
||||
* "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어
|
||||
* StandardMessage 의 필드 값으로 치환한 결과를 반환한다.
|
||||
*
|
||||
* [변수 치환 규칙]
|
||||
* 1. "${callprop.키}" → callProp.getProperty("키") 로 치환
|
||||
* "${callprop.키:기본값}" → 키 없거나 callProp null 이면 기본값 반환
|
||||
* 2. "${callprop[경로]}" → StandardMessage 경로 값을 키로 callProp 간접 조회
|
||||
* "${callprop[경로]:기본값}" → 키 결정 실패 또는 키 없으면 기본값 반환
|
||||
* 3. "${경로}" → StandardMessage.findItemValue("경로") 로 치환
|
||||
* "${경로:기본값}" → 경로 값이 null/공백이면 기본값 반환
|
||||
* 모든 형태에서 기본값 미지정 시 빈 문자열을 반환한다.
|
||||
*
|
||||
* [배열 반복 문법] (MSG_LIST 등 GRID 타입)
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" }
|
||||
* {{/foreach}}
|
||||
* → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환,
|
||||
* 반복 결과를 쉼표(,)로 이어 붙임
|
||||
*
|
||||
* [사용 예 – JSON 템플릿]
|
||||
* {
|
||||
* "adapterName": "${callprop.ADAPTER_NAME}",
|
||||
* "resultCode": "${MSG.MAIN_MSG.outp_msg_cd}",
|
||||
* "resultMsg": "${MSG.MAIN_MSG.outp_msg_ctnt}",
|
||||
* "errors": [
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* {
|
||||
* "code": "${outp_msg_cd}",
|
||||
* "msg": "${outp_msg_ctnt}",
|
||||
* "desc": "${outp_msg_desc}",
|
||||
* "field": "${err_occu_item_nm}"
|
||||
* }
|
||||
* {{/foreach}}
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* [사용 예 – XML 템플릿]
|
||||
* <error>
|
||||
* <adapter>${callprop.ADAPTER_NAME}</adapter>
|
||||
* <code>${MSG.MAIN_MSG.outp_msg_cd}</code>
|
||||
* <errors>
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* <item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>
|
||||
* {{/foreach}}
|
||||
* </errors>
|
||||
* </error>
|
||||
*
|
||||
* [callProp 중첩 Properties 참조]
|
||||
* callProp 의 값이 Properties 인 경우 점(.) 으로 체인 탐색이 가능하다.
|
||||
* callProp.put("OUTBOUND", subProps) // subProps.setProperty("IF_ID", "SVC001")
|
||||
* → 템플릿에서 ${callprop.OUTBOUND.IF_ID} 로 참조
|
||||
* 중간 값이 Properties 가 아닌 경우 전체 keyPath 를 키로 폴백 조회한다.
|
||||
*/
|
||||
public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
/** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
|
||||
static final String PROP_GROUP = "AdapterErrorMessageHandler";
|
||||
|
||||
/** callProp 참조 변수 접두어: ${callprop.키} */
|
||||
static final String CALLPROP_PREFIX = "callprop.";
|
||||
|
||||
/** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */
|
||||
static final String CALLPROP_INDIRECT_PREFIX = "callprop[";
|
||||
|
||||
/** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */
|
||||
private static final Pattern VAR_PATTERN =
|
||||
Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
/** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */
|
||||
private static final Pattern FOREACH_PATTERN =
|
||||
Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}",
|
||||
Pattern.DOTALL);
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardErrorResponseMessage(
|
||||
String inboundAdapterGroupName,
|
||||
String inboundAdapterName,
|
||||
Properties callProp,
|
||||
Object outboundRequestData,
|
||||
StandardMessage resStandardMessage) throws Exception {
|
||||
|
||||
String templateKey = inboundAdapterGroupName + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
|
||||
+ ", key=" + templateKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return render(template, resStandardMessage, callProp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿에 StandardMessage 및 callProp 값을 치환해 최종 문자열을 반환한다.
|
||||
* package-private: 단위 테스트에서 직접 호출 가능
|
||||
*/
|
||||
String render(String template, StandardMessage msg, Properties callProp) {
|
||||
// 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리
|
||||
StringBuffer sb = new StringBuffer();
|
||||
Matcher foreachMatcher = FOREACH_PATTERN.matcher(template);
|
||||
while (foreachMatcher.find()) {
|
||||
String arrayPath = foreachMatcher.group(1).trim();
|
||||
String blockContent = foreachMatcher.group(2);
|
||||
String rendered = renderForeach(arrayPath, blockContent, msg);
|
||||
foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered));
|
||||
}
|
||||
foreachMatcher.appendTail(sb);
|
||||
|
||||
// 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환
|
||||
String intermediate = sb.toString();
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher varMatcher = VAR_PATTERN.matcher(intermediate);
|
||||
while (varMatcher.find()) {
|
||||
String expr = varMatcher.group(1).trim();
|
||||
String value = resolveScalar(expr, msg, callProp);
|
||||
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
|
||||
}
|
||||
varMatcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 변수 표현식을 해석해 값을 반환한다.
|
||||
* 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
|
||||
* - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회
|
||||
* - "callprop.키[:기본값]" → callProp 직접/중첩 조회
|
||||
* - "경로[:기본값]" → StandardMessage.findItemValue(경로)
|
||||
*/
|
||||
private String resolveScalar(String expr, StandardMessage msg, Properties callProp) {
|
||||
if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) {
|
||||
return resolveIndirectCallProp(expr, msg, callProp);
|
||||
}
|
||||
if (expr.startsWith(CALLPROP_PREFIX)) {
|
||||
String rest = expr.substring(CALLPROP_PREFIX.length());
|
||||
int colonIdx = rest.indexOf(':');
|
||||
String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
|
||||
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
|
||||
if (callProp == null) return defaultVal;
|
||||
String value = resolveCallProp(callProp, keyPath);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
int colonIdx = expr.indexOf(':');
|
||||
String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
|
||||
String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : "";
|
||||
String value = resolveMessagePath(msg, path);
|
||||
return StringUtils.isNotEmpty(value) ? value : defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* StandardMessage 경로를 해석한다.
|
||||
* 1. findItemValue(path) 로 직접 조회한다.
|
||||
* 2. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우
|
||||
* 나머지 경로(subPath)를 키로 JSON 필드를 추출한다.
|
||||
* 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo
|
||||
* DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city
|
||||
*/
|
||||
private String resolveMessagePath(StandardMessage msg, String path) {
|
||||
String value = msg.findItemValue(path);
|
||||
if (value != null) return value;
|
||||
|
||||
int dotIdx = path.lastIndexOf('.');
|
||||
while (dotIdx > 0) {
|
||||
String parentPath = path.substring(0, dotIdx);
|
||||
String subPath = path.substring(dotIdx + 1);
|
||||
String parentVal = msg.findItemValue(parentPath);
|
||||
if (StringUtils.isNotBlank(parentVal)) {
|
||||
String extracted = extractFromJson(parentVal, subPath);
|
||||
if (extracted != null) return extracted;
|
||||
}
|
||||
dotIdx = parentPath.lastIndexOf('.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다.
|
||||
* 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로,
|
||||
* 오브젝트/배열이면 JSON 문자열 그대로 반환한다. 파싱 실패 시 null 반환.
|
||||
*/
|
||||
private String extractFromJson(String json, String fieldPath) {
|
||||
try {
|
||||
JsonElement el = new JsonParser().parse(json);
|
||||
for (String part : fieldPath.split("\\.", -1)) {
|
||||
if (!el.isJsonObject()) return null;
|
||||
el = el.getAsJsonObject().get(part);
|
||||
if (el == null) return null;
|
||||
}
|
||||
return el.isJsonPrimitive() ? el.getAsString() : el.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리.
|
||||
* 1. 표준전문 경로로 callProp 키를 동적으로 결정한다.
|
||||
* 2. 결정된 키로 callProp 에서 값을 조회한다.
|
||||
* 3. 키 또는 값을 찾지 못하면 기본값(없으면 빈 문자열)을 반환한다.
|
||||
*/
|
||||
private String resolveIndirectCallProp(String expr, StandardMessage msg, Properties callProp) {
|
||||
int closeIdx = expr.indexOf(']');
|
||||
if (closeIdx < 0) return "";
|
||||
|
||||
String msgPath = expr.substring(CALLPROP_INDIRECT_PREFIX.length(), closeIdx);
|
||||
String defaultVal = (closeIdx + 1 < expr.length() && expr.charAt(closeIdx + 1) == ':')
|
||||
? expr.substring(closeIdx + 2)
|
||||
: "";
|
||||
|
||||
String key = msg.findItemValue(msgPath);
|
||||
if (StringUtils.isBlank(key)) return defaultVal;
|
||||
if (callProp == null) return defaultVal;
|
||||
|
||||
String value = resolveCallProp(callProp, key);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties 체인을 점(.) 구분자로 재귀 탐색한다.
|
||||
* - 첫 세그먼트의 값이 Properties 이면 나머지 경로로 재귀
|
||||
* - String 이면 반환, 그 외 Object 이면 toString() 반환
|
||||
* - 첫 세그먼트 값이 Properties 가 아닌 경우 전체 keyPath 로 폴백 조회
|
||||
*/
|
||||
private String resolveCallProp(Properties props, String keyPath) {
|
||||
int dotIdx = keyPath.indexOf('.');
|
||||
if (dotIdx < 0) {
|
||||
Object val = props.get(keyPath);
|
||||
if (val == null) return null;
|
||||
if (val instanceof String) return (String) val;
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
String first = keyPath.substring(0, dotIdx);
|
||||
String rest = keyPath.substring(dotIdx + 1);
|
||||
Object val = props.get(first);
|
||||
if (val instanceof Properties) {
|
||||
return resolveCallProp((Properties) val, rest);
|
||||
}
|
||||
|
||||
// 중간 값이 Properties 가 아닌 경우: 전체 keyPath 를 키로 폴백
|
||||
Object direct = props.get(keyPath);
|
||||
if (direct == null) return null;
|
||||
if (direct instanceof String) return (String) direct;
|
||||
return direct.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환,
|
||||
* 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
|
||||
*/
|
||||
private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
|
||||
StandardItem arrayItem = msg.findItem(arrayPath);
|
||||
if (arrayItem == null) {
|
||||
if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
|
||||
return "";
|
||||
}
|
||||
|
||||
List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> renderedItems = new ArrayList<>();
|
||||
for (LinkedHashMap<String, StandardItem> row : rows) {
|
||||
String rowText = renderRow(blockContent, row);
|
||||
if (StringUtils.isNotBlank(rowText)) {
|
||||
renderedItems.add(rowText);
|
||||
}
|
||||
}
|
||||
return String.join(",", renderedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다.
|
||||
* fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다.
|
||||
*/
|
||||
private String renderRow(String blockContent, LinkedHashMap<String, StandardItem> row) {
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher varMatcher = VAR_PATTERN.matcher(blockContent);
|
||||
while (varMatcher.find()) {
|
||||
String fieldName = varMatcher.group(1).trim();
|
||||
String value = "";
|
||||
StandardItem item = row.get(fieldName);
|
||||
if (item != null) {
|
||||
value = StringUtils.defaultString(item.getValue());
|
||||
}
|
||||
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
|
||||
}
|
||||
varMatcher.appendTail(result);
|
||||
return result.toString().trim();
|
||||
}
|
||||
}
|
||||
+8
@@ -40,7 +40,9 @@ import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.Keys;
|
||||
import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.message.EAIMessageKeys;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoVO;
|
||||
@@ -566,4 +568,10 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
}
|
||||
|
||||
public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception;
|
||||
|
||||
protected boolean isTas(Properties tempProp) {
|
||||
String simYn = tempProp.getProperty("SIM_YN");
|
||||
return EAIServerManager.getInstance().isTASEnabledEAIServer()
|
||||
&& StringUtils.equals(simYn, EAIMessageKeys.TRANTYPE_TAS);
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -7,6 +7,8 @@ import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.eactive.eai.common.util.TxFileLogger;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
@@ -41,14 +43,11 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
|
||||
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
|
||||
|
||||
if(useForwardProxy) {
|
||||
if(useForwardProxy && !isTas(tempProp)) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
HttpHost proxy = new HttpHost(url.getProtocol(), url.getHost(), url.getPort());
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
@@ -121,7 +120,9 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
|
||||
|
||||
TxFileLogger.logTxFile(tempProp, sendData, "[OUT_SEND]");
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
@@ -150,7 +151,8 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
TxFileLogger.logTxFile(tempProp, responseString, "[OUT_RECV]");
|
||||
|
||||
|
||||
if (status >= 400 && status < 500) {
|
||||
String errMsg = String.format(
|
||||
|
||||
+21
-10
@@ -11,6 +11,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -74,6 +75,7 @@ import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.eactive.eai.common.util.TxFileLogger;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.kjbank.encrypt.exchange.crypto.AES256Cipher;
|
||||
@@ -294,14 +296,11 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
|
||||
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
|
||||
|
||||
if(useForwardProxy) {
|
||||
if(useForwardProxy && !isTas(tempProp)) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
HttpHost proxy = new HttpHost(url.getProtocol(), url.getHost(), url.getPort());
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
@@ -429,6 +428,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
|
||||
TxFileLogger.logTxFile(tempProp, sendData, "[OUT_SEND]");
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
@@ -474,11 +475,14 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
// 여기까지
|
||||
|
||||
responseHeaders = response.getHeaders();
|
||||
|
||||
TxFileLogger.logTxFile(tempProp, new String(responseMessage, vo.getEncode()), "[OUT_RECV]");
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[received status]" + status);
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]");
|
||||
}
|
||||
|
||||
}
|
||||
} catch (ConnectException e) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
@@ -708,12 +712,19 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
return new String(responseMessage, encode);
|
||||
}
|
||||
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
|
||||
JSONObject headerJson = new JSONObject();
|
||||
for (String key : relayKeyArr) {
|
||||
String value = responseHeaderProp.getProperty(key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
headerJson.put(key, value);
|
||||
if (StringUtils.equalsIgnoreCase(relayHeaderKeys, "ALL")) {
|
||||
for (Enumeration<Object> e = responseHeaderProp.keys(); e.hasMoreElements(); ) {
|
||||
String key = (String)e.nextElement();
|
||||
headerJson.put(StringUtils.lowerCase(key), responseHeaderProp.getProperty(key));
|
||||
}
|
||||
} else {
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
|
||||
for (String key : relayKeyArr) {
|
||||
String value = responseHeaderProp.getProperty(key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
headerJson.put(StringUtils.lowerCase(key), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.eactive.eai.adapter.http.client.impl;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dom4j.Node;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.client.impl.filter.HttpClient5AdapterFilterFactory;
|
||||
import com.eactive.eai.adapter.http.client.impl.filter.HttpClientAdapterFilter;
|
||||
|
||||
/**
|
||||
* 1. 기능 : HttpClient5AdapterServiceRest 호출 전후 Filter 적용할 수 있는 기능을 제공한다.<br>
|
||||
* 2. 처리 개요 : <br>
|
||||
* 3. 주의사항 <br>
|
||||
*
|
||||
* @author :
|
||||
* @version : v 1.0.0
|
||||
* @see : HttpClientAdapterServiceFactory.java,
|
||||
* HttpClientAdapterServiceSupport.java, HttpClient5AdapterServiceRest.java
|
||||
* @since :
|
||||
*
|
||||
*/
|
||||
public class HttpClient5AdapterServiceRestAddFilter extends HttpClient5AdapterServiceRest
|
||||
implements HttpClientAdapterServiceKey {
|
||||
|
||||
// 요청전 수행할 필터(쉼표(,)로 구분)
|
||||
static final String PRE_FILTERS = "PRE_FILTERS";
|
||||
|
||||
// 요청후 수행할 필터(쉼표(,)로 구분)
|
||||
static final String POST_FILTERS = "POST_FILTERS";
|
||||
|
||||
// // Adapter에서 Exception이 발생한 경우, 처리할 필터
|
||||
// static final String EXCEPTION_FILTER = "EXCEPTION_FILTER";
|
||||
|
||||
/**
|
||||
* 1. 기능 : HttpClient 호출 전후 Filter 적용용 2. 처리 개요 : <br>
|
||||
* 3. 주의사항 <br>
|
||||
*
|
||||
* @param prop Http Adapter 속성 정보
|
||||
* @return 반환 된 Object
|
||||
* @exception Exception 수동 시스템 간 통신 중 발생
|
||||
*/
|
||||
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
|
||||
|
||||
String adptGrpName = tempProp.getProperty(ADAPTER_GROUP_NAME);
|
||||
String adptName = tempProp.getProperty(ADAPTER_NAME);
|
||||
|
||||
data = doPreFilters(adptGrpName, adptName, prop, data, tempProp);
|
||||
|
||||
Object adapterResponse = super.execute(prop, data, tempProp);
|
||||
|
||||
adapterResponse = doPostFilters(adptGrpName, adptName, prop, adapterResponse, tempProp);
|
||||
if (adapterResponse instanceof JSONObject)
|
||||
adapterResponse = ((JSONObject) adapterResponse).toJSONString();
|
||||
|
||||
if (adapterResponse instanceof Node)
|
||||
adapterResponse = ((Node) adapterResponse).asXML();
|
||||
|
||||
return adapterResponse;
|
||||
}
|
||||
|
||||
protected Object doPreFilters(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
// boolean isSetCommonFilterInAdapterProp = false;
|
||||
// 1. adapter에 설정된 필터 수행
|
||||
String preFiltersStr = prop.getProperty(PRE_FILTERS);
|
||||
if (StringUtils.isNotEmpty(preFiltersStr)) {
|
||||
String[] preFilters = StringUtils.split(preFiltersStr, ",");
|
||||
for (String filterName : preFilters) {
|
||||
if (StringUtils.isBlank(filterName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug("HttpClient5AdapterServiceRestAddFilter] Processing Start [" + filterName + "]");
|
||||
HttpClientAdapterFilter adapterFilter = HttpClient5AdapterFilterFactory.createFilter(filterName.trim());
|
||||
|
||||
if (adapterFilter != null) {
|
||||
// if (adapterFilter instanceof IBKOutCommonFilter)
|
||||
// isSetCommonFilterInAdapterProp = true;
|
||||
|
||||
message = adapterFilter.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
} else {
|
||||
throw new Exception("Failed get Filter Class: " + filterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // 2. IBKOutCommonFilter 필터 수행(adapter 필터에서 수행하지 않을 때만)
|
||||
// if (!isSetCommonFilterInAdapterProp) {
|
||||
// HttpClientAdapterFilter ibkFilter = HttpClient5AdapterFilterFactory
|
||||
// .createFilter(IBKOutCommonFilter.class.getName());
|
||||
// if (ibkFilter != null) {
|
||||
// message = ibkFilter.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
// }
|
||||
// } else {
|
||||
// logger.debug("IBKOutCommonFilter isSetCommonFilterInAdapterProp 'POST_FILTERS'. already in adapter filters");
|
||||
// }
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
protected Object doPostFilters(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
// boolean isSetCommonFilterInAdapterProp = false;
|
||||
// 1. adapter에 설정된 필터 수행
|
||||
String postFiltersStr = prop.getProperty(POST_FILTERS);
|
||||
if (StringUtils.isNotEmpty(postFiltersStr)) {
|
||||
String[] postFilters = StringUtils.split(postFiltersStr, ",");
|
||||
for (String filterName : postFilters) {
|
||||
if (StringUtils.isBlank(filterName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HttpClientAdapterFilter adapterFilter = HttpClient5AdapterFilterFactory.createFilter(filterName.trim());
|
||||
if (adapterFilter != null) {
|
||||
// if (adapterFilter instanceof IBKOutCommonFilter)
|
||||
// isSetCommonFilterInAdapterProp = true;
|
||||
message = adapterFilter.doPostFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
} else {
|
||||
throw new Exception("Failed get Filter Class: " + filterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // 2. IBKOutCommonFilter 필터 수행
|
||||
// if (!isSetCommonFilterInAdapterProp) {
|
||||
// HttpClientAdapterFilter ibkFilter = HttpClient5AdapterFilterFactory
|
||||
// .createFilter(IBKOutCommonFilter.class.getName());
|
||||
// if (ibkFilter != null) {
|
||||
// message = ibkFilter.doPostFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
// }
|
||||
// } else {
|
||||
// logger.debug("IBKOutCommonFilter isSetCommonFilterInAdapterProp 'PRE_FILTERS'. already in adapter filters");
|
||||
// }
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class AllHeaderFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
|
||||
public static final String HEADER_KEY_NAMES = "AllHeaderFilter.blackList";
|
||||
|
||||
|
||||
String[] HttpHeaderRelayBlackList = {
|
||||
"Content-Length",
|
||||
"Transfer-Encoding",
|
||||
"Host",
|
||||
"Authorization",
|
||||
"Accept",
|
||||
"Host",
|
||||
"Cookie",
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"TE",
|
||||
"Trailer",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
"Content-Type",
|
||||
"Content-Encoding",
|
||||
"Content-Language",
|
||||
"Content-Location",
|
||||
"Content-MD5",
|
||||
"Expect",
|
||||
};
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("doPreFilter Processing Start.");
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
|
||||
|
||||
String[] userSettingBlackList = propValue.split(",");
|
||||
|
||||
if( userSettingBlackList.length > 0 ) {
|
||||
HttpHeaderRelayBlackList = Stream
|
||||
.concat(Arrays.stream(HttpHeaderRelayBlackList), Arrays.stream(userSettingBlackList))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
|
||||
for(String keyName : inboundHeaderMap.keySet() ) {
|
||||
|
||||
if( StringUtils.equalsAnyIgnoreCase( keyName, HttpHeaderRelayBlackList ) ) {
|
||||
logger.debug("Skip Processing Key ["+keyName+"], value ["+inboundHeaderMap.get(keyName)+"] in HttpHeaderRelayBlackList");
|
||||
continue;
|
||||
}
|
||||
|
||||
filterHeaders.put(keyName, inboundHeaderMap.get(keyName));
|
||||
logger.debug("Processing Key ["+keyName+"], value ["+inboundHeaderMap.get(keyName)+"]");
|
||||
}
|
||||
|
||||
logger.debug("doPreFilter Processing End.");
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class AsyncReponseFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String X_TRACE_ID = "partnerTraceId";
|
||||
public static final String TRACE_ID = "traceId";
|
||||
|
||||
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
|
||||
public static final String HEADER_KEY_NAMES = "AsyncReponseFilter";
|
||||
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("AsyncReponseFilter] PreFilter Processing Start!!");
|
||||
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
|
||||
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
|
||||
|
||||
String[] headerKeyNames = propValue.split(",");
|
||||
|
||||
|
||||
headerKeyNames = Arrays.stream(headerKeyNames)
|
||||
.map(String::trim)
|
||||
.toArray(String[]::new);
|
||||
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_TRACE_ID)) {
|
||||
filterHeaders.put(X_TRACE_ID, inboundHeaderMap.get(X_TRACE_ID));
|
||||
logger.debug("AsyncReponseFilter] Processing Key ["+X_TRACE_ID+"], value ["+inboundHeaderMap.get(X_TRACE_ID)+"]");
|
||||
}
|
||||
|
||||
if (inboundHeaderMap.containsKey(TransactionContextKeys.X_LOAN_TOKEN)) {
|
||||
filterHeaders.put(TransactionContextKeys.X_LOAN_TOKEN, inboundHeaderMap.get(TransactionContextKeys.X_LOAN_TOKEN));
|
||||
logger.debug("AsyncReponseFilter] Processing Key ["+TransactionContextKeys.X_LOAN_TOKEN+"], value ["+inboundHeaderMap.get(TransactionContextKeys.X_LOAN_TOKEN)+"]");
|
||||
}
|
||||
}
|
||||
|
||||
filterHeaders.setProperty(TRACE_ID, traceId);
|
||||
logger.debug("AsyncReponseFilter] Processing Key ["+TRACE_ID+"], value ["+traceId+"]");
|
||||
|
||||
for (String keyName : inboundHeaderMap.keySet()) {
|
||||
|
||||
if (StringUtils.equalsAnyIgnoreCase(keyName, headerKeyNames)) {
|
||||
String value = inboundHeaderMap.get(keyName);
|
||||
filterHeaders.put(keyName, value);
|
||||
logger.debug("AsyncReponseFilter] Processing Key [" + keyName + "], value [" + value + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HttpClient5AdapterFilterFactory {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
static String customBasePackage = "com.eactive.eai.custom.adapter.http.client.filter";
|
||||
static String basePackage = "com.eactive.eai.adapter.http.client.impl.filter";
|
||||
private static ConcurrentHashMap<String, HttpClientAdapterFilter> h = new ConcurrentHashMap<>();
|
||||
|
||||
private HttpClient5AdapterFilterFactory() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
public static HttpClientAdapterFilter createFilter(String type) {
|
||||
if (h.containsKey(type)) {
|
||||
return h.get(type);
|
||||
}
|
||||
|
||||
HttpClientAdapterFilter filter = null;
|
||||
switch (HttpClient5AdapterFilterType.getValue(type)) {
|
||||
case SIMPLEFRAMEWORK:
|
||||
filter = new SimpleFrameworkFilter();
|
||||
break;
|
||||
case SIMPLEFRAMEWORKBODY:
|
||||
filter = new SimpleframeworkBodyFilter();
|
||||
break;
|
||||
case JBOBP:
|
||||
filter = new JBOBPFilter();
|
||||
break;
|
||||
case KFTCFACE:
|
||||
filter = new KFTCFaceFilter();
|
||||
break;
|
||||
case KFTCP2P:
|
||||
filter = new KFTCP2PFilter();
|
||||
break;
|
||||
case KAKAOBANK:
|
||||
filter = new KAKAOBankFilter();
|
||||
break;
|
||||
case NAVERFIN:
|
||||
filter = new NAVERFinFilter();
|
||||
break;
|
||||
case NICECREDIT:
|
||||
filter = new NICECreditFilter();
|
||||
break;
|
||||
case TRACEBODY:
|
||||
filter = new TraceBodyFilter();
|
||||
break;
|
||||
case TOSSBANK:
|
||||
filter = new TOSSBankFilter();
|
||||
break;
|
||||
case ASYNCRESPONSE:
|
||||
filter = new AsyncReponseFilter();
|
||||
break;
|
||||
case ALLHEADER:
|
||||
filter = new AllHeaderFilter();
|
||||
break;
|
||||
default:
|
||||
filter = classForName(type);
|
||||
if (filter == null) {
|
||||
filter = classForName(customBasePackage + "." + type);
|
||||
}
|
||||
if (filter == null) {
|
||||
filter = classForName(basePackage + "." + type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
h.put(type, filter);
|
||||
return filter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClientAdapterFilter classForName(String type) {
|
||||
try {
|
||||
Class<?> cl = Class.forName(type);
|
||||
return (HttpClientAdapterFilter) cl.newInstance();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
|
||||
logger.error("Cannot create a HttpClientAdapterFilter. - {}", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
public enum HttpClient5AdapterFilterType {
|
||||
SIMPLEFRAMEWORK,
|
||||
SIMPLEFRAMEWORKBODY,
|
||||
JBOBP,
|
||||
KFTCFACE,
|
||||
KFTCP2P,
|
||||
KAKAOBANK,
|
||||
NAVERFIN,
|
||||
NICECREDIT,
|
||||
TOSSBANK,
|
||||
TRACEBODY,
|
||||
ASYNCRESPONSE,
|
||||
ALLHEADER,
|
||||
UNKNOWN;
|
||||
|
||||
public static HttpClient5AdapterFilterType getValue(String type) {
|
||||
try {
|
||||
return valueOf(type.toUpperCase());
|
||||
} catch (NullPointerException e) {
|
||||
return UNKNOWN;
|
||||
} catch (Exception e) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
public interface HttpClientAdapterFilter {
|
||||
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception;
|
||||
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class HttpClientAdapterFilterException extends RuntimeException {
|
||||
private static final long serialVersionUID = -1208983360831093751L;
|
||||
@Getter
|
||||
private final String code;
|
||||
@Getter
|
||||
private final Properties prop;
|
||||
@Getter
|
||||
private final Properties tempProp;
|
||||
|
||||
public HttpClientAdapterFilterException(String code, String msg, Properties prop, Properties tmepProp) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.prop = prop;
|
||||
this.tempProp = tmepProp;
|
||||
}
|
||||
|
||||
public HttpClientAdapterFilterException(String code, String msg, Properties prop, Properties tempProp, Throwable cause) {
|
||||
super(msg, cause);
|
||||
this.code = code;
|
||||
this.prop = prop;
|
||||
this.tempProp = tempProp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpClientAdapterFilterException [code=" + code + ", prop=" + prop + ", tempProp=" + tempProp + ", parent=" + super.toString() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class JBOBPFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String X_OBP_PARTNERCODE = "x-obp-partnercode";
|
||||
public static final String X_OBP_TXID = "x-obp-txid";
|
||||
public static final String X_OBP_TRUST_SYSTEM = "x-obp-trust-system";
|
||||
String systemTrustKey = "APIMGW-0001-QVBJTUdXLTAwMDE=";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("JBOBPFilter] PreFilter Processing Start!!");
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) systemTrustKey = propGroupVo.getProperty(X_OBP_TRUST_SYSTEM, systemTrustKey);
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
String obpPartnerCode = "000000-00";
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_OBP_PARTNERCODE)) {
|
||||
filterHeaders.put(X_OBP_PARTNERCODE, inboundHeaderMap.get(X_OBP_PARTNERCODE));
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+inboundHeaderMap.get(X_OBP_PARTNERCODE)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_OBP_PARTNERCODE, obpPartnerCode);
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+obpPartnerCode+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(X_OBP_PARTNERCODE, obpPartnerCode);
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+obpPartnerCode+"]");
|
||||
}
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
if (StringUtils.isNotBlank(uuid)) {
|
||||
filterHeaders.setProperty(X_OBP_TXID, uuid); // JB OBP Framework용
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_TXID+"], value ["+uuid+"]");
|
||||
}
|
||||
if (StringUtils.isNotBlank(systemTrustKey)) {
|
||||
filterHeaders.setProperty(X_OBP_TRUST_SYSTEM, systemTrustKey); // JB OBP Framework용
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_TRUST_SYSTEM+"], value ["+systemTrustKey+"]");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class KAKAOBankFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "kakaobank";
|
||||
public static final String X_KKB_PARTNER_CODE = "X-KKB-PARTNER-CODE";
|
||||
public static final String X_KKB_API_NAME = "X-KKB-API-NAME";
|
||||
public static final String X_KKB_API_TX_ID = "X-KKB-API-TX-ID";
|
||||
public static final String X_KKB_TX_TIME = "X-KKB-TX-TIME";
|
||||
public static final String X_KKB_CMPR_MGMT_NO = "X-KKB-CMPR-MGMT-NO";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("KAKAOBankFilter] PreFilter Processing Start!!");
|
||||
Object returnMessage = message;
|
||||
String partnerCode = "KJB";
|
||||
String apiName = "status_change";
|
||||
String apiTxId = "0";
|
||||
String txTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String cmprMgmtNo = "0";
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
partnerCode = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_PARTNER_CODE);
|
||||
apiName = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_API_NAME);
|
||||
apiTxId = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_API_TX_ID);
|
||||
cmprMgmtNo = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_CMPR_MGMT_NO);
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_KKB_PARTNER_CODE)) {
|
||||
filterHeaders.put(X_KKB_PARTNER_CODE, inboundHeaderMap.get(X_KKB_PARTNER_CODE));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_PARTNER_CODE, partnerCode);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_API_NAME)) {
|
||||
filterHeaders.put(X_KKB_API_NAME, inboundHeaderMap.get(X_KKB_API_NAME));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_API_NAME, apiName);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_API_TX_ID)) {
|
||||
filterHeaders.put(X_KKB_API_TX_ID, inboundHeaderMap.get(X_KKB_API_TX_ID));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_API_TX_ID, apiTxId);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_TX_TIME)) {
|
||||
filterHeaders.put(X_KKB_TX_TIME, inboundHeaderMap.get(X_KKB_TX_TIME));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_TX_TIME, txTime);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_CMPR_MGMT_NO)) {
|
||||
filterHeaders.put(X_KKB_CMPR_MGMT_NO, inboundHeaderMap.get(X_KKB_CMPR_MGMT_NO));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_CMPR_MGMT_NO, cmprMgmtNo);
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_PARTNER_CODE, partnerCode);
|
||||
filterHeaders.put(X_KKB_API_NAME, apiName);
|
||||
filterHeaders.put(X_KKB_API_TX_ID, apiTxId);
|
||||
filterHeaders.put(X_KKB_TX_TIME, txTime);
|
||||
filterHeaders.put(X_KKB_CMPR_MGMT_NO, cmprMgmtNo);
|
||||
}
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_PARTNER_CODE+"], value ["+filterHeaders.getProperty(X_KKB_PARTNER_CODE)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_API_NAME+"], value ["+filterHeaders.getProperty(X_KKB_API_NAME)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_API_TX_ID+"], value ["+filterHeaders.getProperty(X_KKB_API_TX_ID)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_TX_TIME+"], value ["+filterHeaders.getProperty(X_KKB_TX_TIME)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_CMPR_MGMT_NO+"], value ["+filterHeaders.getProperty(X_KKB_CMPR_MGMT_NO)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return returnMessage;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class KFTCFaceFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "kftcface";
|
||||
public static final String H_CLIENT_ID = "Client-Id";
|
||||
public static final String B_ORG_CODE = "org_code";
|
||||
public static final String B_TRANSACTION_ID = "transaction_id";
|
||||
public static final String B_REQUEST_DATETIME = "request_datetime";
|
||||
public static String instId = null;
|
||||
|
||||
static {
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
instId = eaiServerManager.getInstId();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("KFTCFaceFilter] PreFilter Processing Start!!");
|
||||
JSONObject returnMessage = null;
|
||||
String clientId = "";
|
||||
String orgCode = "034"; // 광주은행 행코드
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
if (StringUtils.isEmpty(clientId))
|
||||
clientId = propGroupVo.getProperty(PROP_PREFIX + "." + H_CLIENT_ID);
|
||||
orgCode = propGroupVo.getProperty(PROP_PREFIX + "." + B_ORG_CODE);
|
||||
}
|
||||
|
||||
String request_datetime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String reqDate = new SimpleDateFormat("yyMMdd").format(new Date());
|
||||
String reqTime = new SimpleDateFormat("HHmmss").format(new Date());
|
||||
// 기관코드(3) + 요청일자(6) -- 금결원 표준화 항목, 고정 9자리.
|
||||
// 요청시간(6) + 인스턴스ID(2) + RandomString(3) -- 기관별 생성 거래고유번호(11자리)
|
||||
// 기관코드(3) + 요청일자(6) + 요청시간(6) + 인스턴스ID(2) + RandomString(3) -- 20자리.
|
||||
String transaction_id = orgCode + reqDate + reqTime + instId + RandomStringUtils.random(3, true, true).toUpperCase();
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(H_CLIENT_ID)) {
|
||||
filterHeaders.put(H_CLIENT_ID, inboundHeaderMap.get(H_CLIENT_ID));
|
||||
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+inboundHeaderMap.get(H_CLIENT_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(H_CLIENT_ID, clientId);
|
||||
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+clientId+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.setProperty(H_CLIENT_ID, clientId);
|
||||
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+clientId+"]");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(adptGrpName);
|
||||
String encode = adptGrpVO.getMessageEncode();
|
||||
if (message == null) {
|
||||
returnMessage = new JSONObject();
|
||||
} else {
|
||||
if (message instanceof JSONObject) {
|
||||
returnMessage = (JSONObject) message;
|
||||
} else if (message instanceof String) {
|
||||
returnMessage = parseJson((String) message);
|
||||
} else if (message instanceof byte[]) {
|
||||
returnMessage = parseJson(new String((byte[]) message, encode));
|
||||
}
|
||||
}
|
||||
|
||||
returnMessage.put(B_ORG_CODE, orgCode);
|
||||
returnMessage.put(B_TRANSACTION_ID, transaction_id);
|
||||
returnMessage.put(B_REQUEST_DATETIME, request_datetime);
|
||||
|
||||
return returnMessage.toJSONString();
|
||||
}
|
||||
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class KFTCP2PFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "kftcp2p";
|
||||
public static final String H_ORG_CODE = "org_code";
|
||||
public static final String H_TRX_NO = "api_trx_no";
|
||||
public static final String H_TRX_DTM = "api_trx_dtm";
|
||||
public static String instId = null;
|
||||
|
||||
static {
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
instId = eaiServerManager.getInstId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("KFTCP2PFilter] PreFilter Processing Start!!");
|
||||
String orgCode = "D210400012"; // 광주은행 개발 기관코드(운영: K210800020)
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
orgCode = propGroupVo.getProperty(PROP_PREFIX + "." + H_ORG_CODE);
|
||||
}
|
||||
|
||||
String apiTrxDtm = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
|
||||
String apiTrxTm = new SimpleDateFormat("HHmmss").format(new Date());
|
||||
// 기관코드(10) + 인스턴스ID(2) + 일시(6) + RandomString(2) -- 총 20자리
|
||||
String apiTrxNo = orgCode + instId + apiTrxTm + RandomStringUtils.random(2, true, true).toUpperCase();
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(H_TRX_NO)) {
|
||||
filterHeaders.put(H_TRX_NO, inboundHeaderMap.get(H_TRX_NO));
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+inboundHeaderMap.get(H_TRX_NO)+"]");
|
||||
} else {
|
||||
filterHeaders.put(H_TRX_NO, apiTrxNo);
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+apiTrxNo+"]");
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(H_TRX_DTM)) {
|
||||
filterHeaders.put(H_TRX_DTM, inboundHeaderMap.get(H_TRX_DTM));
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+inboundHeaderMap.get(H_TRX_DTM)+"]");
|
||||
} else {
|
||||
filterHeaders.put(H_TRX_DTM, apiTrxDtm);
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+apiTrxDtm+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(H_TRX_NO, apiTrxNo);
|
||||
filterHeaders.put(H_TRX_DTM, apiTrxDtm);
|
||||
}
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+filterHeaders.getProperty(H_TRX_NO)+"]");
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+filterHeaders.getProperty(H_TRX_DTM)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class NAVERFinFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "naverfin";
|
||||
public static final String X_PARTNER_ID = "X-Partner-Id";
|
||||
public static final String X_FINTECH_ID = "X-Fintech-Id";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("NAVERFinFilter] PreFilter Processing Start!!");
|
||||
String partnerID = "r0jtqwC9gAW5";
|
||||
String fintechId = "NF";
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
partnerID = propGroupVo.getProperty(PROP_PREFIX + "." + X_PARTNER_ID);
|
||||
fintechId = propGroupVo.getProperty(PROP_PREFIX + "." + X_FINTECH_ID);
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_PARTNER_ID)) {
|
||||
filterHeaders.put(X_PARTNER_ID, inboundHeaderMap.get(X_PARTNER_ID));
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+inboundHeaderMap.get(X_PARTNER_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_PARTNER_ID, partnerID);
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+partnerID+"]");
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_FINTECH_ID)) {
|
||||
filterHeaders.put(X_FINTECH_ID, inboundHeaderMap.get(X_FINTECH_ID));
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+inboundHeaderMap.get(X_FINTECH_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_FINTECH_ID, fintechId);
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+fintechId+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(X_PARTNER_ID, partnerID);
|
||||
filterHeaders.put(X_FINTECH_ID, fintechId);
|
||||
}
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+filterHeaders.getProperty(X_PARTNER_ID)+"]");
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+filterHeaders.getProperty(X_FINTECH_ID)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class NICECreditFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "nicecredit";
|
||||
public static final String H_PRODUCTID = "ProductID";
|
||||
public static final String B_REQ_DTM = "req_dtm";
|
||||
public static final String B_GOODS_CLS = "goods_cls";
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
JSONObject returnMessage = null;
|
||||
String productId = "2303102120";
|
||||
String reqDtm = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String goodsCls = "KJBAPI0001";
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
productId = propGroupVo.getProperty(PROP_PREFIX + "." + H_PRODUCTID);
|
||||
goodsCls = propGroupVo.getProperty(PROP_PREFIX + "." + B_GOODS_CLS);
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
if (inboundHeaderMap != null && inboundHeaderMap.containsKey(H_PRODUCTID)) {
|
||||
for (Entry<String, String> e : inboundHeaderMap.entrySet()) {
|
||||
String key = (String) e.getKey();
|
||||
String value = (String) e.getValue();
|
||||
|
||||
if (StringUtils.equalsIgnoreCase(key, H_PRODUCTID)) {
|
||||
filterHeaders.setProperty(key, value);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Request Header :" + key + "=[" + value + "]");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterHeaders.setProperty(H_PRODUCTID, productId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(adptGrpName);
|
||||
String encode = adptGrpVO.getMessageEncode();
|
||||
if (message == null) {
|
||||
returnMessage = new JSONObject();
|
||||
} else {
|
||||
if (message instanceof JSONObject) {
|
||||
returnMessage = (JSONObject) message;
|
||||
} else if (message instanceof String) {
|
||||
returnMessage = parseJson((String) message);
|
||||
} else if (message instanceof byte[]) {
|
||||
returnMessage = parseJson(new String((byte[]) message, encode));
|
||||
}
|
||||
}
|
||||
|
||||
returnMessage.put(B_REQ_DTM, reqDtm);
|
||||
returnMessage.put(B_GOODS_CLS, goodsCls);
|
||||
|
||||
return returnMessage.toJSONString();
|
||||
}
|
||||
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
|
||||
/**
|
||||
* HTTP 클라이언트 어댑터용 암복호화 필터 (송신 방향).
|
||||
*
|
||||
* doPreFilter : 외부 시스템으로 보내는 요청 암호화 (CRYPTO_ENC_* 프로퍼티 기반)
|
||||
* doPostFilter : 외부 시스템에서 받은 응답 복호화 (CRYPTO_DEC_* 프로퍼티 기반)
|
||||
*
|
||||
* DYNAMIC 키 컨텍스트가 필요한 경우 {@link #buildRuntimeContext}를 오버라이드한다.
|
||||
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
|
||||
*
|
||||
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
|
||||
* AAD 관련 추가 프로퍼티:
|
||||
* CRYPTO_AAD_PROP tempProp에서 AAD 값을 조회할 키 이름.
|
||||
* 미설정 또는 해당 키 없으면 aad=null → IV를 AAD 대체값으로 사용.
|
||||
*/
|
||||
public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientAdapterFilter {
|
||||
|
||||
public static final String PROP_AAD_PROP = "CRYPTO_AAD_PROP";
|
||||
|
||||
/**
|
||||
* DYNAMIC 키 도출용 컨텍스트를 구성한다.
|
||||
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
|
||||
* 서브클래스에서 오버라이드하여 tempProp으로부터 값을 추출할 수 있다.
|
||||
*/
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, Properties tempProp) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* CRYPTO_AAD_PROP 프로퍼티에 지정된 키로 tempProp에서 AAD 값을 조회한다.
|
||||
* 미설정 또는 값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
|
||||
*/
|
||||
protected byte[] buildAad(Properties prop, Properties tempProp) {
|
||||
String propKey = prop.getProperty(PROP_AAD_PROP);
|
||||
if (StringUtils.isBlank(propKey) || tempProp == null) {
|
||||
return null;
|
||||
}
|
||||
String value = tempProp.getProperty(propKey);
|
||||
return StringUtils.isBlank(value) ? null : value.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, tempProp);
|
||||
byte[] aad = buildAad(prop, tempProp);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return encryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(prop, PROP_ENC_FROM_PATH, PATH_ROOT),
|
||||
getProp(prop, PROP_ENC_TO_PATH, PATH_ENCDATA));
|
||||
}
|
||||
return encryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, tempProp);
|
||||
byte[] aad = buildAad(prop, tempProp);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return decryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(prop, PROP_DEC_FROM_PATH, PATH_ENCDATA),
|
||||
getProp(prop, PROP_DEC_TO_PATH, PATH_ROOT));
|
||||
}
|
||||
return decryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class SimpleFrameworkFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String CLIENTID = "clientId";
|
||||
public static final String TRACEID = "traceId";
|
||||
public static final String GUID = "guid";
|
||||
public static final String RECEIVEDTIMESTAMP = "receivedTimestamp";
|
||||
public static final String PARTNER_TRACE_ID = "partnerTraceId";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("SimpleFrameworkFilter] PreFilter Processing Start!!");
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
try {
|
||||
/* 업체정보 */
|
||||
String clientId = tempProp.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
|
||||
if (StringUtils.isEmpty(clientId)) clientId = "";
|
||||
filterHeaders.put(CLIENTID, clientId);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+CLIENTID+"], value ["+clientId+"]");
|
||||
|
||||
/* APIM 거래추적자 */
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
filterHeaders.put(TRACEID, uuid);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+TRACEID+"], value ["+uuid+"]");
|
||||
|
||||
/* GUID */
|
||||
String guid = tempProp.getProperty(TransactionContextKeys.GUID, "");
|
||||
filterHeaders.put(GUID, guid);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+GUID+"], value ["+guid+"]");
|
||||
|
||||
/* 최초요청시간 */
|
||||
String receivedTimestamp = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_REQUESTED_TIME, "");
|
||||
filterHeaders.put(RECEIVEDTIMESTAMP, receivedTimestamp);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+RECEIVEDTIMESTAMP+"], value ["+receivedTimestamp+"]");
|
||||
|
||||
String partnerTraceId = inboundHeaderMap.getOrDefault(PARTNER_TRACE_ID, "");
|
||||
filterHeaders.put(PARTNER_TRACE_ID, partnerTraceId);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+PARTNER_TRACE_ID+"], value ["+partnerTraceId+"]");
|
||||
|
||||
String x_loan_token = inboundHeaderMap.getOrDefault(TransactionContextKeys.X_LOAN_TOKEN, "");
|
||||
filterHeaders.put(TransactionContextKeys.X_LOAN_TOKEN, x_loan_token);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+TransactionContextKeys.X_LOAN_TOKEN+"], value ["+x_loan_token+"]");
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
|
||||
public class SimpleframeworkBodyFilter extends SimpleFrameworkFilter {
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
Object returnMessage = super.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
|
||||
JSONObject bizJsonStr = null;
|
||||
|
||||
if ( returnMessage instanceof String ) {
|
||||
bizJsonStr = (JSONObject) JSONValue.parse( (String)returnMessage);
|
||||
if( bizJsonStr != null ) {
|
||||
bizJsonStr.put("GUID", tempProp.getOrDefault(TransactionContextKeys.GUID, "") );
|
||||
}
|
||||
}
|
||||
|
||||
return bizJsonStr != null ? bizJsonStr.toJSONString() : returnMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class TOSSBankFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "tossbank";
|
||||
public static final String X_TOSSBANK_LOAN_TRACE_ID = "X-TOSSBANK-LOAN-TRACE-ID";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("TOSSBankFilter] PreFilter Processing Start!!");
|
||||
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_TOSSBANK_LOAN_TRACE_ID)) {
|
||||
filterHeaders.put(X_TOSSBANK_LOAN_TRACE_ID, inboundHeaderMap.get(X_TOSSBANK_LOAN_TRACE_ID));
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+inboundHeaderMap.get(X_TOSSBANK_LOAN_TRACE_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_TOSSBANK_LOAN_TRACE_ID, traceId);
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+traceId+"]");
|
||||
}
|
||||
} else {
|
||||
/* APIM 거래추적자 */
|
||||
filterHeaders.setProperty(X_TOSSBANK_LOAN_TRACE_ID, traceId);
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+traceId+"]");
|
||||
}
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+filterHeaders.getProperty(X_TOSSBANK_LOAN_TRACE_ID)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.nimbusds.jose.shaded.gson.JsonObject;
|
||||
|
||||
public class TraceBodyFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String BIZ_HEADER = "header";
|
||||
public static final String GUID = "guid";
|
||||
public static final String TRACEID = "traceId";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
JSONObject bizJsonStr = null;
|
||||
|
||||
if (message instanceof String) {
|
||||
|
||||
bizJsonStr = (JSONObject) JSONValue.parse((String) message);
|
||||
|
||||
if ( bizJsonStr == null ) return message; //json string이 아님.
|
||||
|
||||
if (bizJsonStr.containsKey(BIZ_HEADER)) {
|
||||
JSONObject bizHeader = (JSONObject) bizJsonStr.get(BIZ_HEADER);
|
||||
|
||||
putBizHeaderData(bizHeader, tempProp);
|
||||
|
||||
}else {
|
||||
|
||||
JSONObject bizHeader = new JSONObject();
|
||||
bizJsonStr.put(BIZ_HEADER, bizHeader);
|
||||
putBizHeaderData(bizHeader, tempProp);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return bizJsonStr != null ? bizJsonStr.toJSONString() : message;
|
||||
}
|
||||
|
||||
static private void putBizHeaderData(JSONObject bizHeader, Properties tempProp) {
|
||||
|
||||
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
|
||||
String guid = tempProp.getProperty(TransactionContextKeys.GUID, "");
|
||||
|
||||
bizHeader.put(GUID, guid);
|
||||
logger.debug("Processing Key [" + GUID + "], value [" + guid + "]");
|
||||
|
||||
bizHeader.put(TRACEID, traceId);
|
||||
logger.debug("Processing Key [" + TRACEID + "], value [" + traceId + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
// TODO Auto-generated method stub
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
@@ -13,10 +14,12 @@ import com.eactive.eai.adapter.RequestDispatcher;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactoryKjb;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactory;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.UUIDGenerator;
|
||||
|
||||
public abstract class HttpAdapterServiceSupport implements HttpAdapterService, HttpAdapterServiceKey {
|
||||
private static final String LOG_PREFIX = "HttpAdapterServiceSupport] ";
|
||||
@@ -27,53 +30,70 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
|
||||
public Object service(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
boolean bMDCput = false;
|
||||
if(uuid == null) {
|
||||
uuid = UUIDGenerator.getUUID().toString().replaceAll("-", "");
|
||||
prop.setProperty(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
}
|
||||
|
||||
String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID);
|
||||
if (StringUtils.isNotBlank(transactionId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.TRANSACTION_ID, transactionId);
|
||||
if(MDC.get(Logger.DISCRIMINATOR) == null) {
|
||||
MDC.put(Logger.DISCRIMINATOR, uuid);
|
||||
bMDCput = true;
|
||||
}
|
||||
|
||||
try {
|
||||
String clientId = request.getHeader(HEADER_NAME_CLIENT_ID);
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
prop.put(PROPERTIES_NAME_CLIENT_ID, clientId);
|
||||
String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID);
|
||||
if (StringUtils.isNotBlank(transactionId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.TRANSACTION_ID, transactionId);
|
||||
}
|
||||
|
||||
String instanceId = request.getHeader(HttpClientAdapterServiceKey.INSTANCE_ID);
|
||||
if (StringUtils.isNotBlank(instanceId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
|
||||
try {
|
||||
String clientId = request.getHeader(HEADER_NAME_CLIENT_ID);
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
prop.put(PROPERTIES_NAME_CLIENT_ID, clientId);
|
||||
}
|
||||
|
||||
String instanceId = request.getHeader(HttpClientAdapterServiceKey.INSTANCE_ID);
|
||||
if (StringUtils.isNotBlank(instanceId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn(LOG_PREFIX + "Failed to read request headers: " + e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
Object obj = null;
|
||||
try {
|
||||
message = doPreFilters(adptGrpName, adptName, message, prop, request, response);
|
||||
obj = RequestDispatcher.getRequestDispatcher(adptGrpName).handle(adptName, message, prop);
|
||||
obj = doPostFilters(adptGrpName, adptName, obj, prop, request, response);
|
||||
} catch (HttpStatusException e) { // inbound error
|
||||
logger.warn(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (JwtAuthException e) { // filter error
|
||||
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
|
||||
"RECEAIIRP202", e);
|
||||
throw e;
|
||||
} catch (Exception e) { // filter error
|
||||
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
|
||||
"RECEAIIRP201", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (obj == null) {
|
||||
return null;
|
||||
} else if (obj instanceof byte[]) {
|
||||
return (byte[]) obj;
|
||||
} else if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
} else {
|
||||
throw new Exception("RECEAIAHA001");
|
||||
|
||||
Object obj = null;
|
||||
try {
|
||||
message = doPreFilters(adptGrpName, adptName, message, prop, request, response);
|
||||
obj = RequestDispatcher.getRequestDispatcher(adptGrpName).handle(adptName, message, prop);
|
||||
obj = doPostFilters(adptGrpName, adptName, obj, prop, request, response);
|
||||
} catch (HttpStatusException e) { // inbound error
|
||||
logger.warn(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (JwtAuthException e) { // filter error
|
||||
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
|
||||
"RECEAIIRP202", e);
|
||||
throw e;
|
||||
} catch (Exception e) { // filter error
|
||||
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
|
||||
"RECEAIIRP201", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (obj == null) {
|
||||
return null;
|
||||
} else if (obj instanceof byte[]) {
|
||||
return (byte[]) obj;
|
||||
} else if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
} else {
|
||||
throw new Exception("RECEAIAHA001");
|
||||
}
|
||||
} finally {
|
||||
if(bMDCput)
|
||||
MDC.remove(Logger.DISCRIMINATOR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +105,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
if (!"HTC".equals(group.getType())) {
|
||||
String allowIp = prop.getProperty(ALLOW_IP,"");
|
||||
if (StringUtils.isNotBlank(allowIp)) {
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(HttpAdapterFilterType.ADAPTERALLOWIPLISTFILTER.toString());
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(HttpAdapterFilterType.ADAPTERALLOWIPLISTFILTER.toString());
|
||||
if (adapterFilter != null) {
|
||||
message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response);
|
||||
}
|
||||
@@ -103,7 +123,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
continue;
|
||||
}
|
||||
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(filterName.trim());
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(filterName.trim());
|
||||
if (adapterFilter != null) {
|
||||
message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response);
|
||||
} else {
|
||||
@@ -126,7 +146,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
continue;
|
||||
}
|
||||
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(filterName.trim());
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(filterName.trim());
|
||||
if (adapterFilter != null) {
|
||||
resultMessage = adapterFilter.doPostFilter(adptGrpName, adptName, resultMessage, prop, request,
|
||||
response);
|
||||
|
||||
+24
-10
@@ -6,13 +6,10 @@ import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HttpAdapterFilterFactory {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
static String basePackage = "com.eactive.eai.adapter.http.dynamic.filter";
|
||||
static String customBasePackage = "com.eactive.eai.custom.adapter.http.dynamic.filter";
|
||||
static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>();
|
||||
|
||||
public HttpAdapterFilterFactory() {
|
||||
super();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static HttpAdapterFilter createFilter(String type) {
|
||||
if (h.containsKey(type)) {
|
||||
return (HttpAdapterFilter) h.get(type);
|
||||
@@ -32,12 +29,19 @@ public class HttpAdapterFilterFactory {
|
||||
case ADAPTERALLOWIPLISTFILTER:
|
||||
filter = new AdapterAllowIPListFilter();
|
||||
break;
|
||||
case HMAC_SHA256:
|
||||
filter = new HmacSha256VerifyFilterKjb();
|
||||
break;
|
||||
case REFLECTALLHEADER:
|
||||
filter = new ReflectAllHeaderFilter();
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
Class cl = Class.forName(type);
|
||||
filter = (HttpAdapterFilter) cl.newInstance();
|
||||
} catch (Exception e) {
|
||||
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
|
||||
filter = classForName(type);
|
||||
if (filter == null) {
|
||||
filter = classForName(basePackage + "." + type);
|
||||
}
|
||||
if (filter == null) {
|
||||
filter = classForName(customBasePackage + "." + type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -49,4 +53,14 @@ public class HttpAdapterFilterFactory {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpAdapterFilter classForName(String type) {
|
||||
try {
|
||||
Class<?> cl = Class.forName(type);
|
||||
return (HttpAdapterFilter) cl.newInstance();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
|
||||
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HttpAdapterFilterFactoryKjb extends HttpAdapterFilterFactory {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
static String basePackage = "com.eactive.eai.custom.adapter.http.dynamic.filter";
|
||||
static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>();
|
||||
|
||||
public HttpAdapterFilterFactoryKjb() {
|
||||
super();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static HttpAdapterFilter createFilter(String type) {
|
||||
if (h.containsKey(type)) {
|
||||
return (HttpAdapterFilter) h.get(type);
|
||||
}
|
||||
|
||||
HttpAdapterFilter filter = null;
|
||||
switch (HttpAdapterFilterType.getValue(type)) {
|
||||
case APIAUTHFILTER:
|
||||
filter = new ApiAuthFilter();
|
||||
break;
|
||||
case JWTAUTHFILTER:
|
||||
filter = new JwtAuthFilter();
|
||||
break;
|
||||
case IPWHITELISTFILTER:
|
||||
filter = new IpWhiteListFilter();
|
||||
break;
|
||||
case ADAPTERALLOWIPLISTFILTER:
|
||||
filter = new AdapterAllowIPListFilter();
|
||||
break;
|
||||
case HMAC_SHA256:
|
||||
filter = new HmacSha256VerifyFilterKjb();
|
||||
break;
|
||||
case REFLECTALLHEADER:
|
||||
filter = new ReflectAllHeaderFilter();
|
||||
break;
|
||||
default:
|
||||
filter = classForName(type);
|
||||
if (filter == null) {
|
||||
filter = classForName(basePackage + "." + type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
h.put(type, filter);
|
||||
return filter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpAdapterFilter classForName(String type) {
|
||||
try {
|
||||
Class<?> cl = Class.forName(type);
|
||||
return (HttpAdapterFilter) cl.newInstance();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
|
||||
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
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.adapter.http.filter.AbstractCryptoFilter;
|
||||
|
||||
/**
|
||||
* HTTP 서버 어댑터용 암복호화 필터 (수신 방향).
|
||||
*
|
||||
* doPreFilter : 수신 요청 복호화 (CRYPTO_DEC_* 프로퍼티 기반)
|
||||
* doPostFilter : 송신 응답 암호화 (CRYPTO_ENC_* 프로퍼티 기반)
|
||||
*
|
||||
* DYNAMIC 키 컨텍스트가 필요한 경우 {@link #buildRuntimeContext}를 오버라이드한다.
|
||||
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
|
||||
*
|
||||
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
|
||||
*/
|
||||
public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterFilter {
|
||||
|
||||
/**
|
||||
* DYNAMIC 키 도출용 컨텍스트를 구성한다.
|
||||
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
|
||||
* 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다.
|
||||
*/
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* CRYPTO_AAD_HEADER 프로퍼티에 지정된 헤더값을 UTF-8 바이트로 반환한다.
|
||||
* 미설정 또는 헤더값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
|
||||
*/
|
||||
protected byte[] buildAad(Properties prop, HttpServletRequest request) {
|
||||
String headerName = prop.getProperty(PROP_AAD_HEADER);
|
||||
if (StringUtils.isBlank(headerName) || request == null) {
|
||||
return null;
|
||||
}
|
||||
String headerValue = request.getHeader(headerName);
|
||||
return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
|
||||
byte[] aad = buildAad(prop, request);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return decryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(prop, PROP_DEC_FROM_PATH, PATH_ENCDATA),
|
||||
getProp(prop, PROP_DEC_TO_PATH, PATH_ROOT));
|
||||
}
|
||||
return decryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(resultMessage);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
|
||||
byte[] aad = buildAad(prop, request);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return encryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(prop, PROP_ENC_FROM_PATH, PATH_ROOT),
|
||||
getProp(prop, PROP_ENC_TO_PATH, PATH_ENCDATA));
|
||||
}
|
||||
return encryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.eactive.eai.adapter.http.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
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;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* InCryptoFilter / OutCryptoFilter 공통 기반 클래스.
|
||||
*
|
||||
* BODY/FIELD 범위 암복호화 로직, 프로퍼티 상수, 유틸리티 메서드를 제공한다.
|
||||
* HTTP 서버 어댑터용은 {@link InCryptoFilter}, 클라이언트 어댑터용은 {@link OutCryptoFilter} 참조.
|
||||
*
|
||||
* 프로퍼티 키:
|
||||
* CRYPTO_MODULE_NAME 필수. DB에 등록된 암호화 모듈명.
|
||||
* CRYPTO_SCOPE BODY | FIELD (기본값: FIELD)
|
||||
* BODY : 메시지 전체를 암복호화 (Base64 인코딩/디코딩)
|
||||
* FIELD : 특정 JSON 경로의 값만 암복호화
|
||||
*
|
||||
* FIELD 범위 전용:
|
||||
* CRYPTO_DEC_FROM_PATH 복호화 대상 JSON 경로 (기본값: /encrypted_data)
|
||||
* CRYPTO_DEC_TO_PATH 복호화 결과 반영 경로. "/" = 전체 body 교체.
|
||||
* CRYPTO_ENC_FROM_PATH 암호화 대상 JSON 경로. "/" = 전체 body 암호화.
|
||||
* CRYPTO_ENC_TO_PATH 암호화 결과(Base64)를 넣을 JSON 경로 (기본값: /encrypted_data)
|
||||
*
|
||||
* GCM AAD 전용 (선택):
|
||||
* CRYPTO_AAD_HEADER AAD로 사용할 HTTP 요청 헤더명 (InCryptoFilter 전용).
|
||||
* 미설정 또는 헤더값 없으면 aad=null → IV를 AAD 대체값으로 사용.
|
||||
*/
|
||||
public abstract class AbstractCryptoFilter {
|
||||
|
||||
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_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";
|
||||
public static final String PROP_AAD_HEADER = "CRYPTO_AAD_HEADER";
|
||||
|
||||
public static final String SCOPE_BODY = "BODY";
|
||||
public static final String SCOPE_FIELD = "FIELD";
|
||||
public static final String PATH_ROOT = "/";
|
||||
public static final String PATH_ENCDATA = "/encrypted_data";
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 복호화 구현
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
protected String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad) throws Exception {
|
||||
byte[] cipherBytes = Base64.getDecoder().decode(body.trim());
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
|
||||
return new String(plainBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* fromPath의 Base64 값을 복호화하여 toPath에 반영. JSON 파싱 1회.
|
||||
* toPath = "/" → fromPath 필드를 제거하고 복호화된 JSON을 body에 병합.
|
||||
*/
|
||||
protected String decryptField(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad, String fromPath, String toPath) throws Exception {
|
||||
JsonNode root = JsonPathUtil.toTree(body);
|
||||
String encBase64 = JsonPathUtil.getAt(root, 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, aad, cipherBytes);
|
||||
String plainText = new String(plainBytes, StandardCharsets.UTF_8);
|
||||
if (PATH_ROOT.equals(toPath)) {
|
||||
return JsonPathUtil.mergeAtRoot(root, fromPath, plainText);
|
||||
}
|
||||
JsonPathUtil.setAt(root, toPath, plainText);
|
||||
return JsonPathUtil.fromTree(root);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 암호화 구현
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
protected String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad) throws Exception {
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(cipherBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* fromPath 값을 암호화하여 toPath(Base64)에 반영. JSON 파싱 1회.
|
||||
* fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑 (파싱 불필요).
|
||||
*/
|
||||
protected String encryptField(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad, String fromPath, String toPath) throws Exception {
|
||||
if (PATH_ROOT.equals(fromPath)) {
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
|
||||
String fieldName = toPath.replaceFirst("^/", "");
|
||||
return "{\"" + fieldName + "\":\"" + encBase64 + "\"}";
|
||||
}
|
||||
JsonNode root = JsonPathUtil.toTree(body);
|
||||
String plainText = JsonPathUtil.getAt(root, fromPath);
|
||||
if (StringUtils.isBlank(plainText)) {
|
||||
logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath);
|
||||
return body;
|
||||
}
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
plainText.getBytes(StandardCharsets.UTF_8));
|
||||
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
|
||||
JsonPathUtil.setAt(root, toPath, encBase64);
|
||||
return JsonPathUtil.fromTree(root);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 유틸
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 필터로 전달되는 message 객체를 JSON 문자열로 변환한다.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
protected String required(Properties prop, String key) {
|
||||
String v = prop.getProperty(key);
|
||||
if (StringUtils.isBlank(v)) {
|
||||
throw new IllegalArgumentException("CryptoFilter 필수 프로퍼티 누락: " + key);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
protected String getProp(Properties prop, String key, String defaultVal) {
|
||||
String v = prop.getProperty(key);
|
||||
return StringUtils.isBlank(v) ? defaultVal : v;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowAdapterControlCommand extends Command {
|
||||
@@ -27,7 +28,7 @@ public class ReloadInflowAdapterControlCommand extends Command {
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowClientControlCommand extends Command {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ReloadInflowClientControlCommand() {
|
||||
super("ReloadInflowClientControlCommand");
|
||||
}
|
||||
|
||||
public Object execute() throws CommandException {
|
||||
Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
if (logger.isInfo())
|
||||
logger.info(this.name + " is executed");
|
||||
|
||||
if (!(args instanceof String)) {
|
||||
String rspErrorCode = "RECEAIMCM001";
|
||||
String msg = makeException(rspErrorCode, null);
|
||||
if (logger.isError())
|
||||
logger.error(msg);
|
||||
throw new CommandException(msg);
|
||||
}
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager();
|
||||
if (!(base instanceof ClientDualInflowControlManager)) {
|
||||
throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다.");
|
||||
}
|
||||
ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base;
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
manager.reloadClient();
|
||||
if (logger.isWarn())
|
||||
logger.warn(this.name + "] all rule Reload.");
|
||||
} else {
|
||||
manager.reloadClient(keyName);
|
||||
if (logger.isWarn())
|
||||
logger.warn(this.name + "] " + keyName + " Reload.");
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
String msg = makeException(rspErrorCode, e);
|
||||
if (logger.isError())
|
||||
logger.error(msg, e);
|
||||
throw new CommandException(msg);
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowGroupControlCommand extends Command {
|
||||
@@ -28,7 +29,7 @@ public class ReloadInflowGroupControlCommand extends Command {
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowInterfaceControlCommand extends Command {
|
||||
@@ -28,7 +29,7 @@ public class ReloadInflowInterfaceControlCommand extends Command {
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowAdapterControlCommand extends Command {
|
||||
@@ -26,7 +27,7 @@ public class RemoveInflowAdapterControlCommand extends Command {
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
manager.removeAdapter(key);
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowClientControlCommand extends Command {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public RemoveInflowClientControlCommand() {
|
||||
super("RemoveInflowClientControlCommand");
|
||||
}
|
||||
|
||||
public Object execute() throws CommandException {
|
||||
Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
if (logger.isInfo())
|
||||
logger.info(this.name + " is executed");
|
||||
|
||||
if (!(args instanceof String)) {
|
||||
String rspErrorCode = "RECEAIMCM001";
|
||||
String msg = makeException(rspErrorCode, null);
|
||||
if (logger.isError())
|
||||
logger.error(msg);
|
||||
throw new CommandException(msg);
|
||||
}
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager();
|
||||
if (!(base instanceof ClientDualInflowControlManager)) {
|
||||
throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다.");
|
||||
}
|
||||
ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base;
|
||||
manager.removeClient(key);
|
||||
if (logger.isWarn())
|
||||
logger.warn(this.name + "] " + key + " removed.");
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
String msg = makeException(rspErrorCode, e);
|
||||
if (logger.isError())
|
||||
logger.error(msg, e);
|
||||
throw new CommandException(msg);
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowGroupControlCommand extends Command {
|
||||
@@ -28,7 +29,7 @@ public class RemoveInflowGroupControlCommand extends Command {
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
manager.removeGroup(key);
|
||||
if (logger.isWarn())
|
||||
logger.warn(this.name + "] group " + key + " removed.");
|
||||
|
||||
@@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowInterfaceControlCommand extends Command {
|
||||
@@ -25,7 +26,7 @@ public class RemoveInflowInterfaceControlCommand extends Command {
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
manager.removeInterface(key);
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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 "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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,21 @@ import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.session.SessionManager;
|
||||
import com.eactive.eai.common.session.SessionManagerForIgnite;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.mchange.v1.cachedstore.CachedStore.Manager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 1. 기능 : Access 토큰 정보를 DB로 부터 로딩하고 만료시 재발급 정보를 메모리에 관리하는 Manager 클래스
|
||||
@@ -55,13 +61,8 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
/**
|
||||
* 기동 여부
|
||||
*/
|
||||
private boolean started;
|
||||
|
||||
/**
|
||||
* 토큰 정보를 저장하는 collection
|
||||
*/
|
||||
private HashMap<String, AccessTokenVO> tokens;
|
||||
|
||||
private boolean started;
|
||||
|
||||
/**
|
||||
* 인증 정보를 저장하는 Map
|
||||
*/
|
||||
@@ -78,7 +79,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
private static final int TOKEN_SCHEDULER_THREAD_POOL_SIZE = 30;
|
||||
|
||||
public AccessTokenManagerByDB() {
|
||||
tokens = new HashMap<>();
|
||||
scheduledTasks = new ConcurrentHashMap<>();
|
||||
outboundOAuthCredentialVos = new HashMap<>();
|
||||
runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
@@ -117,7 +117,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||
|
||||
try {
|
||||
tokens.clear();
|
||||
init();
|
||||
} catch(Exception e) {
|
||||
throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201"));
|
||||
@@ -168,7 +167,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
logger.error("Scheduler shutdown interrupted", e);
|
||||
}
|
||||
|
||||
this.tokens = null;
|
||||
started = false;
|
||||
|
||||
if (logger.isWarn()){
|
||||
@@ -240,27 +238,34 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
try {
|
||||
logger.debug("Executing token issuance for adapter group: {}", adapterGroupName);
|
||||
|
||||
// 현재 토큰 가져오기
|
||||
AccessTokenVO accessToken = tokens.get(adapterGroupName);
|
||||
SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function<AccessTokenVO, AccessTokenVO>() {
|
||||
|
||||
// 다음 스케줄 시간 계산
|
||||
long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000);
|
||||
// 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급
|
||||
if (accessToken == null) {
|
||||
issueToken(credential, false);
|
||||
} else if(accessToken.getExpiration() != null) {
|
||||
// 토큰이 있고 만료시간이 설정 된 경우
|
||||
if(accessToken.getExpiration().before(new Date(intervalTime))){
|
||||
// 다음 스케줄 전에 토큰이 만료되는 경우 재발급
|
||||
issueToken(credential, true);
|
||||
@Override
|
||||
public AccessTokenVO apply(AccessTokenVO accessToken) {
|
||||
|
||||
long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000);
|
||||
// // 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급
|
||||
if (accessToken == null) {
|
||||
return issueToken(credential);
|
||||
} else if(accessToken.getExpiration() != null) {
|
||||
// 토큰이 있고 만료시간이 설정 된 경우
|
||||
if(accessToken.getExpiration().before(new Date(intervalTime))){
|
||||
// 다음 스케줄 전에 토큰이 만료되는 경우 재발급
|
||||
return issueToken(credential);
|
||||
} else {
|
||||
// 토큰이 아직 유효한 경우
|
||||
logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
|
||||
return accessToken;
|
||||
}
|
||||
} else {
|
||||
// 토큰이 아직 유효한 경우
|
||||
logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
|
||||
//토큰이 있지만 만료 시간이 없는 경우
|
||||
logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName);
|
||||
return accessToken;
|
||||
}
|
||||
} else {
|
||||
//토큰이 있지만 만료 시간이 없는 경우
|
||||
logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
logger.debug("Token issuance completed for adapter group: {}", adapterGroupName);
|
||||
} catch (Exception e) {
|
||||
@@ -282,19 +287,24 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
private void issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo, boolean isTokenExpiredWithinInterval) throws Exception {
|
||||
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
|
||||
boolean isExpired = true;
|
||||
private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo) {
|
||||
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
|
||||
AccessTokenVO accessToken = null;
|
||||
try {
|
||||
|
||||
// boolean isExpired = true;
|
||||
|
||||
// if (tokens.containsKey(adapterGroupName)) {
|
||||
// AccessTokenVO accessToken = tokens.get(adapterGroupName);
|
||||
// isExpired = accessToken.isExpired();
|
||||
// }
|
||||
// logger.debug("Token status for adapter group: {}, expired: {}, isTokenExpiredWithinInterval: {}", adapterGroupName, isExpired, isTokenExpiredWithinInterval);
|
||||
|
||||
if (tokens.containsKey(adapterGroupName)) {
|
||||
AccessTokenVO accessToken = tokens.get(adapterGroupName);
|
||||
isExpired = accessToken.isExpired();
|
||||
}
|
||||
logger.debug("Token status for adapter group: {}, expired: {}, isTokenExpiredWithinInterval: {}", adapterGroupName, isExpired, isTokenExpiredWithinInterval);
|
||||
|
||||
if (isExpired || isTokenExpiredWithinInterval) {
|
||||
// if (isExpired || isTokenExpiredWithinInterval) {
|
||||
|
||||
|
||||
logger.info("Issuing new token for adapter group: {}", adapterGroupName);
|
||||
tokens.remove(adapterGroupName);
|
||||
// tokens.remove(adapterGroupName);
|
||||
|
||||
AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
|
||||
|
||||
@@ -306,16 +316,23 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
|
||||
if(service != null) {
|
||||
logger.debug("Executing token service of type: {} for adapter group: {}", type, adapterGroupName);
|
||||
AccessTokenVO accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
|
||||
this.tokens.put(adapterGroupName, accessToken);
|
||||
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
|
||||
logger.info("New token issued successfully for adapter group: {}", adapterGroupName);
|
||||
return accessToken;
|
||||
|
||||
} else {
|
||||
logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
|
||||
}
|
||||
} else {
|
||||
logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
}
|
||||
|
||||
}catch (Exception e) {
|
||||
logger.error("Task execution failed for adapter group: {}", adapterGroupName, e);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
|
||||
}
|
||||
|
||||
public void stopToken(String adapterGroupName) {
|
||||
@@ -324,7 +341,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
task.cancel(true);
|
||||
logger.warn("Token task stopped for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
tokens.remove(adapterGroupName);
|
||||
SessionManager.getInstance().removeOutboundAccessToken(adapterGroupName);
|
||||
logger.debug("Token removed for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
|
||||
@@ -382,15 +399,28 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
* @return Access 토큰 정보
|
||||
**/
|
||||
public AccessTokenVO getAccessTokenVO(String adapterGroupName) {
|
||||
AccessTokenVO accessToken = null;
|
||||
|
||||
try {
|
||||
accessToken = tokens.get(adapterGroupName);
|
||||
} catch(Exception e) {
|
||||
if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
|
||||
}
|
||||
AccessTokenVO accessToken = null;
|
||||
|
||||
return accessToken;
|
||||
try {
|
||||
accessToken = SessionManager.getInstance().getOutboundAccessToken(adapterGroupName,
|
||||
new Function<AccessTokenVO, AccessTokenVO>() {
|
||||
|
||||
@Override
|
||||
public AccessTokenVO apply(AccessTokenVO t) {
|
||||
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos
|
||||
.get(adapterGroupName);
|
||||
|
||||
return issueToken(outboundOAuthCredentialVo);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
if (logger.isError())
|
||||
logger.error("토큰을 찾을 수 없습니다. [" + adapterGroupName + "] ", e);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,9 +433,9 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
**/
|
||||
public String getAccessToken(String adapterGroupName) {
|
||||
String accessToken = null;
|
||||
|
||||
|
||||
try {
|
||||
accessToken = tokens.get(adapterGroupName).getAccessToken();
|
||||
accessToken = getAccessTokenVO(adapterGroupName).getAccessToken();
|
||||
} catch(Exception e) {
|
||||
if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
|
||||
}
|
||||
@@ -435,27 +465,29 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
* 3. 주의사항
|
||||
*
|
||||
**/
|
||||
public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties, String oldToken) throws Exception {
|
||||
|
||||
AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName);
|
||||
public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties,
|
||||
String oldToken) throws Exception {
|
||||
|
||||
// 동시 요청이 발생할 경우 synchronized 처리를 했기때문에 토큰을 다시 한번 체크한다.
|
||||
if (accessToken == null || accessToken.isExpired()
|
||||
|| StringUtils.equals(accessToken.getAccessToken(), oldToken)) {
|
||||
AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName);
|
||||
|
||||
// 동시 요청이 발생할 경우 synchronized 처리를 했기때문에 토큰을 다시 한번 체크한다.
|
||||
if (accessToken == null || accessToken.isExpired()
|
||||
|| StringUtils.equals(accessToken.getAccessToken(), oldToken)) {
|
||||
|
||||
if (isOAuthCredentialRegistered(adapterGroupName) == false) {
|
||||
throw new Exception(
|
||||
"There is no OAuthCredentialRegistered information, or whether to use it is 'N'.");
|
||||
}
|
||||
String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
|
||||
|
||||
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
if (service != null) {
|
||||
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
|
||||
outboundOAuthCredentialVo);
|
||||
}
|
||||
}
|
||||
|
||||
if(isOAuthCredentialRegistered(adapterGroupName) == false){
|
||||
throw new Exception("There is no OAuthCredentialRegistered information, or whether to use it is 'N'.");
|
||||
}
|
||||
String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
|
||||
|
||||
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
if(service != null) {
|
||||
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
|
||||
}
|
||||
tokens.put(adapterGroupName, accessToken);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-16
@@ -17,10 +17,6 @@ import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.custom.alarm.AlarmStateManager;
|
||||
import com.eactive.eai.custom.alarm.condition.AlarmCondition;
|
||||
import com.eactive.eai.custom.alarm.event.AlarmEvent;
|
||||
import com.eactive.eai.custom.alarm.key.CoreAlarmKey;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
@@ -45,7 +41,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
TypedCircuitBreakerVO defaultConfigVo;
|
||||
HashMap<String, TypedCircuitBreakerVO> idCBConfigMap = new HashMap<>();
|
||||
HashMap<String, TypedCircuitBreakerVO> apiIdCBConfigMap = new HashMap<>();
|
||||
HashMap<String, TypedCircuitBreakerVO> urlCBConfigMap = new HashMap<>();
|
||||
// HashMap<String, TypedCircuitBreakerVO> urlCBConfigMap = new HashMap<>();
|
||||
private boolean started;
|
||||
|
||||
@Autowired
|
||||
@@ -116,7 +112,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
CircuitBreakerConfig circuitBreakerConfig = this.createCircuitBreakerConfig(vo);
|
||||
vo.setCircuitBreakerConfig(circuitBreakerConfig);
|
||||
this.apiIdCBConfigMap.put(vo.getName(), vo);
|
||||
this.idCBConfigMap.remove(vo.getId(), vo);
|
||||
this.idCBConfigMap.put(vo.getId(), vo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +336,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
switch (event.getEventType()) {
|
||||
case ERROR :
|
||||
TypedCircuitBreakerManager.logger
|
||||
.error("CirbuitBreaker ERROR event occurred. CircuitBreakerEvent={}" + event);
|
||||
.error("CircuitBreaker ERROR event occurred. CircuitBreakerEvent={}" + event);
|
||||
break;
|
||||
case STATE_TRANSITION :
|
||||
if (event instanceof CircuitBreakerOnStateTransitionEvent) {
|
||||
@@ -349,7 +345,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
CircuitBreaker.State toState = stateTransition.getToState();
|
||||
CircuitBreaker.State fromState = stateTransition.getFromState();
|
||||
TypedCircuitBreakerManager.logger.error(
|
||||
"CirbuitBreaker STATE_TRANSITION event occurred. {} : {} -> {}. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
"CircuitBreaker STATE_TRANSITION event occurred. {} : {} -> {}. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
new Object[]{event.getCircuitBreakerName(), fromState, toState, event, this.vo});
|
||||
// if (!toState.equals(State.OPEN) && !toState.equals(State.HALF_OPEN)
|
||||
// && !toState.equals(State.CLOSED)) {
|
||||
@@ -359,18 +355,19 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
// } else {
|
||||
// this.sendAlert(stateTransitionEvent);
|
||||
// }
|
||||
AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker)
|
||||
.condition(AlarmCondition.builder().build())
|
||||
.message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", event.getCircuitBreakerName(), fromState, toState))
|
||||
.build();
|
||||
|
||||
AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent);
|
||||
|
||||
// AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker)
|
||||
// .condition(AlarmCondition.builder().build())
|
||||
// .message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", event.getCircuitBreakerName(), fromState, toState))
|
||||
// .build();
|
||||
//
|
||||
// AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent);
|
||||
|
||||
logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", event.getCircuitBreakerName(), fromState, toState);
|
||||
TypedCircuitBreakerManager.logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", event.getCircuitBreakerName(), fromState, toState);
|
||||
|
||||
} else {
|
||||
TypedCircuitBreakerManager.logger.error(
|
||||
"CirbuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
"CircuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
new Object[]{event, this.vo});
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
package com.eactive.eai.common.inflow;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.Refill;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
import io.github.bucket4j.local.LocalBucketBuilder;
|
||||
|
||||
public abstract class AbstractInflowControlManager implements Lifecycle, Bucket {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
public static final String QUOTA_TIMEUNIT_SECOND = "SEC";
|
||||
public static final String QUOTA_TIMEUNIT_MINUTE = "MIN";
|
||||
public static final String QUOTA_TIMEUNIT_HOUR = "HOU";
|
||||
public static final String QUOTA_TIMEUNIT_DAY = "DAY";
|
||||
public static final String QUOTA_TIMEUNIT_MONTH = "MON";
|
||||
|
||||
private volatile Map<String, CustomBucket> adapterBucketList = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, CustomBucket> interfaceBucketList = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, CustomGroupBucket> groupBucketList = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, String> interfaceToGroupMap = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, InflowGroupVO> groupVoMap = new ConcurrentHashMap<>();
|
||||
private boolean started;
|
||||
private LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
@Autowired
|
||||
protected InflowControlDAO inflowControlDAO;
|
||||
|
||||
public void start() throws LifecycleException {
|
||||
if (started)
|
||||
throw new LifecycleException("RECEAICMM201");
|
||||
|
||||
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||
try {
|
||||
init();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAICMM202"));
|
||||
}
|
||||
started = true;
|
||||
lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
|
||||
}
|
||||
|
||||
private void init() throws Exception {
|
||||
initAdapter();
|
||||
initInterface();
|
||||
initGroup();
|
||||
}
|
||||
|
||||
private void initAdapter() throws Exception {
|
||||
Map<String, CustomBucket> newMap = new HashMap<>();
|
||||
List<InflowTargetVO> inflowAdapterVoList = inflowControlDAO.getInflowAdapterList();
|
||||
|
||||
for (InflowTargetVO inflowVo : inflowAdapterVoList) {
|
||||
if (AbstractInflowControlManager.isInflowTarget(inflowVo)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowVo);
|
||||
if (bucket != null) {
|
||||
newMap.put(inflowVo.getName(), bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.adapterBucketList = newMap;
|
||||
}
|
||||
|
||||
private void initInterface() throws Exception {
|
||||
Map<String, CustomBucket> newMap = new HashMap<>();
|
||||
List<InflowTargetVO> inflowInterfaceVoList = inflowControlDAO.getInflowInterfaceList();
|
||||
|
||||
for (InflowTargetVO inflowVo : inflowInterfaceVoList) {
|
||||
if (AbstractInflowControlManager.isInflowTarget(inflowVo)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowVo);
|
||||
if (bucket != null) {
|
||||
newMap.put(inflowVo.getName(), bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.interfaceBucketList = newMap;
|
||||
}
|
||||
|
||||
private void initGroup() throws Exception {
|
||||
Map<String, CustomGroupBucket> newGroupBucketList = new HashMap<>();
|
||||
Map<String, String> newInterfaceToGroupMap = new HashMap<>();
|
||||
Map<String, InflowGroupVO> newGroupVoMap = new HashMap<>();
|
||||
|
||||
List<InflowGroupVO> inflowGroupVoList = inflowControlDAO.getInflowGroupList();
|
||||
|
||||
for (InflowGroupVO groupVo : inflowGroupVoList) {
|
||||
if (isInflowGroupTarget(groupVo)) {
|
||||
CustomGroupBucket bucket = makeGroupBucket(groupVo);
|
||||
if (bucket != null) {
|
||||
newGroupBucketList.put(groupVo.getGroupId(), bucket);
|
||||
newGroupVoMap.put(groupVo.getGroupId(), groupVo);
|
||||
for (String interfaceId : groupVo.getInterfaceList()) {
|
||||
newInterfaceToGroupMap.put(interfaceId, groupVo.getGroupId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("InflowControlManager] initGroup completed. groups=" + newGroupBucketList.size()
|
||||
+ ", mappings=" + newInterfaceToGroupMap.size());
|
||||
}
|
||||
|
||||
this.groupBucketList = newGroupBucketList;
|
||||
this.interfaceToGroupMap = newInterfaceToGroupMap;
|
||||
this.groupVoMap = newGroupVoMap;
|
||||
}
|
||||
|
||||
public synchronized void reloadAdapter() throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all Started ...");
|
||||
}
|
||||
initAdapter();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadInterface() throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all Started ...");
|
||||
}
|
||||
initInterface();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadAdapter(String adapter) throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload Started ...");
|
||||
}
|
||||
Map<String, InflowTargetVO> map = inflowControlDAO.getInflowTargetByAdater(adapter);
|
||||
if (map != null) {
|
||||
Iterator<String> it = map.keySet().iterator();
|
||||
String key = "";
|
||||
while (it.hasNext()) {
|
||||
key = it.next();
|
||||
|
||||
InflowTargetVO inflowTarget = map.get(key);
|
||||
if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget);
|
||||
if (bucket != null) {
|
||||
adapterBucketList.put(inflowTarget.getName(), bucket);
|
||||
} else {
|
||||
adapterBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
} else {
|
||||
adapterBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadInterface(String inter) throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload Started ...");
|
||||
}
|
||||
Map<String, InflowTargetVO> map = inflowControlDAO.getInflowTargetByInterface(inter);
|
||||
if (map != null) {
|
||||
Iterator<String> it = map.keySet().iterator();
|
||||
String key = "";
|
||||
while (it.hasNext()) {
|
||||
key = it.next();
|
||||
|
||||
InflowTargetVO inflowTarget = map.get(key);
|
||||
if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget);
|
||||
if (bucket != null) {
|
||||
interfaceBucketList.put(inflowTarget.getName(), bucket);
|
||||
} else {
|
||||
interfaceBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
} else {
|
||||
interfaceBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadGroup() throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup all Started ...");
|
||||
}
|
||||
initGroup();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup all finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadGroup(String groupId) throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup Started ... groupId=" + groupId);
|
||||
}
|
||||
Map<String, InflowGroupVO> map = inflowControlDAO.getInflowTargetByGroup(groupId);
|
||||
if (map != null) {
|
||||
Iterator<Map.Entry<String, String>> mappingIt = interfaceToGroupMap.entrySet().iterator();
|
||||
while (mappingIt.hasNext()) {
|
||||
Map.Entry<String, String> entry = mappingIt.next();
|
||||
if (groupId.equals(entry.getValue())) {
|
||||
mappingIt.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Iterator<String> it = map.keySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
InflowGroupVO groupVo = map.get(key);
|
||||
|
||||
if (isInflowGroupTarget(groupVo)) {
|
||||
CustomGroupBucket bucket = makeGroupBucket(groupVo);
|
||||
if (bucket != null) {
|
||||
groupBucketList.put(groupVo.getGroupId(), bucket);
|
||||
groupVoMap.put(groupVo.getGroupId(), groupVo);
|
||||
for (String interfaceId : groupVo.getInterfaceList()) {
|
||||
interfaceToGroupMap.put(interfaceId, groupVo.getGroupId());
|
||||
}
|
||||
} else {
|
||||
groupBucketList.remove(groupVo.getGroupId());
|
||||
groupVoMap.remove(groupVo.getGroupId());
|
||||
}
|
||||
} else {
|
||||
groupBucketList.remove(groupVo.getGroupId());
|
||||
groupVoMap.remove(groupVo.getGroupId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() throws LifecycleException {
|
||||
if (!started)
|
||||
throw new LifecycleException("RECEAICMM203");
|
||||
|
||||
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
|
||||
|
||||
adapterBucketList = new ConcurrentHashMap<>();
|
||||
interfaceBucketList = new ConcurrentHashMap<>();
|
||||
groupBucketList = new ConcurrentHashMap<>();
|
||||
interfaceToGroupMap = new ConcurrentHashMap<>();
|
||||
groupVoMap = new ConcurrentHashMap<>();
|
||||
|
||||
started = false;
|
||||
lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
|
||||
}
|
||||
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return this.started;
|
||||
}
|
||||
|
||||
public String[] getAdapterAllKeys() {
|
||||
Iterator<String> it = this.adapterBucketList.keySet().iterator();
|
||||
String[] svcCd = new String[this.adapterBucketList.size()];
|
||||
for (int i = 0; it.hasNext(); i++) {
|
||||
svcCd[i] = it.next();
|
||||
}
|
||||
Arrays.sort(svcCd);
|
||||
return svcCd;
|
||||
}
|
||||
|
||||
public String[] getInterfaceAllKeys() {
|
||||
Iterator<String> it = this.interfaceBucketList.keySet().iterator();
|
||||
String[] svcCd = new String[this.interfaceBucketList.size()];
|
||||
for (int i = 0; it.hasNext(); i++) {
|
||||
svcCd[i] = it.next();
|
||||
}
|
||||
Arrays.sort(svcCd);
|
||||
return svcCd;
|
||||
}
|
||||
|
||||
public synchronized void removeAdapter(String adapter) {
|
||||
adapterBucketList.remove(adapter);
|
||||
}
|
||||
|
||||
public synchronized void removeInterface(String inter) {
|
||||
interfaceBucketList.remove(inter);
|
||||
}
|
||||
|
||||
public synchronized void removeGroup(String groupId) {
|
||||
groupBucketList.remove(groupId);
|
||||
groupVoMap.remove(groupId);
|
||||
Iterator<Map.Entry<String, String>> it = interfaceToGroupMap.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, String> entry = it.next();
|
||||
if (groupId.equals(entry.getValue())) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getGroupAllKeys() {
|
||||
Iterator<String> it = this.groupBucketList.keySet().iterator();
|
||||
String[] groupIds = new String[this.groupBucketList.size()];
|
||||
for (int i = 0; it.hasNext(); i++) {
|
||||
groupIds[i] = it.next();
|
||||
}
|
||||
Arrays.sort(groupIds);
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
public CustomGroupBucket getGroupBucket(String groupId) {
|
||||
return groupBucketList.get(groupId);
|
||||
}
|
||||
|
||||
private CustomBucket makeBucketUsingInflowVO(InflowTargetVO inflowTarget) {
|
||||
if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
LocalBucketBuilder builder = Bucket4j.builder();
|
||||
if (inflowTarget.getThresholdPerSecond() > 0) {
|
||||
builder.addLimit(Bandwidth.classic(inflowTarget.getThresholdPerSecond(),
|
||||
Refill.greedy(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1))));
|
||||
}
|
||||
|
||||
if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) {
|
||||
builder.addLimit(Bandwidth.classic(inflowTarget.getThreshold(),
|
||||
Refill.intervally(inflowTarget.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit()))));
|
||||
}
|
||||
|
||||
return new CustomBucket(builder.build(), inflowTarget);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private CustomGroupBucket makeGroupBucket(InflowGroupVO groupVo) {
|
||||
if (!isInflowGroupTarget(groupVo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LocalBucket perSecondBucket = null;
|
||||
LocalBucket thresholdBucket = null;
|
||||
|
||||
if (groupVo.getThresholdPerSecond() > 0) {
|
||||
perSecondBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.classic(groupVo.getThresholdPerSecond(),
|
||||
Refill.greedy(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1))))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) {
|
||||
thresholdBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.classic(groupVo.getThreshold(),
|
||||
Refill.intervally(groupVo.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit()))))
|
||||
.build();
|
||||
}
|
||||
|
||||
return new CustomGroupBucket(perSecondBucket, thresholdBucket, groupVo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAdapterPass(String adapter) {
|
||||
CustomBucket b = adapterBucketList.get(adapter);
|
||||
|
||||
if (b == null)
|
||||
return true;
|
||||
|
||||
return b.getLocalBucket().tryConsume(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInterfacePass(String inter) {
|
||||
CustomBucket b = interfaceBucketList.get(inter);
|
||||
|
||||
if (b == null)
|
||||
return true;
|
||||
|
||||
return b.getLocalBucket().tryConsume(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getAdapterInflowThreashold(String adapter) {
|
||||
CustomBucket b = adapterBucketList.get(adapter);
|
||||
if (b == null)
|
||||
return null;
|
||||
return b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getInterfaceInflowThreashold(String inter) {
|
||||
CustomBucket b = interfaceBucketList.get(inter);
|
||||
if (b == null)
|
||||
return null;
|
||||
return b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getInflowThreashold(String inter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public InflowTargetVO getAdapterInflow(String adapter) {
|
||||
return getAdapterInflowThreashold(adapter);
|
||||
}
|
||||
|
||||
public InflowTargetVO getInterfaceInflow(String inter) {
|
||||
return getInterfaceInflowThreashold(inter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String isGroupPass(String groupId) {
|
||||
CustomGroupBucket b = groupBucketList.get(groupId);
|
||||
|
||||
if (b == null)
|
||||
return null;
|
||||
|
||||
return b.tryConsume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowGroupVO getGroupInflowThreshold(String groupId) {
|
||||
return groupVoMap.get(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupIdByInterface(String interfaceId) {
|
||||
return interfaceToGroupMap.get(interfaceId);
|
||||
}
|
||||
|
||||
public InflowGroupVO getGroupInflow(String groupId) {
|
||||
return getGroupInflowThreshold(groupId);
|
||||
}
|
||||
|
||||
public static boolean isInflowTarget(InflowTargetVO inflowTarget) {
|
||||
if (inflowTarget == null || !inflowTarget.isActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return inflowTarget.getThresholdPerSecond() > 0 || (inflowTarget.getThreshold() > 0 && StringUtils
|
||||
.equalsAnyIgnoreCase(inflowTarget.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND,
|
||||
QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH));
|
||||
|
||||
}
|
||||
|
||||
public static boolean isInflowGroupTarget(InflowGroupVO groupVo) {
|
||||
if (groupVo == null || !groupVo.isActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return groupVo.getThresholdPerSecond() > 0 || (groupVo.getThreshold() > 0 && StringUtils
|
||||
.equalsAnyIgnoreCase(groupVo.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND,
|
||||
QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH));
|
||||
}
|
||||
|
||||
public static Duration getPeriod(String timeUnit) {
|
||||
if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_SECOND)) {
|
||||
return Duration.ofSeconds(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MINUTE)) {
|
||||
return Duration.ofMinutes(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_HOUR)) {
|
||||
return Duration.ofHours(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_DAY)) {
|
||||
return Duration.ofDays(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MONTH)) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
return Duration.ofDays(calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,11 @@
|
||||
package com.eactive.eai.common.inflow;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.message.service.InterfaceMapper;
|
||||
import com.ext.eai.common.stdmessage.STDMessageKeys;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
import io.github.bucket4j.local.LocalBucketBuilder;
|
||||
|
||||
@Component
|
||||
public class InflowControlManager implements Lifecycle, Bucket {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
public static final String QUOTA_TIMEUNIT_SECOND = "SEC";
|
||||
public static final String QUOTA_TIMEUNIT_MINUTE = "MIN";
|
||||
public static final String QUOTA_TIMEUNIT_HOUR = "HOU";
|
||||
public static final String QUOTA_TIMEUNIT_DAY = "DAY";
|
||||
public static final String QUOTA_TIMEUNIT_MONTH = "MON";
|
||||
|
||||
private volatile Map<String, CustomBucket> adapterBucketList = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, CustomBucket> interfaceBucketList = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, CustomGroupBucket> groupBucketList = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, String> interfaceToGroupMap = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, InflowGroupVO> groupVoMap = new ConcurrentHashMap<>();
|
||||
private boolean started;
|
||||
private LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
@Autowired
|
||||
private InflowControlDAO inflowControlDAO;
|
||||
public class InflowControlManager extends AbstractInflowControlManager {
|
||||
|
||||
public static synchronized InflowControlManager getInstance() {
|
||||
return ApplicationContextProvider.getContext().getBean(InflowControlManager.class);
|
||||
@@ -58,485 +14,4 @@ public class InflowControlManager implements Lifecycle, Bucket {
|
||||
public static synchronized Bucket getBucket() {
|
||||
return ApplicationContextProvider.getContext().getBean(InflowControlManager.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 유량제어 대상인 거래인지 체크한다.(사이트에 맞게 구성)
|
||||
*
|
||||
* @param eaiServerManager
|
||||
* @param eaiMessage
|
||||
* @return
|
||||
*/
|
||||
public static Boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) {
|
||||
InterfaceMapper mapper = eaiMessage.getMapper();
|
||||
String returnType = mapper.getSendRecvDivision(eaiMessage.getStandardMessage()); // S|R
|
||||
|
||||
if (STDMessageKeys.SEND_RECV_CD_SEND.equals(returnType)) {
|
||||
return Boolean.valueOf(true);
|
||||
}
|
||||
|
||||
return Boolean.valueOf(false);
|
||||
}
|
||||
|
||||
public void start() throws LifecycleException {
|
||||
if (started)
|
||||
throw new LifecycleException("RECEAICMM201");
|
||||
|
||||
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||
try {
|
||||
init();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAICMM202"));
|
||||
}
|
||||
started = true;
|
||||
lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
|
||||
}
|
||||
|
||||
private void init() throws Exception {
|
||||
initAdapter();
|
||||
initInterface();
|
||||
initGroup();
|
||||
}
|
||||
|
||||
private void initAdapter() throws Exception {
|
||||
Map<String, CustomBucket> newMap = new HashMap<>();
|
||||
List<InflowTargetVO> inflowAdapterVoList = inflowControlDAO.getInflowAdapterList();
|
||||
|
||||
for (InflowTargetVO inflowVo : inflowAdapterVoList) {
|
||||
if (InflowControlManager.isInflowTarget(inflowVo)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowVo);
|
||||
if (bucket != null) {
|
||||
newMap.put(inflowVo.getName(), bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.adapterBucketList = newMap;
|
||||
}
|
||||
|
||||
private void initInterface() throws Exception {
|
||||
Map<String, CustomBucket> newMap = new HashMap<>();
|
||||
List<InflowTargetVO> inflowInterfaceVoList = inflowControlDAO.getInflowInterfaceList();
|
||||
|
||||
for (InflowTargetVO inflowVo : inflowInterfaceVoList) {
|
||||
if (InflowControlManager.isInflowTarget(inflowVo)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowVo);
|
||||
if (bucket != null) {
|
||||
newMap.put(inflowVo.getName(), bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.interfaceBucketList = newMap;
|
||||
}
|
||||
|
||||
private void initGroup() throws Exception {
|
||||
Map<String, CustomGroupBucket> newGroupBucketList = new HashMap<>();
|
||||
Map<String, String> newInterfaceToGroupMap = new HashMap<>();
|
||||
Map<String, InflowGroupVO> newGroupVoMap = new HashMap<>();
|
||||
|
||||
List<InflowGroupVO> inflowGroupVoList = inflowControlDAO.getInflowGroupList();
|
||||
|
||||
for (InflowGroupVO groupVo : inflowGroupVoList) {
|
||||
if (isInflowGroupTarget(groupVo)) {
|
||||
CustomGroupBucket bucket = makeGroupBucket(groupVo);
|
||||
if (bucket != null) {
|
||||
newGroupBucketList.put(groupVo.getGroupId(), bucket);
|
||||
newGroupVoMap.put(groupVo.getGroupId(), groupVo);
|
||||
// 인터페이스 → 그룹 매핑 등록
|
||||
for (String interfaceId : groupVo.getInterfaceList()) {
|
||||
newInterfaceToGroupMap.put(interfaceId, groupVo.getGroupId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("InflowControlManager] initGroup completed. groups=" + newGroupBucketList.size()
|
||||
+ ", mappings=" + newInterfaceToGroupMap.size());
|
||||
}
|
||||
|
||||
this.groupBucketList = newGroupBucketList;
|
||||
this.interfaceToGroupMap = newInterfaceToGroupMap;
|
||||
this.groupVoMap = newGroupVoMap;
|
||||
}
|
||||
|
||||
public synchronized void reloadAdapter() throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all Started ...");
|
||||
}
|
||||
initAdapter();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadInterface() throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all Started ...");
|
||||
}
|
||||
initInterface();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload all finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadAdapter(String adapter) throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload Started ...");
|
||||
}
|
||||
Map<String, InflowTargetVO> map = inflowControlDAO.getInflowTargetByAdater(adapter);
|
||||
if (map != null) {
|
||||
Iterator<String> it = map.keySet().iterator();
|
||||
String key = "";
|
||||
while (it.hasNext()) {
|
||||
key = it.next();
|
||||
|
||||
InflowTargetVO inflowTarget = map.get(key);
|
||||
if (InflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget);
|
||||
if (bucket != null) {
|
||||
adapterBucketList.put(inflowTarget.getName(), bucket);
|
||||
} else {
|
||||
adapterBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
} else {
|
||||
adapterBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadInterface(String inter) throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload Started ...");
|
||||
}
|
||||
Map<String, InflowTargetVO> map = inflowControlDAO.getInflowTargetByInterface(inter);
|
||||
if (map != null) {
|
||||
Iterator<String> it = map.keySet().iterator();
|
||||
String key = "";
|
||||
while (it.hasNext()) {
|
||||
key = it.next();
|
||||
|
||||
InflowTargetVO inflowTarget = map.get(key);
|
||||
if (InflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget);
|
||||
if (bucket != null) {
|
||||
interfaceBucketList.put(inflowTarget.getName(), bucket);
|
||||
} else {
|
||||
interfaceBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
} else {
|
||||
interfaceBucketList.remove(inflowTarget.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reload finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadGroup() throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup all Started ...");
|
||||
}
|
||||
initGroup();
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup all finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reloadGroup(String groupId) throws Exception {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup Started ... groupId=" + groupId);
|
||||
}
|
||||
Map<String, InflowGroupVO> map = inflowControlDAO.getInflowTargetByGroup(groupId);
|
||||
if (map != null) {
|
||||
// 기존 그룹에 속한 인터페이스 매핑 제거
|
||||
Iterator<Map.Entry<String, String>> mappingIt = interfaceToGroupMap.entrySet().iterator();
|
||||
while (mappingIt.hasNext()) {
|
||||
Map.Entry<String, String> entry = mappingIt.next();
|
||||
if (groupId.equals(entry.getValue())) {
|
||||
mappingIt.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Iterator<String> it = map.keySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
InflowGroupVO groupVo = map.get(key);
|
||||
|
||||
if (isInflowGroupTarget(groupVo)) {
|
||||
CustomGroupBucket bucket = makeGroupBucket(groupVo);
|
||||
if (bucket != null) {
|
||||
groupBucketList.put(groupVo.getGroupId(), bucket);
|
||||
groupVoMap.put(groupVo.getGroupId(), groupVo);
|
||||
// 인터페이스 → 그룹 매핑 재등록
|
||||
for (String interfaceId : groupVo.getInterfaceList()) {
|
||||
interfaceToGroupMap.put(interfaceId, groupVo.getGroupId());
|
||||
}
|
||||
} else {
|
||||
groupBucketList.remove(groupVo.getGroupId());
|
||||
groupVoMap.remove(groupVo.getGroupId());
|
||||
}
|
||||
} else {
|
||||
groupBucketList.remove(groupVo.getGroupId());
|
||||
groupVoMap.remove(groupVo.getGroupId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("InflowControlManager] reloadGroup finished ...");
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() throws LifecycleException {
|
||||
// Validate and update our current component state
|
||||
if (!started)
|
||||
throw new LifecycleException("RECEAICMM203");
|
||||
|
||||
// Notify our interested LifecycleListeners
|
||||
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
|
||||
|
||||
adapterBucketList = new ConcurrentHashMap<>();
|
||||
interfaceBucketList = new ConcurrentHashMap<>();
|
||||
groupBucketList = new ConcurrentHashMap<>();
|
||||
interfaceToGroupMap = new ConcurrentHashMap<>();
|
||||
groupVoMap = new ConcurrentHashMap<>();
|
||||
|
||||
started = false;
|
||||
// Notify our interested LifecycleListeners
|
||||
lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
|
||||
}
|
||||
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return this.started;
|
||||
}
|
||||
|
||||
public String[] getAdapterAllKeys() {
|
||||
Iterator<String> it = this.adapterBucketList.keySet().iterator();
|
||||
String[] svcCd = new String[this.adapterBucketList.size()];
|
||||
for (int i = 0; it.hasNext(); i++) {
|
||||
svcCd[i] = it.next();
|
||||
}
|
||||
Arrays.sort(svcCd);
|
||||
return svcCd;
|
||||
}
|
||||
|
||||
public String[] getInterfaceAllKeys() {
|
||||
Iterator<String> it = this.interfaceBucketList.keySet().iterator();
|
||||
String[] svcCd = new String[this.interfaceBucketList.size()];
|
||||
for (int i = 0; it.hasNext(); i++) {
|
||||
svcCd[i] = it.next();
|
||||
}
|
||||
Arrays.sort(svcCd);
|
||||
return svcCd;
|
||||
}
|
||||
|
||||
public synchronized void removeAdapter(String adapter) {
|
||||
adapterBucketList.remove(adapter);
|
||||
}
|
||||
|
||||
public synchronized void removeInterface(String inter) {
|
||||
interfaceBucketList.remove(inter);
|
||||
}
|
||||
|
||||
public synchronized void removeGroup(String groupId) {
|
||||
groupBucketList.remove(groupId);
|
||||
groupVoMap.remove(groupId);
|
||||
// 해당 그룹에 속한 인터페이스 매핑도 제거
|
||||
Iterator<Map.Entry<String, String>> it = interfaceToGroupMap.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, String> entry = it.next();
|
||||
if (groupId.equals(entry.getValue())) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getGroupAllKeys() {
|
||||
Iterator<String> it = this.groupBucketList.keySet().iterator();
|
||||
String[] groupIds = new String[this.groupBucketList.size()];
|
||||
for (int i = 0; it.hasNext(); i++) {
|
||||
groupIds[i] = it.next();
|
||||
}
|
||||
Arrays.sort(groupIds);
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
public CustomGroupBucket getGroupBucket(String groupId) {
|
||||
return groupBucketList.get(groupId);
|
||||
}
|
||||
|
||||
private CustomBucket makeBucketUsingInflowVO(InflowTargetVO inflowTarget) {
|
||||
if (InflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
LocalBucketBuilder builder = Bucket4j.builder();
|
||||
if (inflowTarget.getThresholdPerSecond() > 0) {
|
||||
builder.addLimit(Bandwidth.simple(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1)));
|
||||
}
|
||||
|
||||
if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) {
|
||||
builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(),
|
||||
InflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit())));
|
||||
}
|
||||
|
||||
return new CustomBucket(builder.build(), inflowTarget);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹용 버킷 생성 - 초당/추가 임계치를 별도 버킷으로 분리
|
||||
*/
|
||||
private CustomGroupBucket makeGroupBucket(InflowGroupVO groupVo) {
|
||||
if (!isInflowGroupTarget(groupVo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LocalBucket perSecondBucket = null;
|
||||
LocalBucket thresholdBucket = null;
|
||||
|
||||
// 초당 임계치 버킷 (별도)
|
||||
if (groupVo.getThresholdPerSecond() > 0) {
|
||||
perSecondBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1)))
|
||||
.build();
|
||||
}
|
||||
|
||||
// 추가 임계치 버킷 (별도)
|
||||
if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) {
|
||||
thresholdBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(groupVo.getThreshold(),
|
||||
InflowControlManager.getPeriod(groupVo.getThresholdTimeUnit())))
|
||||
.build();
|
||||
}
|
||||
|
||||
return new CustomGroupBucket(perSecondBucket, thresholdBucket, groupVo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAdapterPass(String adapter) {
|
||||
CustomBucket b = adapterBucketList.get(adapter);
|
||||
|
||||
if (b == null)
|
||||
return true;
|
||||
|
||||
return b.getLocalBucket().tryConsume(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInterfacePass(String inter) {
|
||||
CustomBucket b = interfaceBucketList.get(inter);
|
||||
|
||||
if (b == null)
|
||||
return true;
|
||||
|
||||
return b.getLocalBucket().tryConsume(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getAdapterInflowThreashold(String adapter) {
|
||||
CustomBucket b = adapterBucketList.get(adapter);
|
||||
if (b == null)
|
||||
return null;
|
||||
return b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getInterfaceInflowThreashold(String inter) {
|
||||
CustomBucket b = interfaceBucketList.get(inter);
|
||||
if (b == null)
|
||||
return null;
|
||||
return b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getInflowThreashold(String inter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public InflowTargetVO getAdapterInflow(String adapter) {
|
||||
return getAdapterInflowThreashold(adapter);
|
||||
}
|
||||
|
||||
public InflowTargetVO getInterfaceInflow(String inter) {
|
||||
return getInterfaceInflowThreashold(inter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String isGroupPass(String groupId) {
|
||||
CustomGroupBucket b = groupBucketList.get(groupId);
|
||||
|
||||
if (b == null)
|
||||
return null;
|
||||
|
||||
return b.tryConsume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowGroupVO getGroupInflowThreshold(String groupId) {
|
||||
return groupVoMap.get(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupIdByInterface(String interfaceId) {
|
||||
return interfaceToGroupMap.get(interfaceId);
|
||||
}
|
||||
|
||||
public InflowGroupVO getGroupInflow(String groupId) {
|
||||
return getGroupInflowThreshold(groupId);
|
||||
}
|
||||
|
||||
public static boolean isInflowTarget(InflowTargetVO inflowTarget) {
|
||||
if (inflowTarget == null || !inflowTarget.isActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return inflowTarget.getThresholdPerSecond() > 0 || (inflowTarget.getThreshold() > 0 && StringUtils
|
||||
.equalsAnyIgnoreCase(inflowTarget.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND,
|
||||
QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH));
|
||||
|
||||
}
|
||||
|
||||
public static boolean isInflowGroupTarget(InflowGroupVO groupVo) {
|
||||
if (groupVo == null || !groupVo.isActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return groupVo.getThresholdPerSecond() > 0 || (groupVo.getThreshold() > 0 && StringUtils
|
||||
.equalsAnyIgnoreCase(groupVo.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND,
|
||||
QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH));
|
||||
}
|
||||
|
||||
public static Duration getPeriod(String timeUnit) {
|
||||
if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_SECOND)) {
|
||||
return Duration.ofSeconds(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MINUTE)) {
|
||||
return Duration.ofMinutes(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_HOUR)) {
|
||||
return Duration.ofHours(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_DAY)) {
|
||||
return Duration.ofDays(1);
|
||||
} else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MONTH)) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
return Duration.ofDays(calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.message.service.InterfaceMapper;
|
||||
import com.ext.eai.common.stdmessage.STDMessageKeys;
|
||||
|
||||
public class InflowControlUtil {
|
||||
private InflowControlUtil() {
|
||||
@@ -16,28 +19,70 @@ public class InflowControlUtil {
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static volatile AbstractInflowControlManager cachedInflowControlManager;
|
||||
private static volatile Bucket cachedBucket;
|
||||
|
||||
/**
|
||||
* INFLOW.properties의 inflow.control.bucket.className 설정에 따라
|
||||
* 활성화된 InflowControlManager 구현체를 반환한다.
|
||||
*
|
||||
* getBean(InflowControlManager.class) 대신 설정된 구체 타입으로 조회하므로
|
||||
* InflowControlManager와 DualInflowControlManager가 모두 Bean으로 등록된
|
||||
* 환경에서도 NoUniqueBeanDefinitionException 없이 올바른 인스턴스를 반환한다.
|
||||
*
|
||||
* 최초 1회만 조회하고 이후에는 캐시된 인스턴스를 반환한다.
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public static Bucket getBucket() {
|
||||
Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW");
|
||||
String className = inFlowProp.getProperty("inflow.control.bucket.className");
|
||||
if (StringUtils.isBlank(className)
|
||||
|| StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) {
|
||||
return InflowControlManager.getBucket();
|
||||
} else {
|
||||
try {
|
||||
Class clazz = Class.forName(className);
|
||||
Method method = clazz.getMethod("getBucket", (Class<?>[]) null);
|
||||
return (Bucket) method.invoke(null, (Object[]) null);
|
||||
} catch (Exception e) {
|
||||
logger.error("Method call error class= {}, method= getBucket", className);
|
||||
logger.error(e.getMessage());
|
||||
public static AbstractInflowControlManager getInflowControlManager() {
|
||||
if (cachedInflowControlManager == null) {
|
||||
synchronized (InflowControlUtil.class) {
|
||||
if (cachedInflowControlManager == null) {
|
||||
Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW");
|
||||
String className = inFlowProp.getProperty("inflow.control.bucket.className");
|
||||
if (StringUtils.isBlank(className)
|
||||
|| StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) {
|
||||
cachedInflowControlManager = InflowControlManager.getInstance();
|
||||
} else {
|
||||
try {
|
||||
Class clazz = Class.forName(className);
|
||||
cachedInflowControlManager = (AbstractInflowControlManager) ApplicationContextProvider.getContext().getBean(clazz);
|
||||
} catch (Exception e) {
|
||||
logger.error("getInflowControlManager error class= {}", className);
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return cachedInflowControlManager;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public static Bucket getBucket() {
|
||||
if (cachedBucket == null) {
|
||||
synchronized (InflowControlUtil.class) {
|
||||
if (cachedBucket == null) {
|
||||
Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW");
|
||||
String className = inFlowProp.getProperty("inflow.control.bucket.className");
|
||||
if (StringUtils.isBlank(className)
|
||||
|| StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) {
|
||||
cachedBucket = InflowControlManager.getBucket();
|
||||
} else {
|
||||
try {
|
||||
Class clazz = Class.forName(className);
|
||||
Method method = clazz.getMethod("getBucket", (Class<?>[]) null);
|
||||
cachedBucket = (Bucket) method.invoke(null, (Object[]) null);
|
||||
} catch (Exception e) {
|
||||
logger.error("Method call error class= {}, method= getBucket", className);
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedBucket;
|
||||
}
|
||||
|
||||
public static boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) {
|
||||
Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW");
|
||||
String controlInflow = inFlowProp.getProperty("inflow.control.yn", "N");
|
||||
@@ -45,21 +90,18 @@ public class InflowControlUtil {
|
||||
if (!"Y".equalsIgnoreCase(controlInflow)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
InterfaceMapper mapper = eaiMessage.getMapper();
|
||||
String returnType = mapper.getSendRecvDivision(eaiMessage.getStandardMessage()); // S|R
|
||||
|
||||
String className = inFlowProp.getProperty("inflow.control.bucket.className");
|
||||
if (StringUtils.isBlank(className)
|
||||
|| StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) {
|
||||
return InflowControlManager.isTargetOfInflowControl(eaiServerManager, eaiMessage).booleanValue();
|
||||
} else {
|
||||
try {
|
||||
Class clazz = Class.forName(className);
|
||||
Method method = clazz.getMethod("isTargetOfInflowControl", EAIServerManager.class, EAIMessage.class);
|
||||
Boolean returnBoolean = (Boolean) method.invoke(null, eaiServerManager, eaiMessage);
|
||||
return returnBoolean.booleanValue();
|
||||
} catch (Exception e) {
|
||||
logger.error("Method call error class= {}, method= isTargetOfInflowControl", className);
|
||||
logger.error(e.getMessage());
|
||||
if (STDMessageKeys.SEND_RECV_CD_SEND.equals(returnType)) {
|
||||
return Boolean.valueOf(true);
|
||||
}
|
||||
|
||||
return Boolean.valueOf(false);
|
||||
} catch (Exception e) {
|
||||
logger.error("isTargetOfInflowControl call error", e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
/**
|
||||
* DualBucket에 클라이언트 유량제어 메서드를 추가한 인터페이스.
|
||||
* {@link ClientDualInflowControlManager}가 구현한다.
|
||||
*/
|
||||
public interface ClientDualBucket extends DualBucket {
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 체크 — 차단 원인 포함.
|
||||
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
|
||||
*/
|
||||
String isClientPassDetail(String clientId);
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용.
|
||||
* @return 등록된 설정이 없으면 null
|
||||
*/
|
||||
InflowTargetVO getClientInflowThreshold(String clientId);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.InflowType;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* 어댑터/인터페이스 이중 버킷(DualInflowControlManager)에 클라이언트 유량제어를 추가한 관리자.
|
||||
*
|
||||
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
|
||||
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
|
||||
*/
|
||||
@Component
|
||||
public class ClientDualInflowControlManager extends DualInflowControlManager implements ClientDualBucket {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
public static synchronized Bucket getBucket() {
|
||||
return ApplicationContextProvider.getContext().getBean(ClientDualInflowControlManager.class);
|
||||
}
|
||||
|
||||
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> clientMetricsMap = new ConcurrentHashMap<>();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void start() throws LifecycleException {
|
||||
super.start();
|
||||
try {
|
||||
initClients();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException("ClientDualInflowControlManager init failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 초기화 / 리로드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void initClients() throws Exception {
|
||||
this.clientBucketMap = loadBucketMap(InflowType.CLIENT);
|
||||
if (logger.isInfo()) logger.info("ClientDualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets");
|
||||
}
|
||||
|
||||
public synchronized void reloadClient() throws Exception {
|
||||
initClients();
|
||||
clientMetricsMap.clear();
|
||||
}
|
||||
|
||||
public synchronized void reloadClient(String clientId) throws Exception {
|
||||
reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId);
|
||||
TargetMetrics m = clientMetricsMap.get(clientId);
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
public synchronized void removeClient(String clientId) {
|
||||
clientBucketMap.remove(clientId);
|
||||
clientMetricsMap.remove(clientId);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 클라이언트 메트릭 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public TargetMetrics getClientMetrics(String clientId) { return clientMetricsMap.get(clientId); }
|
||||
public Map<String, TargetMetrics> getAllClientMetrics() { return Collections.unmodifiableMap(clientMetricsMap); }
|
||||
public void resetClientMetrics(String clientId) { TargetMetrics m = clientMetricsMap.get(clientId); if (m != null) m.reset(); }
|
||||
public void resetAllClientMetrics() { clientMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ClientDualBucket — 클라이언트 유량제어
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public String isClientPassDetail(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
if (b == null) return DualCustomBucket.RESULT_PASS;
|
||||
String result = b.tryConsume();
|
||||
recordMetrics(clientMetricsMap, clientId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getClientInflowThreshold(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
return (b == null) ? null : b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 모니터링용 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public DualCustomBucket getClientBucket(String clientId) {
|
||||
return clientBucketMap.get(clientId);
|
||||
}
|
||||
|
||||
public Map<String, DualCustomBucket> getClientBucketMap() {
|
||||
return clientBucketMap;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
/**
|
||||
* Bucket 인터페이스 확장.
|
||||
* 기존 isAdapterPass/isInterfacePass(boolean)에 더해
|
||||
* 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지 반환하는 메서드 추가.
|
||||
* 클라이언트별 유량제어 메서드도 포함.
|
||||
*
|
||||
* <p>클라이언트 유량제어는 {@link ClientDualBucket}으로 분리됨.
|
||||
*/
|
||||
public interface DualBucket extends Bucket {
|
||||
|
||||
@@ -22,16 +22,4 @@ public interface DualBucket extends Bucket {
|
||||
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
|
||||
*/
|
||||
String isInterfacePassDetail(String inter);
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 체크 — 차단 원인 포함.
|
||||
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
|
||||
*/
|
||||
String isClientPassDetail(String clientId);
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용.
|
||||
* @return 등록된 설정이 없으면 null
|
||||
*/
|
||||
InflowTargetVO getClientInflowThreshold(String clientId);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,38 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.CustomGroupBucket;
|
||||
import com.eactive.eai.common.inflow.InflowControlDAO;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.InflowType;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.Refill;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
/**
|
||||
* 어댑터/인터페이스/클라이언트 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자.
|
||||
* 어댑터/인터페이스 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자.
|
||||
*
|
||||
* <p>기존 InflowControlManager는 어댑터/인터페이스에 단일 LocalBucket(복수 Bandwidth)을 사용하여
|
||||
* 차단 시 어느 한도(초당/기간)를 초과했는지 알 수 없었다.
|
||||
* 이 클래스는 그룹 버킷(CustomGroupBucket)과 동일하게 두 버킷을 분리하고
|
||||
* {@link DualBucket#isAdapterPassDetail}/{@link DualBucket#isInterfacePassDetail}로
|
||||
* 차단 원인을 반환한다. 그룹 버킷 통과 여부는 {@link TargetMetrics}로 카운팅된다.
|
||||
* <p>클라이언트 유량제어는 {@link ClientDualInflowControlManager}로 분리됨.
|
||||
*
|
||||
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
|
||||
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager</pre>
|
||||
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
|
||||
*/
|
||||
@Component
|
||||
public class DualInflowControlManager extends InflowControlManager implements DualBucket {
|
||||
public class DualInflowControlManager extends AbstractInflowControlManager implements DualBucket {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
@@ -50,18 +41,12 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
return ApplicationContextProvider.getContext().getBean(DualInflowControlManager.class);
|
||||
}
|
||||
|
||||
public static Boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) {
|
||||
return InflowControlManager.isTargetOfInflowControl(eaiServerManager, eaiMessage);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private InflowControlDAO dualDao;
|
||||
|
||||
private volatile Map<String, DualCustomBucket> adapterBucketMap = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, DualCustomBucket> interfaceBucketMap = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final ConcurrentHashMap<String, TargetMetrics> groupMetricsMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> adapterMetricsMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> interfaceMetricsMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> groupMetricsMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static class TargetMetrics {
|
||||
public final AtomicLong allowed = new AtomicLong();
|
||||
@@ -87,7 +72,6 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
try {
|
||||
initAdapters();
|
||||
initInterfaces();
|
||||
initClients();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException("DualInflowControlManager init failed: " + e.getMessage());
|
||||
}
|
||||
@@ -107,14 +91,9 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
if (logger.isInfo()) logger.info("DualInflowControlManager] initInterfaces: " + interfaceBucketMap.size() + " buckets");
|
||||
}
|
||||
|
||||
private void initClients() throws Exception {
|
||||
this.clientBucketMap = loadBucketMap(InflowType.CLIENT);
|
||||
if (logger.isInfo()) logger.info("DualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets");
|
||||
}
|
||||
|
||||
private Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
|
||||
protected Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
|
||||
Map<String, DualCustomBucket> newMap = new HashMap<>();
|
||||
for (InflowTargetVO vo : dualDao.getInflowList(type)) {
|
||||
for (InflowTargetVO vo : inflowControlDAO.getInflowList(type)) {
|
||||
DualCustomBucket bucket = makeBucket(vo);
|
||||
if (bucket != null) newMap.put(vo.getName(), bucket);
|
||||
}
|
||||
@@ -123,34 +102,28 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
|
||||
@Override
|
||||
public synchronized void reloadAdapter() throws Exception {
|
||||
super.reloadAdapter();
|
||||
initAdapters();
|
||||
adapterMetricsMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reloadAdapter(String adapter) throws Exception {
|
||||
super.reloadAdapter(adapter);
|
||||
reloadBucketMap(adapterBucketMap, InflowType.ADAPTER, adapter);
|
||||
TargetMetrics m = adapterMetricsMap.get(adapter);
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reloadInterface() throws Exception {
|
||||
super.reloadInterface();
|
||||
initInterfaces();
|
||||
interfaceMetricsMap.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reloadInterface(String inter) throws Exception {
|
||||
super.reloadInterface(inter);
|
||||
reloadBucketMap(interfaceBucketMap, InflowType.INTERFACE, inter);
|
||||
}
|
||||
|
||||
public synchronized void reloadClient() throws Exception {
|
||||
initClients();
|
||||
}
|
||||
|
||||
public synchronized void reloadClient(String clientId) throws Exception {
|
||||
reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId);
|
||||
TargetMetrics m = interfaceMetricsMap.get(inter);
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,8 +139,8 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
private void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
|
||||
Map<String, InflowTargetVO> updated = dualDao.getInflowMap(type, name);
|
||||
protected void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
|
||||
Map<String, InflowTargetVO> updated = inflowControlDAO.getInflowMap(type, name);
|
||||
if (updated == null) return;
|
||||
for (InflowTargetVO vo : updated.values()) {
|
||||
DualCustomBucket bucket = makeBucket(vo);
|
||||
@@ -176,6 +149,22 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 어댑터/인터페이스 메트릭 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public TargetMetrics getAdapterMetrics(String adapter) { return adapterMetricsMap.get(adapter); }
|
||||
public TargetMetrics getInterfaceMetrics(String inter) { return interfaceMetricsMap.get(inter); }
|
||||
|
||||
public Map<String, TargetMetrics> getAllAdapterMetrics() { return Collections.unmodifiableMap(adapterMetricsMap); }
|
||||
public Map<String, TargetMetrics> getAllInterfaceMetrics() { return Collections.unmodifiableMap(interfaceMetricsMap); }
|
||||
|
||||
public void resetAdapterMetrics(String adapter) { TargetMetrics m = adapterMetricsMap.get(adapter); if (m != null) m.reset(); }
|
||||
public void resetInterfaceMetrics(String inter) { TargetMetrics m = interfaceMetricsMap.get(inter); if (m != null) m.reset(); }
|
||||
|
||||
public void resetAllAdapterMetrics() { adapterMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
public void resetAllInterfaceMetrics() { interfaceMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 그룹 메트릭 — isGroupPass() 카운팅 및 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -218,31 +207,30 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
@Override
|
||||
public String isAdapterPassDetail(String adapter) {
|
||||
DualCustomBucket b = adapterBucketMap.get(adapter);
|
||||
return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume();
|
||||
if (b == null) return DualCustomBucket.RESULT_PASS;
|
||||
String result = b.tryConsume();
|
||||
recordMetrics(adapterMetricsMap, adapter, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String isInterfacePassDetail(String inter) {
|
||||
DualCustomBucket b = interfaceBucketMap.get(inter);
|
||||
return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume();
|
||||
if (b == null) return DualCustomBucket.RESULT_PASS;
|
||||
String result = b.tryConsume();
|
||||
recordMetrics(interfaceMetricsMap, inter, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String isClientPassDetail(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getClientInflowThreshold(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
return (b == null) ? null : b.getInflowTargetVo();
|
||||
protected void recordMetrics(ConcurrentHashMap<String, TargetMetrics> metricsMap, String key, String result) {
|
||||
TargetMetrics m = metricsMap.computeIfAbsent(key, k -> new TargetMetrics());
|
||||
if (result == null) m.allowed.incrementAndGet();
|
||||
else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result)) m.rejectedPerSecond.incrementAndGet();
|
||||
else m.rejectedThreshold.incrementAndGet();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bucket 인터페이스 오버라이드 — 이중 버킷으로 교체 (boolean 반환 유지)
|
||||
// isAdapterPassDetail/isInterfacePassDetail이 토큰을 소비하므로
|
||||
// 두 메서드를 동일 요청에서 중복 호출하면 토큰이 이중 소비됨에 주의.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
@@ -255,6 +243,48 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
return isInterfacePassDetail(inter) == DualCustomBucket.RESULT_PASS;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 어댑터/인터페이스 키·VO·삭제 — Dual 맵 기준으로 오버라이드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public String[] getAdapterAllKeys() {
|
||||
String[] keys = adapterBucketMap.keySet().toArray(new String[0]);
|
||||
Arrays.sort(keys);
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getInterfaceAllKeys() {
|
||||
String[] keys = interfaceBucketMap.keySet().toArray(new String[0]);
|
||||
Arrays.sort(keys);
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getAdapterInflowThreashold(String adapter) {
|
||||
DualCustomBucket b = adapterBucketMap.get(adapter);
|
||||
return (b == null) ? null : b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getInterfaceInflowThreashold(String inter) {
|
||||
DualCustomBucket b = interfaceBucketMap.get(inter);
|
||||
return (b == null) ? null : b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removeAdapter(String adapter) {
|
||||
adapterBucketMap.remove(adapter);
|
||||
adapterMetricsMap.remove(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removeInterface(String inter) {
|
||||
interfaceBucketMap.remove(inter);
|
||||
interfaceMetricsMap.remove(inter);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 모니터링용 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -275,20 +305,17 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
return interfaceBucketMap;
|
||||
}
|
||||
|
||||
public DualCustomBucket getClientBucket(String clientId) {
|
||||
return clientBucketMap.get(clientId);
|
||||
}
|
||||
|
||||
public Map<String, DualCustomBucket> getClientBucketMap() {
|
||||
return clientBucketMap;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 버킷 팩토리
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private DualCustomBucket makeBucket(InflowTargetVO vo) {
|
||||
if (!InflowControlManager.isInflowTarget(vo)) {
|
||||
/**
|
||||
* perSecond 버킷: greedy refill — 초당 N개 요청을 균등하게 허용.
|
||||
* threshold 버킷: intervally refill — 기간(시/일/월 등) 단위 쿼터를 기간 시작 시 일괄 충전.
|
||||
* Bandwidth.simple은 두 버킷 모두 greedy로 생성되어 시간 단위 구분이 없어지므로 classic으로 교체.
|
||||
*/
|
||||
protected DualCustomBucket makeBucket(InflowTargetVO vo) {
|
||||
if (!AbstractInflowControlManager.isInflowTarget(vo)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -297,14 +324,16 @@ public class DualInflowControlManager extends InflowControlManager implements Du
|
||||
|
||||
if (vo.getThresholdPerSecond() > 0) {
|
||||
perSecondBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(vo.getThresholdPerSecond(), Duration.ofSeconds(1)))
|
||||
.addLimit(Bandwidth.classic(vo.getThresholdPerSecond(),
|
||||
Refill.greedy(vo.getThresholdPerSecond(), Duration.ofSeconds(1))))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (vo.getThreshold() > 0 && StringUtils.isNotBlank(vo.getThresholdTimeUnit())) {
|
||||
Duration period = AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit());
|
||||
thresholdBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(vo.getThreshold(),
|
||||
InflowControlManager.getPeriod(vo.getThresholdTimeUnit())))
|
||||
.addLimit(Bandwidth.classic(vo.getThreshold(),
|
||||
Refill.intervally(vo.getThreshold(), period)))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Key;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.jcajce.spec.FPEParameterSpec;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
public class AESCryptoModuleExtension implements CryptoModuleExtension {
|
||||
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
private static final String DEFAULT_PROVIDER = "BC";
|
||||
|
||||
private Cipher encryptEngine;
|
||||
private Cipher decryptEngine;
|
||||
private String mode;
|
||||
private String pad;
|
||||
private byte[] iv;
|
||||
private byte[] encKey;
|
||||
private byte[] decKey;
|
||||
/** 명시적 프로바이더. null이면 모든 모드에서 BC(DEFAULT_PROVIDER)를 사용한다. */
|
||||
private String provider;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// init — 모든 오버로드는 최종적으로 6+provider 버전에 위임한다
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey) throws Exception {
|
||||
init(alg, mode, pad, null, encKey, decKey, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey) throws Exception {
|
||||
init(alg, mode, pad, iv, encKey, decKey, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
init(alg, mode, pad, null, encKey, decKey, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
this.mode = mode;
|
||||
this.pad = pad;
|
||||
this.iv = iv;
|
||||
this.encKey = encKey;
|
||||
this.decKey = decKey;
|
||||
this.provider = (provider != null && !provider.isEmpty()) ? provider : null;
|
||||
|
||||
// GCM 포함 모든 모드에서 BC 프로바이더를 사용 — 기본값 BC
|
||||
if (Security.getProvider("BC") == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
if (!mode.equalsIgnoreCase("GCM")) {
|
||||
String m = mode != null ? mode : "";
|
||||
String p = pad != null ? pad : "";
|
||||
String transformation = String.format("AES/%s/%s", m, p);
|
||||
|
||||
Key encKeySpec = new SecretKeySpec(encKey, "AES");
|
||||
Key decKeySpec = new SecretKeySpec(decKey, "AES");
|
||||
AlgorithmParameterSpec param = iv != null ? new IvParameterSpec(iv) : null;
|
||||
if (m.toLowerCase().contains("ff")) {
|
||||
param = new FPEParameterSpec(256, iv);
|
||||
}
|
||||
|
||||
String prov = this.provider != null ? this.provider : DEFAULT_PROVIDER;
|
||||
this.encryptEngine = Cipher.getInstance(transformation, prov);
|
||||
this.decryptEngine = Cipher.getInstance(transformation, prov);
|
||||
this.encryptEngine.init(Cipher.ENCRYPT_MODE, encKeySpec, param, (SecureRandom) null);
|
||||
this.decryptEngine.init(Cipher.DECRYPT_MODE, decKeySpec, param, (SecureRandom) null);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// encrypt
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int encrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.encryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
return encrypt(src, sindex, slength, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AAD를 명시한 암호화.
|
||||
* GCM 모드에서 aad != null 이면 해당 값을 사용하고, null 이면 iv를 AAD로 사용하는 기본 동작을 따른다.
|
||||
* 비-GCM 모드는 AAD를 무시하고 사전 초기화된 엔진으로 처리한다.
|
||||
*/
|
||||
@Override
|
||||
public byte[] encrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
if (this.mode.equalsIgnoreCase("GCM")) {
|
||||
Key key = new SecretKeySpec(this.encKey, "AES");
|
||||
byte[] nonce = new byte[GCM_IV_LENGTH];
|
||||
new SecureRandom().nextBytes(nonce);
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce);
|
||||
|
||||
String prov = this.provider != null ? this.provider : DEFAULT_PROVIDER;
|
||||
Cipher cipher = Cipher.getInstance(String.format("AES/%s/%s", this.mode, this.pad), prov);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);
|
||||
|
||||
byte[] effectiveAad = aad != null ? aad : this.iv;
|
||||
if (effectiveAad != null) cipher.updateAAD(effectiveAad);
|
||||
|
||||
byte[] cipherTextWithTag = cipher.doFinal(src, sindex, slength);
|
||||
byte[] output = new byte[GCM_IV_LENGTH + cipherTextWithTag.length];
|
||||
System.arraycopy(nonce, 0, output, 0, GCM_IV_LENGTH);
|
||||
System.arraycopy(cipherTextWithTag, 0, output, GCM_IV_LENGTH, cipherTextWithTag.length);
|
||||
return output;
|
||||
}
|
||||
|
||||
int outLen = this.encryptEngine.getOutputSize(slength);
|
||||
byte[] dst = new byte[outLen];
|
||||
int actual = encrypt(src, sindex, slength, dst, 0);
|
||||
return ByteBuffer.allocate(actual).put(dst, 0, actual).array();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// decrypt
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int decrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.decryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
return decrypt(src, sindex, slength, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AAD를 명시한 복호화.
|
||||
* GCM 모드에서 aad != null 이면 해당 값을 사용하고, null 이면 iv를 AAD로 사용하는 기본 동작을 따른다.
|
||||
* 비-GCM 모드는 AAD를 무시하고 사전 초기화된 엔진으로 처리한다.
|
||||
*/
|
||||
@Override
|
||||
public byte[] decrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
if (this.mode.equalsIgnoreCase("GCM")) {
|
||||
Key key = new SecretKeySpec(this.decKey, "AES");
|
||||
byte[] nonce = new byte[GCM_IV_LENGTH];
|
||||
System.arraycopy(src, sindex, nonce, 0, GCM_IV_LENGTH);
|
||||
byte[] cipherTextWithTag = new byte[slength - GCM_IV_LENGTH];
|
||||
System.arraycopy(src, sindex + GCM_IV_LENGTH, cipherTextWithTag, 0, cipherTextWithTag.length);
|
||||
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce);
|
||||
String prov = this.provider != null ? this.provider : DEFAULT_PROVIDER;
|
||||
Cipher cipher = Cipher.getInstance(String.format("AES/%s/%s", this.mode, this.pad), prov);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);
|
||||
|
||||
byte[] effectiveAad = aad != null ? aad : this.iv;
|
||||
if (effectiveAad != null) cipher.updateAAD(effectiveAad);
|
||||
|
||||
return cipher.doFinal(cipherTextWithTag);
|
||||
}
|
||||
return this.decryptEngine.doFinal(src, sindex, slength);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Key;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.jcajce.spec.FPEParameterSpec;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
public class ARIACryptoModuleExtension implements CryptoModuleExtension {
|
||||
|
||||
private Cipher encryptEngine;
|
||||
private Cipher decryptEngine;
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey) throws Exception {
|
||||
this.init("ARIA", mode, pad, (byte[]) null, encKey, decKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey) throws Exception {
|
||||
if (Security.getProvider("BC") == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
String m = mode != null ? mode : "";
|
||||
String p = pad != null ? pad : "";
|
||||
Key encKeySpec = new SecretKeySpec(encKey, "ARIA");
|
||||
Key decKeySpec = new SecretKeySpec(decKey, "ARIA");
|
||||
AlgorithmParameterSpec param = iv != null ? new IvParameterSpec(iv) : null;
|
||||
if (m.toLowerCase().contains("ff")) {
|
||||
param = new FPEParameterSpec(256, iv);
|
||||
}
|
||||
this.encryptEngine = Cipher.getInstance(String.format("ARIA/%s/%s", m, p));
|
||||
this.encryptEngine.init(Cipher.ENCRYPT_MODE, encKeySpec, param, (SecureRandom) null);
|
||||
this.decryptEngine = Cipher.getInstance(String.format("ARIA/%s/%s", m, p));
|
||||
this.decryptEngine.init(Cipher.DECRYPT_MODE, decKeySpec, param, (SecureRandom) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int encrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.encryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
int size = (slength + 17) / this.encryptEngine.getBlockSize() * this.encryptEngine.getBlockSize();
|
||||
size += (slength + 17) % this.encryptEngine.getBlockSize() == 0 ? 0 : this.encryptEngine.getBlockSize();
|
||||
byte[] dst = new byte[this.encryptEngine.getOutputSize(size)];
|
||||
size = this.encrypt(src, sindex, slength, dst, 0);
|
||||
return ByteBuffer.allocate(size).put(dst, 0, size).array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.decryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
return this.decryptEngine.doFinal(src, sindex, slength);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CryptoModuleConfigVO {
|
||||
|
||||
String cryptoId;
|
||||
String cryptoName;
|
||||
String cryptoDesc;
|
||||
String algType;
|
||||
String cipherMode;
|
||||
String padding;
|
||||
String ivHex;
|
||||
String keySourceType;
|
||||
String encKeyHex;
|
||||
String decKeyHex;
|
||||
String keyDerivStrategy;
|
||||
String keyDerivParams;
|
||||
String cacheYn;
|
||||
Integer cacheTtlSec;
|
||||
String useYn;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
public interface CryptoModuleExtension {
|
||||
|
||||
void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey) throws Exception;
|
||||
|
||||
void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey) throws Exception;
|
||||
|
||||
/** 프로바이더를 명시한 초기화. 미지원 구현체는 provider를 무시하고 기본 동작한다. */
|
||||
default void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
init(alg, mode, pad, encKey, decKey);
|
||||
}
|
||||
|
||||
/** 프로바이더를 명시한 초기화 (IV 포함). 미지원 구현체는 provider를 무시하고 기본 동작한다. */
|
||||
default void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
init(alg, mode, pad, iv, encKey, decKey);
|
||||
}
|
||||
|
||||
int encrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception;
|
||||
|
||||
byte[] encrypt(byte[] src, int sindex, int slength) throws Exception;
|
||||
|
||||
/** AAD를 명시한 암호화. null이면 구현체 기본 동작(IV를 AAD로 사용하거나 AAD 없음)을 따른다. */
|
||||
default byte[] encrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
return encrypt(src, sindex, slength);
|
||||
}
|
||||
|
||||
default byte[] encrypt(byte[] src) throws Exception {
|
||||
return encrypt(src, 0, src.length);
|
||||
}
|
||||
|
||||
default byte[] encrypt(byte[] src, byte[] aad) throws Exception {
|
||||
return encrypt(src, 0, src.length, aad);
|
||||
}
|
||||
|
||||
int decrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception;
|
||||
|
||||
byte[] decrypt(byte[] src, int sindex, int slength) throws Exception;
|
||||
|
||||
/** AAD를 명시한 복호화. null이면 구현체 기본 동작을 따른다. */
|
||||
default byte[] decrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
return decrypt(src, sindex, slength);
|
||||
}
|
||||
|
||||
default byte[] decrypt(byte[] src) throws Exception {
|
||||
return decrypt(src, 0, src.length);
|
||||
}
|
||||
|
||||
default byte[] decrypt(byte[] src, byte[] aad) throws Exception {
|
||||
return decrypt(src, 0, src.length, aad);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
|
||||
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@Component
|
||||
public class CryptoModuleManager implements Lifecycle {
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final ConcurrentHashMap<String, CryptoModuleConfigVO> configMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, KeyDerivationStrategy> strategyCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, CachedDerivedKey> dynamicKeyCache = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean started;
|
||||
private final LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
private CryptoModuleManager() {}
|
||||
|
||||
public static CryptoModuleManager getInstance() {
|
||||
return ApplicationContextProvider.getContext().getBean(CryptoModuleManager.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws LifecycleException {
|
||||
if (started) throw new LifecycleException("RECEAICRY001");
|
||||
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||
try {
|
||||
loadAll();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException("RECEAICRY002");
|
||||
}
|
||||
started = true;
|
||||
lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws LifecycleException {
|
||||
if (!started) throw new LifecycleException("RECEAICRY003");
|
||||
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
|
||||
configMap.clear();
|
||||
dynamicKeyCache.clear();
|
||||
strategyCache.clear();
|
||||
started = false;
|
||||
lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
|
||||
}
|
||||
|
||||
private void loadAll() {
|
||||
CryptoModuleConfigLoader loader = ctx().getBean(CryptoModuleConfigLoader.class);
|
||||
CryptoModuleConfigMapper mapper = ctx().getBean(CryptoModuleConfigMapper.class);
|
||||
|
||||
List<CryptoModuleConfig> all = loader.findAll();
|
||||
configMap.clear();
|
||||
for (CryptoModuleConfig entity : all) {
|
||||
if ("Y".equalsIgnoreCase(entity.getUseYn())) {
|
||||
CryptoModuleConfigVO vo = mapper.toVo(entity);
|
||||
configMap.put(vo.getCryptoName(), vo);
|
||||
}
|
||||
}
|
||||
logger.warn("CryptoModuleManager] 암호화모듈 로드 완료. 건수=" + configMap.size());
|
||||
}
|
||||
|
||||
public void reload(String cryptoId) {
|
||||
CryptoModuleConfigLoader loader = ctx().getBean(CryptoModuleConfigLoader.class);
|
||||
CryptoModuleConfigMapper mapper = ctx().getBean(CryptoModuleConfigMapper.class);
|
||||
|
||||
configMap.values().removeIf(vo -> cryptoId.equals(vo.getCryptoId()));
|
||||
evictDynamicCache(cryptoId);
|
||||
|
||||
loader.findById(cryptoId).ifPresent(entity -> {
|
||||
if ("Y".equalsIgnoreCase(entity.getUseYn())) {
|
||||
CryptoModuleConfigVO vo = mapper.toVo(entity);
|
||||
configMap.put(vo.getCryptoName(), vo);
|
||||
logger.warn("CryptoModuleManager] 재로드 완료: " + vo.getCryptoName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* STATIC 키 방식 — 초기화된 CryptoModuleExtension 반환.
|
||||
* Cipher는 thread-safe하지 않으므로 호출마다 새 인스턴스를 생성한다.
|
||||
*/
|
||||
public CryptoModuleExtension createExtension(String cryptoName) throws Exception {
|
||||
CryptoModuleConfigVO vo = getVO(cryptoName);
|
||||
byte[] encKey = DatatypeConverter.parseHexBinary(vo.getEncKeyHex());
|
||||
byte[] decKey = vo.getDecKeyHex() != null
|
||||
? DatatypeConverter.parseHexBinary(vo.getDecKeyHex())
|
||||
: encKey;
|
||||
byte[] iv = vo.getIvHex() != null ? DatatypeConverter.parseHexBinary(vo.getIvHex()) : null;
|
||||
return buildExtension(vo, iv, encKey, decKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* DYNAMIC 키 방식 — 전략으로 키를 도출하고 캐시를 적용한 뒤 CryptoModuleExtension 반환.
|
||||
*/
|
||||
public CryptoModuleExtension createExtension(String cryptoName, Map<String, String> runtimeContext)
|
||||
throws Exception {
|
||||
CryptoModuleConfigVO vo = getVO(cryptoName);
|
||||
|
||||
if ("STATIC".equalsIgnoreCase(vo.getKeySourceType())) {
|
||||
return createExtension(cryptoName);
|
||||
}
|
||||
|
||||
KeyDerivationStrategy strategy = resolveStrategy(vo.getKeyDerivStrategy());
|
||||
Map<String, String> params = parseParams(vo.getKeyDerivParams());
|
||||
String cacheKey = strategy.buildCacheKey(cryptoName, params, runtimeContext);
|
||||
|
||||
DerivedKey derivedKey = null;
|
||||
|
||||
if ("Y".equalsIgnoreCase(vo.getCacheYn())) {
|
||||
CachedDerivedKey cached = dynamicKeyCache.get(cacheKey);
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
derivedKey = cached.getDerivedKey();
|
||||
}
|
||||
}
|
||||
|
||||
if (derivedKey == null) {
|
||||
derivedKey = strategy.deriveKey(params, runtimeContext);
|
||||
if ("Y".equalsIgnoreCase(vo.getCacheYn())) {
|
||||
int ttl = vo.getCacheTtlSec() != null ? vo.getCacheTtlSec() : 300;
|
||||
dynamicKeyCache.put(cacheKey, new CachedDerivedKey(derivedKey, ttl));
|
||||
}
|
||||
}
|
||||
|
||||
byte[] iv = vo.getIvHex() != null ? DatatypeConverter.parseHexBinary(vo.getIvHex()) : null;
|
||||
return buildExtension(vo, iv, derivedKey.getEncKey(), derivedKey.getDecKey());
|
||||
}
|
||||
|
||||
private CryptoModuleConfigVO getVO(String cryptoName) {
|
||||
CryptoModuleConfigVO vo = configMap.get(cryptoName);
|
||||
if (vo == null) {
|
||||
throw new IllegalArgumentException("암호화모듈 미등록: " + cryptoName);
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private CryptoModuleExtension buildExtension(CryptoModuleConfigVO vo, byte[] iv,
|
||||
byte[] encKey, byte[] decKey) throws Exception {
|
||||
CryptoModuleExtension ext = "ARIA".equalsIgnoreCase(vo.getAlgType())
|
||||
? new ARIACryptoModuleExtension()
|
||||
: new AESCryptoModuleExtension();
|
||||
ext.init(vo.getAlgType(), vo.getCipherMode(), vo.getPadding(), iv, encKey, decKey);
|
||||
return ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* DB key_deriv_strategy 컬럼에 저장된 FQCN으로 전략 클래스를 로드한다.
|
||||
* 한 번 로드된 인스턴스는 strategyCache에 보관하여 재사용한다.
|
||||
*/
|
||||
private KeyDerivationStrategy resolveStrategy(String fqcn) {
|
||||
return strategyCache.computeIfAbsent(fqcn, key -> {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(key);
|
||||
return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, String> parseParams(String json) throws Exception {
|
||||
if (json == null || json.trim().isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {});
|
||||
}
|
||||
|
||||
private void evictDynamicCache(String cryptoId) {
|
||||
configMap.values().stream()
|
||||
.filter(vo -> cryptoId.equals(vo.getCryptoId()))
|
||||
.map(CryptoModuleConfigVO::getCryptoName)
|
||||
.forEach(name -> {
|
||||
Iterator<String> it = dynamicKeyCache.keySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
if (it.next().startsWith(name + ":")) it.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private org.springframework.context.ApplicationContext ctx() {
|
||||
return ApplicationContextProvider.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
private static class CachedDerivedKey {
|
||||
private final DerivedKey derivedKey;
|
||||
private final long expireAt;
|
||||
|
||||
CachedDerivedKey(DerivedKey derivedKey, int ttlSec) {
|
||||
this.derivedKey = derivedKey;
|
||||
this.expireAt = System.currentTimeMillis() + (ttlSec * 1000L);
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() > expireAt;
|
||||
}
|
||||
|
||||
DerivedKey getDerivedKey() {
|
||||
return derivedKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* 어댑터 필터 등에서 암복호화를 위임하는 진입점.
|
||||
*
|
||||
* STATIC 키 방식:
|
||||
* service.encrypt("MODULE_NAME", plainBytes);
|
||||
*
|
||||
* DYNAMIC 키 방식 (런타임 컨텍스트 전달):
|
||||
* Map<String,String> ctx = Map.of("X-Api-Enc-Key", request.getHeader("X-Api-Enc-Key"));
|
||||
* service.encrypt("MODULE_NAME", ctx, plainBytes);
|
||||
*
|
||||
* AAD 명시 방식 (GCM 인증 데이터 전달):
|
||||
* byte[] aad = request.getHeader("X-Api-Request-Id").getBytes(StandardCharsets.UTF_8);
|
||||
* service.encrypt("MODULE_NAME", ctx, aad, plainBytes);
|
||||
*/
|
||||
@Service
|
||||
public class CryptoModuleService {
|
||||
|
||||
public static CryptoModuleService getInstance() {
|
||||
return ApplicationContextProvider.getContext().getBean(CryptoModuleService.class);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// STATIC 키
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public byte[] encrypt(String cryptoName, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length);
|
||||
}
|
||||
|
||||
public byte[] encrypt(String cryptoName, byte[] aad, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length, aad);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, byte[] aad, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length, aad);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// DYNAMIC 키
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public byte[] encrypt(String cryptoName, Map<String, String> runtimeContext, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length);
|
||||
}
|
||||
|
||||
public byte[] encrypt(String cryptoName, Map<String, String> runtimeContext, byte[] aad, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length, aad);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, Map<String, String> runtimeContext, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, Map<String, String> runtimeContext, byte[] aad, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length, aad);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
public class DerivedKey {
|
||||
|
||||
private final byte[] encKey;
|
||||
private final byte[] decKey;
|
||||
|
||||
public DerivedKey(byte[] encKey, byte[] decKey) {
|
||||
this.encKey = encKey;
|
||||
this.decKey = decKey != null ? decKey : encKey;
|
||||
}
|
||||
|
||||
public byte[] getEncKey() {
|
||||
return encKey;
|
||||
}
|
||||
|
||||
public byte[] getDecKey() {
|
||||
return decKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface KeyDerivationStrategy {
|
||||
|
||||
/**
|
||||
* 키를 도출한다.
|
||||
*
|
||||
* @param params DB의 key_deriv_params (JSON 파싱된 Map)
|
||||
* @param runtimeContext 런타임 컨텍스트 (HTTP 헤더, 거래 정보 등 호출자가 채워서 전달)
|
||||
*/
|
||||
DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception;
|
||||
|
||||
/**
|
||||
* 캐시 키를 생성한다. 전략마다 어떤 runtimeContext 값이 키를 결정하는지 다르다.
|
||||
*
|
||||
* @param cryptoName 암호화모듈 이름
|
||||
* @param params DB의 key_deriv_params
|
||||
* @param runtimeContext 런타임 컨텍스트
|
||||
*/
|
||||
String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.hsm.HsmException;
|
||||
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HSM 마스터키를 이용하는 전략 클래스들의 기본 클래스
|
||||
*/
|
||||
public abstract class BaseHsmKeyDerivationStrategy implements KeyDerivationStrategy {
|
||||
|
||||
protected static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
|
||||
|
||||
protected byte[] getHsmKey(Map<String, String> params) throws HsmException {
|
||||
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
|
||||
return getHsmKey(hsmKeyAlias);
|
||||
}
|
||||
|
||||
protected byte[] getHsmKey(String hsmKeyAlias) throws HsmException {
|
||||
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
|
||||
return masterKey.getEncoded();
|
||||
}
|
||||
|
||||
private String required(Map<String, String> params, String key) {
|
||||
String value = params.get(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private HsmCryptoService hsmCryptoService() {
|
||||
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HSM 마스터키 + 런타임 컨텍스트값을 연결(concatenate)한 뒤 SHA-256 해싱으로 키를 도출하는 전략.
|
||||
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
|
||||
*
|
||||
* key_deriv_params (JSON) 예시:
|
||||
* {
|
||||
* "hsmKeyAlias" : "MASTER_KEY_AES",
|
||||
* "contextKey" : "X-Api-Group-Seq" -- runtimeContext에서 꺼낼 키 이름
|
||||
* }
|
||||
*/
|
||||
public class HsmContextSha256KeyDerivationStrategy implements KeyDerivationStrategy {
|
||||
|
||||
private static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
|
||||
private static final String PARAM_CONTEXT_KEY = "contextKey";
|
||||
|
||||
@Override
|
||||
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
|
||||
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
|
||||
String contextKey = required(params, PARAM_CONTEXT_KEY);
|
||||
|
||||
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
|
||||
byte[] masterKeyBytes = masterKey.getEncoded();
|
||||
byte[] contextBytes = runtimeContext.getOrDefault(contextKey, "")
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] combined = new byte[masterKeyBytes.length + contextBytes.length];
|
||||
System.arraycopy(masterKeyBytes, 0, combined, 0, masterKeyBytes.length);
|
||||
System.arraycopy(contextBytes, 0, combined, masterKeyBytes.length, contextBytes.length);
|
||||
|
||||
byte[] derived = MessageDigest.getInstance("SHA-256").digest(combined);
|
||||
return new DerivedKey(derived, derived);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
|
||||
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
|
||||
String contextValue = runtimeContext.getOrDefault(contextKey, "");
|
||||
return cryptoName + ":" + contextValue;
|
||||
}
|
||||
|
||||
private String required(Map<String, String> params, String key) {
|
||||
String value = params.get(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private HsmCryptoService hsmCryptoService() {
|
||||
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HSM 마스터키 + 런타임 컨텍스트값 XOR 조합으로 키를 도출하는 전략.
|
||||
*
|
||||
* key_deriv_params (JSON) 예시:
|
||||
* {
|
||||
* "hsmKeyAlias" : "MASTER_KEY_AES",
|
||||
* "contextKey" : "X-Api-Enc-Key", -- runtimeContext에서 꺼낼 키 이름
|
||||
* "offset" : "0", -- contextKey 값 중 사용할 시작 위치 (byte)
|
||||
* "length" : "16" -- contextKey 값 중 사용할 길이 (byte)
|
||||
* }
|
||||
*/
|
||||
public class HsmContextXorKeyDerivationStrategy implements KeyDerivationStrategy {
|
||||
|
||||
/** DB key_deriv_strategy 컬럼에 사용하는 FQCN 참조용 상수. */
|
||||
public static final String STRATEGY_CLASS = HsmContextXorKeyDerivationStrategy.class.getName();
|
||||
|
||||
private static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
|
||||
private static final String PARAM_CONTEXT_KEY = "contextKey";
|
||||
private static final String PARAM_OFFSET = "offset";
|
||||
private static final String PARAM_LENGTH = "length";
|
||||
|
||||
@Override
|
||||
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
|
||||
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
|
||||
String contextKey = required(params, PARAM_CONTEXT_KEY);
|
||||
int offset = Integer.parseInt(params.getOrDefault(PARAM_OFFSET, "0"));
|
||||
int length = Integer.parseInt(required(params, PARAM_LENGTH));
|
||||
|
||||
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
|
||||
byte[] masterBytes = masterKey.getEncoded();
|
||||
|
||||
String contextValue = runtimeContext.getOrDefault(contextKey, "");
|
||||
byte[] contextBytes = contextValue.getBytes("UTF-8");
|
||||
|
||||
byte[] derived = xor(masterBytes, contextBytes, offset, length);
|
||||
return new DerivedKey(derived, derived);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
|
||||
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
|
||||
String contextValue = runtimeContext.getOrDefault(contextKey, "");
|
||||
return cryptoName + ":" + contextValue;
|
||||
}
|
||||
|
||||
private byte[] xor(byte[] masterBytes, byte[] contextBytes, int offset, int length) {
|
||||
byte[] derived = Arrays.copyOf(masterBytes, masterBytes.length);
|
||||
for (int i = 0; i < length && i < derived.length; i++) {
|
||||
int contextIdx = offset + i;
|
||||
if (contextIdx < contextBytes.length) {
|
||||
derived[i] ^= contextBytes[contextIdx];
|
||||
}
|
||||
}
|
||||
return derived;
|
||||
}
|
||||
|
||||
private String required(Map<String, String> params, String key) {
|
||||
String value = params.get(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private HsmCryptoService hsmCryptoService() {
|
||||
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
|
||||
/**
|
||||
* HSM 마스터키를 사용하는 전략
|
||||
*
|
||||
* key_deriv_params (JSON) 예시: { "hsmKeyAlias" : "MASTER_KEY_AES"}
|
||||
*/
|
||||
public class HsmKeyDerivationStrategy extends BaseHsmKeyDerivationStrategy {
|
||||
|
||||
@Override
|
||||
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
|
||||
byte[] masterKeyBytes = super.getHsmKey(params);
|
||||
return new DerivedKey(masterKeyBytes, masterKeyBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
|
||||
return cryptoName;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
|
||||
/**
|
||||
* HSM 마스터키를 SHA-256 해싱으로 키를 도출하는 전략.
|
||||
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
|
||||
*
|
||||
* key_deriv_params (JSON) 예시:
|
||||
* {
|
||||
* "hsmKeyAlias" : "MASTER_KEY_AES"
|
||||
* }
|
||||
*/
|
||||
public class HsmSha256KeyDerivationStrategy extends BaseHsmKeyDerivationStrategy {
|
||||
|
||||
@Override
|
||||
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
|
||||
byte[] masterKeyBytes = super.getHsmKey(params);
|
||||
byte[] derived = MessageDigest.getInstance("SHA-256").digest(masterKeyBytes);
|
||||
return new DerivedKey(derived, derived);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
|
||||
return cryptoName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.eactive.eai.common.security.mapper;
|
||||
|
||||
import org.mapstruct.InheritInverseConfiguration;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import com.eactive.eai.common.security.CryptoModuleConfigVO;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.eactive.eai.data.mapper.GenericMapper;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface CryptoModuleConfigMapper extends GenericMapper<CryptoModuleConfigVO, CryptoModuleConfig> {
|
||||
|
||||
@Override
|
||||
CryptoModuleConfigVO toVo(CryptoModuleConfig entity);
|
||||
|
||||
@InheritInverseConfiguration
|
||||
CryptoModuleConfig toEntity(CryptoModuleConfigVO vo);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package com.eactive.eai.common.session;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.ignite.IgniteCache;
|
||||
|
||||
import com.eactive.eai.authserver.service.BearerTokenInfo;
|
||||
import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
@@ -11,6 +16,7 @@ import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.server.EAIServerVO;
|
||||
import com.eactive.eai.common.sessioninfo.TerminalInfoVO;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
|
||||
/**
|
||||
* 1. 기능 :
|
||||
@@ -60,6 +66,7 @@ public abstract class SessionManager implements Lifecycle {
|
||||
public static final String WEBSOCKER_CAHCE_NAME = "webSocketTimeout";
|
||||
public static final String TERMINAL_CAHCE_NAME = "terminalSession";
|
||||
public static final String CAGATEWAY_TOKEN_CACHE_NAME = "CAGatewayToken";
|
||||
public static final String OUTBOUND_ACCESS_TOKEN_CACHE_NAME = "OutBoundAccessToken";
|
||||
|
||||
public static final String SESSION_MANAGER_GROUPNAME = "RMIInfo";
|
||||
public static final String SESSION_MANAGER_CACHETYPE = "cacheType";
|
||||
@@ -204,6 +211,13 @@ public abstract class SessionManager implements Lifecycle {
|
||||
|
||||
public abstract void removeCAToken(String key);
|
||||
|
||||
//Outbound Access Token Cache
|
||||
public abstract AccessTokenVO getOutboundAccessToken(String key, Function<AccessTokenVO, AccessTokenVO> getNewToken);
|
||||
|
||||
public abstract void removeOutboundAccessToken(String key);
|
||||
|
||||
public abstract void clearOutboundAccessToken();
|
||||
|
||||
|
||||
// HTTP Cache
|
||||
public abstract SessionVO getHttpLogin(String key);
|
||||
@@ -245,4 +259,5 @@ public abstract class SessionManager implements Lifecycle {
|
||||
public abstract void closeManager();
|
||||
|
||||
public abstract String getCacheStatus();
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.eactive.eai.common.session;
|
||||
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.eactive.eai.authserver.service.BearerTokenInfo;
|
||||
import com.eactive.eai.common.ehcache.CustomRMICacheReplicatorFactory;
|
||||
@@ -14,6 +16,7 @@ import com.eactive.eai.common.sessioninfo.TerminalInfoDAO;
|
||||
import com.eactive.eai.common.sessioninfo.TerminalInfoVO;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.util.HealthCheckManager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
|
||||
import net.sf.ehcache.Cache;
|
||||
import net.sf.ehcache.CacheException;
|
||||
@@ -937,4 +940,19 @@ public class SessionManagerForEhcache extends SessionManager {
|
||||
public void removeCAToken(String key) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenVO getOutboundAccessToken(String key, Function<AccessTokenVO, AccessTokenVO> getNewToken) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeOutboundAccessToken(String key) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearOutboundAccessToken() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.cache.Cache;
|
||||
import javax.cache.configuration.Factory;
|
||||
@@ -54,6 +58,7 @@ import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.worker.Future;
|
||||
import com.eactive.eai.common.worker.ResponseMap;
|
||||
import com.eactive.eai.util.HealthCheckManager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
|
||||
@@ -66,6 +71,8 @@ import ch.qos.logback.classic.Level;
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
public class SessionManagerForIgnite extends SessionManager {
|
||||
|
||||
private static final String DISTRIBUTED_TOKEN_LOCK = "DISTRIBUTED_TOKEN_LOCK";
|
||||
// evictMaster Instance - single socket 관련
|
||||
private static IgniteCache<String, String> evictMasterCache = null;
|
||||
// Socket Session cache 정보
|
||||
@@ -76,8 +83,11 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
private static IgniteCache<String, CacheStoreObject> cacheStore = null;
|
||||
// CA Gateway Token 관리용
|
||||
private static IgniteCache<String, BearerTokenInfo> cacheCAToken = null;
|
||||
// OutboundAccessToekn 관리용
|
||||
private static IgniteCache<String, AccessTokenVO> cacheOutBoundAccessToken = null;
|
||||
// webSocket Login cache 정보
|
||||
private static IgniteCache<String, SessionVO> cacheLogin = null; // key = loginId
|
||||
|
||||
private static IgniteCache<String, String> cacheConvertUserId = null; // 단말ID 를 userID로 변경
|
||||
// HTTP Polling 에 대한 timeout 관리용
|
||||
private static IgniteCache<String, SessionVO> httpLogin = null; // key == uuid
|
||||
@@ -392,6 +402,7 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
caches.add(initCache(config, sessionTimeout, bootstrapAsynchronously, USERID_CAHCE_NAME, 20000, false));
|
||||
caches.add(initCache(config, webSocketTimeout, bootstrapAsynchronously, WEBSOCKER_CAHCE_NAME, 20000, false));
|
||||
caches.add(initCache(config, sessionTimeout, bootstrapAsynchronously, TERMINAL_CAHCE_NAME, 20000, false));
|
||||
caches.add(initCache(config, 0, bootstrapAsynchronously, OUTBOUND_ACCESS_TOKEN_CACHE_NAME, 1000, false, true)); // transactionMode true
|
||||
caches.add(initCache(config, 0, bootstrapAsynchronously, CAGATEWAY_TOKEN_CACHE_NAME, 1000, false));
|
||||
|
||||
config.setCacheConfiguration(caches.stream().toArray(CacheConfiguration[]::new));
|
||||
@@ -421,6 +432,7 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
cacheTopic = manager.cache(TOPIC_CAHCE_NAME);
|
||||
cacheStore = manager.cache(STORE_CAHCE_NAME);
|
||||
cacheCAToken = manager.cache(CAGATEWAY_TOKEN_CACHE_NAME);
|
||||
cacheOutBoundAccessToken = manager.cache(OUTBOUND_ACCESS_TOKEN_CACHE_NAME);
|
||||
|
||||
cacheLogin = manager.cache(LOGIN_CAHCE_NAME);
|
||||
httpLogin = manager.cache(HTTP_CAHCE_NAME);
|
||||
@@ -428,49 +440,56 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
cacheWebSocketTimeout = manager.cache(WEBSOCKER_CAHCE_NAME);
|
||||
cacheTerminal = manager.cache(TERMINAL_CAHCE_NAME);
|
||||
}
|
||||
|
||||
private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs,
|
||||
String bootstrapAsynchronously, String name, int maxsize, boolean useCacheWriteModeAsync,
|
||||
boolean transactionMode) {
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(logPrefix + " create " + name + " cache expiryDurationSecs=" + expiryDurationSecs);
|
||||
}
|
||||
// near-cache
|
||||
NearCacheConfiguration nearConfiguration = new NearCacheConfiguration();
|
||||
LruEvictionPolicy nearEvictionPolicy = new LruEvictionPolicy();
|
||||
nearEvictionPolicy.setMaxSize(maxsize);
|
||||
nearConfiguration.setNearEvictionPolicy(nearEvictionPolicy);
|
||||
|
||||
CacheConfiguration cacheConfig = new CacheConfiguration(name);
|
||||
|
||||
cacheConfig.setCacheMode(CacheMode.PARTITIONED);
|
||||
|
||||
// ATOMIC,TRANSACTIONAL
|
||||
if (transactionMode) {
|
||||
cacheConfig.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
|
||||
} else {
|
||||
cacheConfig.setAtomicityMode(CacheAtomicityMode.ATOMIC);
|
||||
}
|
||||
|
||||
// Set the number of backups (replicas) for each partition : default(0)
|
||||
cacheConfig.setBackups(1);
|
||||
// Enable reading from backups to improve read consistency : default(true)
|
||||
cacheConfig.setReadFromBackup(true);
|
||||
|
||||
// FULL_SYNC/FULL_ASYNC/PRIMARY_SYNC(default)
|
||||
if (useCacheWriteModeAsync) {
|
||||
cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_ASYNC);
|
||||
} else {
|
||||
// cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC);
|
||||
cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.PRIMARY_SYNC);
|
||||
}
|
||||
|
||||
if (expiryDurationSecs > 0) {
|
||||
cacheConfig.setExpiryPolicyFactory(
|
||||
ModifiedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, expiryDurationSecs)));
|
||||
} else {
|
||||
cacheConfig.setExpiryPolicyFactory(ModifiedExpiryPolicy.factoryOf(Duration.ETERNAL));
|
||||
}
|
||||
cacheConfig.setNearConfiguration(nearConfiguration);
|
||||
return cacheConfig;
|
||||
}
|
||||
|
||||
private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs, String bootstrapAsynchronously,
|
||||
String name, int maxsize, boolean useCacheWriteModeAsync) {
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(logPrefix + " create " + name + " cache expiryDurationSecs=" + expiryDurationSecs);
|
||||
}
|
||||
// near-cache
|
||||
NearCacheConfiguration nearConfiguration = new NearCacheConfiguration();
|
||||
LruEvictionPolicy nearEvictionPolicy = new LruEvictionPolicy();
|
||||
nearEvictionPolicy.setMaxSize(maxsize);
|
||||
nearConfiguration.setNearEvictionPolicy(nearEvictionPolicy);
|
||||
|
||||
CacheConfiguration cacheConfig = new CacheConfiguration(name);
|
||||
// REPLICATED,PARTITIONED(default)
|
||||
// cacheConfig.setCacheMode(CacheMode.REPLICATED);
|
||||
cacheConfig.setCacheMode(CacheMode.PARTITIONED);
|
||||
|
||||
// ATOMIC,TRANSACTIONAL
|
||||
cacheConfig.setAtomicityMode(CacheAtomicityMode.ATOMIC);
|
||||
|
||||
// Set the number of backups (replicas) for each partition : default(0)
|
||||
cacheConfig.setBackups(1);
|
||||
// Enable reading from backups to improve read consistency : default(true)
|
||||
cacheConfig.setReadFromBackup(true);
|
||||
|
||||
// FULL_SYNC/FULL_ASYNC/PRIMARY_SYNC(default)
|
||||
if (useCacheWriteModeAsync) {
|
||||
cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_ASYNC);
|
||||
} else {
|
||||
// cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC);
|
||||
cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.PRIMARY_SYNC);
|
||||
}
|
||||
|
||||
if(expiryDurationSecs > 0) {
|
||||
cacheConfig
|
||||
.setExpiryPolicyFactory(ModifiedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, expiryDurationSecs)));
|
||||
}
|
||||
else {
|
||||
cacheConfig
|
||||
.setExpiryPolicyFactory( ModifiedExpiryPolicy.factoryOf(Duration.ETERNAL) );
|
||||
}
|
||||
cacheConfig.setNearConfiguration(nearConfiguration);
|
||||
return cacheConfig;
|
||||
return initCache(config, expiryDurationSecs, bootstrapAsynchronously, name, maxsize, useCacheWriteModeAsync, false);
|
||||
}
|
||||
|
||||
// WebSocket Login 관련
|
||||
@@ -868,5 +887,114 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
cacheCAToken.remove(key);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void sleepQuietly(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
// 인터럽트 상태 복구 (중요)
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public AccessTokenVO getOutboundAccessToken(String key, Function<AccessTokenVO, AccessTokenVO> tokenSupplier) {
|
||||
// 1. 먼저 캐시에서 조회 (락 없이 빠르게)
|
||||
AccessTokenVO token = cacheOutBoundAccessToken.get(key);
|
||||
|
||||
// 2. 만료되었거나 없다면 락 획득 시도
|
||||
if (token == null || token.isExpired()) {
|
||||
Lock lock = cacheOutBoundAccessToken.lock(DISTRIBUTED_TOKEN_LOCK); // Ignite 분산 락
|
||||
|
||||
boolean acquired = false;
|
||||
|
||||
try {
|
||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock");
|
||||
acquired = lock.tryLock(60, TimeUnit.SECONDS);
|
||||
|
||||
if ( acquired ) {
|
||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock success=>"+acquired);
|
||||
// 3. 락 획득 후 다시 확인 (그 사이 다른 인스턴스가 갱신했을 수 있음)
|
||||
token = cacheOutBoundAccessToken.get(key);
|
||||
|
||||
logger.debug(String.format("getting token in ignite=%s", token));
|
||||
|
||||
if (token == null || token.isExpired()) {
|
||||
// 4. 실제로 비어있을 때만 외부 API 호출 (I/O 발생)
|
||||
token = tokenSupplier.apply(token);
|
||||
|
||||
logger.debug(String.format("getting token is null or expired borrow new token =%s", token));
|
||||
|
||||
cacheOutBoundAccessToken.put(key, token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock fail=>"+acquired);
|
||||
}
|
||||
}catch(Throwable e) {
|
||||
logger.error("occuring exception in getOutboundAccessToken", e);
|
||||
}finally {
|
||||
|
||||
try {
|
||||
if( acquired ) {
|
||||
lock.unlock();
|
||||
logger.debug("calling func getOutboundAccessToken = distributed ignite unlock");
|
||||
}
|
||||
}catch(Throwable e) {
|
||||
logger.error("occuring exception in getOutboundAccessToken unlock fail.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void removeOutboundAccessToken(String key) {
|
||||
|
||||
Lock lock = cacheOutBoundAccessToken.lock(DISTRIBUTED_TOKEN_LOCK); // Ignite 분산 락
|
||||
|
||||
boolean acquired = false;
|
||||
try {
|
||||
|
||||
logger.debug("calling func removeOutboundAccessToken = distributed ignite trylock");
|
||||
acquired = lock.tryLock(60, TimeUnit.SECONDS);
|
||||
|
||||
if( acquired ) {
|
||||
if (logger.isDebug()) {
|
||||
|
||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock success=>"
|
||||
+ acquired);
|
||||
|
||||
logger.debug("calling func removeOutboundAccessToken Remove OutBoundAccessToken key - " + key);
|
||||
}
|
||||
cacheOutBoundAccessToken.remove(key);
|
||||
}else {
|
||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock fail=>"+acquired);
|
||||
}
|
||||
|
||||
|
||||
} catch (Throwable e) {
|
||||
logger.error("occuring exception in removeOutboundAccessToken", e);
|
||||
} finally {
|
||||
|
||||
try {
|
||||
if (acquired) {
|
||||
lock.unlock();
|
||||
logger.debug("calling func removeOutboundAccessToken = distributed ignite unlock");
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.error("occuring exception in removeOutboundAccessToken unlock fail.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearOutboundAccessToken() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ public class HttpAdapterExtraLogUtil {
|
||||
}
|
||||
httpAdapterExtraLogVo.setHeaderList(headerVoList);
|
||||
httpAdapterExtraLogVo.setHttpStatus(httpStatus);
|
||||
|
||||
logger.debug("[{}] Insert HTTP Log. - {}", guid, httpAdapterExtraLogVo);
|
||||
|
||||
if(ElinkConfig.isUseAsyncLogging()) {
|
||||
AsyncHttpLoggingPoolManager.getInstance().publish(httpAdapterExtraLogVo);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
package com.eactive.eai.common.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 보안아키텍처 마스킹 정책 구현 유틸리티
|
||||
*
|
||||
* <pre>
|
||||
* [고유식별정보]
|
||||
* 주민등록번호 123456-1234567 → 123456-1******
|
||||
* 운전면허번호 제주-22-300001-50 → 제주-22-3*****-**
|
||||
* 여권번호 M12345678 → M1234****
|
||||
* 외국인등록번호 123456-5234567 → 123456-5******
|
||||
*
|
||||
* [금융식별정보]
|
||||
* 계좌번호 99-912345-1 → 99-9*-*****1
|
||||
* 카드번호 4518-4212-3456-1234 → 4518-42**-****-1234
|
||||
* 유효기간 12/25 → **/**
|
||||
* CVV 123 → ***
|
||||
*
|
||||
* [인증정보]
|
||||
* 비밀번호 password → ********
|
||||
*
|
||||
* [신상정보]
|
||||
* 전화번호 064-1234-5678 → 064-12**-56**
|
||||
* 이메일 aajjbacc@shinhan.com → **jjba**@shinhan.com
|
||||
* 한글 성명 홍길동 → 홍*동
|
||||
* 영문 성명 GILDONG HONG → ******* HONG
|
||||
* 지번주소 제주특별자치도 제주시 연동 123번지, 101동 202호 → 제주특별자치도 제주시 연동 ***번지, **** ****
|
||||
* 도로명주소 제주특별자치도 제주시 은남2길 5, 101동 202호 → 제주특별자치도 제주시 은남**길 ***, **** ****
|
||||
*
|
||||
* [온라인 정보]
|
||||
* IPv4 100.200.123.45 → 100.200.***.45
|
||||
* IPv6 21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A → 21DA:00D3:0000:2F3B:02AA:00FF:FE28:****
|
||||
* </pre>
|
||||
*/
|
||||
public final class MaskingUtils {
|
||||
|
||||
private static final char MASK = '*';
|
||||
|
||||
private MaskingUtils() {}
|
||||
|
||||
// =========================================================================
|
||||
// 고유식별정보
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 주민등록번호 마스킹: 뒷 6자리 마스킹
|
||||
* <pre>123456-1234567 → 123456-1******</pre>
|
||||
*/
|
||||
public static String maskResidentNumber(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
int idx = value.indexOf('-');
|
||||
if (idx < 0) {
|
||||
return maskTailChars(value, 6);
|
||||
}
|
||||
// 하이픈 + 성별 1자리까지 노출, 이후 6자리 마스킹
|
||||
String front = value.substring(0, Math.min(idx + 2, value.length()));
|
||||
return front + stars(6);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운전면허번호 마스킹: 뒤에서부터 숫자 7자리 마스킹
|
||||
* <pre>제주-22-300001-50 → 제주-22-3*****-**</pre>
|
||||
*/
|
||||
public static String maskDriverLicense(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
char[] chars = value.toCharArray();
|
||||
int digitCount = 0;
|
||||
for (int i = chars.length - 1; i >= 0 && digitCount < 7; i--) {
|
||||
if (Character.isDigit(chars[i])) {
|
||||
chars[i] = MASK;
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 여권번호 마스킹: 뒤에서부터 4자리 마스킹
|
||||
* <pre>M12345678 → M1234****</pre>
|
||||
*/
|
||||
public static String maskPassport(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
return maskTailChars(value, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 외국인등록번호 마스킹: 뒷 6자리 마스킹 (주민등록번호와 동일)
|
||||
* <pre>123456-5234567 → 123456-5******</pre>
|
||||
*/
|
||||
public static String maskForeignerNumber(String value) {
|
||||
return maskResidentNumber(value);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 금융식별정보
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 계좌번호 마스킹: 첫 3자리, 마지막 1자리만 노출
|
||||
* <pre>99-912345-1 → 99-9*-*****1</pre>
|
||||
*/
|
||||
public static String maskAccountNumber(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
String digitsOnly = value.replaceAll("[^0-9]", "");
|
||||
int totalDigits = digitsOnly.length();
|
||||
if (totalDigits <= 4) return value;
|
||||
|
||||
char[] chars = value.toCharArray();
|
||||
int digitIdx = 0;
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
if (Character.isDigit(chars[i])) {
|
||||
digitIdx++;
|
||||
// 4번째 자리부터 마지막 자리 직전까지 마스킹
|
||||
if (digitIdx > 3 && digitIdx < totalDigits) {
|
||||
chars[i] = MASK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드번호 마스킹: PCI-DSS 기준 7~12번째 숫자 자리 마스킹
|
||||
* <pre>4518-4212-3456-1234 → 4518-42**-****-1234</pre>
|
||||
*/
|
||||
public static String maskCardNumber(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
char[] chars = value.toCharArray();
|
||||
int digitIdx = 0;
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
if (Character.isDigit(chars[i])) {
|
||||
digitIdx++;
|
||||
if (digitIdx >= 7 && digitIdx <= 12) {
|
||||
chars[i] = MASK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 유효기간 마스킹: 월/년 모두 마스킹
|
||||
* <pre>12/25 → **/**</pre>
|
||||
*/
|
||||
public static String maskCardExpiry(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
return value.replaceAll("[0-9]", String.valueOf(MASK));
|
||||
}
|
||||
|
||||
/**
|
||||
* CVV 마스킹: 3자리 전체 마스킹
|
||||
* <pre>123 → ***</pre>
|
||||
*/
|
||||
public static String maskCvv(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
return stars(value.length());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 인증정보
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 비밀번호 마스킹: 전체 자리 마스킹
|
||||
* <pre>password1234 → ************</pre>
|
||||
*/
|
||||
public static String maskPassword(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
return stars(value.length());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 신상정보
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 전화번호/FAX 마스킹: 가운데 뒷 2자리, 뒷자리 뒷 2자리 마스킹
|
||||
* 하이픈 없는 형식은 자릿수·접두사 기반으로 세그먼트를 분리한 뒤 마스킹하고 하이픈 없이 반환
|
||||
* <pre>
|
||||
* 064-1234-5678 → 064-12**-56**
|
||||
* 06412345678 → 0641234**56**
|
||||
* 01012345678 → 01012**56**
|
||||
* </pre>
|
||||
*/
|
||||
public static String maskPhoneNumber(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
boolean hasHyphen = value.contains("-");
|
||||
String normalized = hasHyphen ? value : normalizePhoneNumber(value);
|
||||
String[] parts = normalized.split("-");
|
||||
if (parts.length == 3) {
|
||||
String m0 = parts[0];
|
||||
String m1 = maskSegmentTail(parts[1], 2);
|
||||
String m2 = maskSegmentTail(parts[2], 2);
|
||||
return hasHyphen ? m0 + "-" + m1 + "-" + m2 : m0 + m1 + m2;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국 전화번호(숫자만) → XXX-XXXX-XXXX 형식으로 정규화
|
||||
* <pre>
|
||||
* 02 + 7자리 → 02-XXX-XXXX
|
||||
* 02 + 8자리 → 02-XXXX-XXXX
|
||||
* 0XX + 7자리 → 0XX-XXX-XXXX
|
||||
* 0XX + 8자리 → 0XX-XXXX-XXXX
|
||||
* </pre>
|
||||
*/
|
||||
private static String normalizePhoneNumber(String digits) {
|
||||
int len = digits.length();
|
||||
if (digits.startsWith("02")) {
|
||||
if (len == 9) return digits.substring(0, 2) + "-" + digits.substring(2, 5) + "-" + digits.substring(5);
|
||||
if (len == 10) return digits.substring(0, 2) + "-" + digits.substring(2, 6) + "-" + digits.substring(6);
|
||||
} else if (digits.startsWith("0")) {
|
||||
if (len == 10) return digits.substring(0, 3) + "-" + digits.substring(3, 6) + "-" + digits.substring(6);
|
||||
if (len == 11) return digits.substring(0, 3) + "-" + digits.substring(3, 7) + "-" + digits.substring(7);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴대폰 번호 마스킹: 가운데 뒷 2자리, 뒷자리 뒷 2자리 마스킹
|
||||
* <pre>010-1234-5678 → 010-12**-56**</pre>
|
||||
*/
|
||||
public static String maskMobileNumber(String value) {
|
||||
return maskPhoneNumber(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 마스킹: 앞 2자리와 @ 앞 2자리 마스킹
|
||||
* <pre>aajjbacc@shinhan.com → **jjba**@shinhan.com</pre>
|
||||
*/
|
||||
public static String maskEmail(String value) {
|
||||
if (value == null || value.isEmpty() || !value.contains("@")) return value;
|
||||
int atIdx = value.indexOf('@');
|
||||
String local = value.substring(0, atIdx);
|
||||
String domain = value.substring(atIdx);
|
||||
if (local.length() <= 4) {
|
||||
return stars(local.length()) + domain;
|
||||
}
|
||||
return stars(2) + local.substring(2, local.length() - 2) + stars(2) + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 한글/한자 성명 마스킹
|
||||
* <pre>
|
||||
* 2자: 뒤 1자리 마스킹 홍동 → 홍*
|
||||
* 3자: 가운데 1자리 마스킹 홍길동 → 홍*동
|
||||
* 4자 이상: 앞뒤 1자 제외한 가운데 전체 마스킹 홍길순동 → 홍**동
|
||||
* </pre>
|
||||
*/
|
||||
public static String maskKoreanName(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
int len = value.length();
|
||||
if (len == 1) return value;
|
||||
if (len == 2) {
|
||||
return String.valueOf(value.charAt(0)) + MASK;
|
||||
}
|
||||
if (len == 3) {
|
||||
return String.valueOf(value.charAt(0)) + MASK + value.charAt(2);
|
||||
}
|
||||
// 4자 이상: 첫 자 + 가운데 전체 마스킹 + 마지막 자
|
||||
return String.valueOf(value.charAt(0)) + stars(len - 2) + value.charAt(len - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 영문 성명 마스킹: 성(last name)만 노출, 이름(first name)은 마스킹
|
||||
* <pre>GILDONG HONG → ******* HONG</pre>
|
||||
* (마지막 공백 구분 토큰이 성(last name)으로 간주)
|
||||
*/
|
||||
public static String maskEnglishName(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
String[] parts = value.trim().split("\\s+");
|
||||
if (parts.length == 1) return value;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
if (i > 0) sb.append(' ');
|
||||
sb.append(stars(parts[i].length()));
|
||||
}
|
||||
sb.append(' ').append(parts[parts.length - 1]);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 지번주소 마스킹: 번지 이하 주소 마스킹 (String.length() 보존)
|
||||
* <pre>
|
||||
* 제주특별자치도 제주시 연동 123번지, 101동 202호 → 제주특별자치도 제주시 연동 ***번지, **** ****
|
||||
* </pre>
|
||||
*/
|
||||
public static String maskJibunAddress(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
// 번지 앞 숫자 자릿수 보존 마스킹
|
||||
String result = maskDigitsBefore(value, "번지");
|
||||
// 쉼표 이후 상세주소 마스킹 (공백 유지, 자릿수 보존)
|
||||
int commaIdx = result.indexOf(',');
|
||||
if (commaIdx >= 0) {
|
||||
String after = result.substring(commaIdx + 1);
|
||||
result = result.substring(0, commaIdx + 1) + after.replaceAll("[^\\s]", String.valueOf(MASK));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 도로명주소 마스킹: 도로명 번호 및 번지 이하 마스킹 (String.length() 보존)
|
||||
* <pre>
|
||||
* 제주특별자치도 제주시 은남2길 5, 101동 202호 → 제주특별자치도 제주시 은남*길 *, **** ****
|
||||
* </pre>
|
||||
*/
|
||||
public static String maskRoadAddress(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
// 도로명 번호 자릿수 보존 마스킹 (긴 suffix 먼저 처리)
|
||||
String result = maskDigitsBefore(value, "대로");
|
||||
result = maskDigitsBefore(result, "번길");
|
||||
result = maskDigitsBefore(result, "길");
|
||||
result = maskDigitsBefore(result, "로");
|
||||
// 도로명 뒤 집 번호 자릿수 보존 마스킹
|
||||
result = maskDigitsAfter(result, "대로");
|
||||
result = maskDigitsAfter(result, "번길");
|
||||
result = maskDigitsAfter(result, "길");
|
||||
result = maskDigitsAfter(result, "로");
|
||||
// 쉼표 이후 상세주소 마스킹 (공백 유지, 자릿수 보존)
|
||||
int commaIdx = result.indexOf(',');
|
||||
if (commaIdx >= 0) {
|
||||
String after = result.substring(commaIdx + 1);
|
||||
result = result.substring(0, commaIdx + 1) + after.replaceAll("[^\\s]", String.valueOf(MASK));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 온라인 정보
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* IPv4 마스킹: 3번째 옥텟(17~24 비트) 마스킹
|
||||
* <pre>100.200.123.45 → 100.200.***.45</pre>
|
||||
*/
|
||||
public static String maskIpv4(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
String[] parts = value.split("\\.", -1);
|
||||
if (parts.length != 4) return value;
|
||||
return parts[0] + "." + parts[1] + "." + stars(3) + "." + parts[3];
|
||||
}
|
||||
|
||||
/**
|
||||
* IPv6 마스킹: 마지막 그룹(113~128 비트) 마스킹
|
||||
* <pre>21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A → 21DA:00D3:0000:2F3B:02AA:00FF:FE28:****</pre>
|
||||
*/
|
||||
public static String maskIpv6(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
int lastColon = value.lastIndexOf(':');
|
||||
if (lastColon < 0) return value;
|
||||
return value.substring(0, lastColon + 1) + stars(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* IP 주소 마스킹: IPv4/IPv6 자동 판별, 쉼표 구분 다중 IP 지원
|
||||
* <pre>
|
||||
* 100.200.123.45 → 100.200.***.45
|
||||
* 21DA:...FE28:9C5A → 21DA:...FE28:****
|
||||
* 192.168.1.1,10.0.0.1 → 192.168.***.1,10.0.***.1
|
||||
* </pre>
|
||||
*/
|
||||
public static String maskIpAddress(String value) {
|
||||
if (value == null || value.isEmpty()) return value;
|
||||
String[] ips = value.split(",");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < ips.length; i++) {
|
||||
if (i > 0) sb.append(',');
|
||||
String ip = ips[i].trim();
|
||||
if (ip.contains(":")) {
|
||||
sb.append(maskIpv6(ip));
|
||||
} else {
|
||||
sb.append(maskIpv4(ip));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// private helpers
|
||||
// =========================================================================
|
||||
|
||||
private static String stars(int count) {
|
||||
if (count <= 0) return "";
|
||||
char[] arr = new char[count];
|
||||
Arrays.fill(arr, MASK);
|
||||
return new String(arr);
|
||||
}
|
||||
|
||||
private static String maskTailChars(String value, int count) {
|
||||
if (value.length() <= count) return stars(value.length());
|
||||
return value.substring(0, value.length() - count) + stars(count);
|
||||
}
|
||||
|
||||
private static String maskSegmentTail(String segment, int count) {
|
||||
if (segment.length() <= count) return stars(segment.length());
|
||||
return segment.substring(0, segment.length() - count) + stars(count);
|
||||
}
|
||||
|
||||
/** keyword 바로 앞에 연속된 숫자를 동일 자릿수의 '*'로 마스킹 */
|
||||
private static String maskDigitsBefore(String value, String keyword) {
|
||||
StringBuilder sb = new StringBuilder(value);
|
||||
int from = 0;
|
||||
while (true) {
|
||||
int kIdx = sb.indexOf(keyword, from);
|
||||
if (kIdx < 0) break;
|
||||
int end = kIdx;
|
||||
int start = end;
|
||||
while (start > 0 && Character.isDigit(sb.charAt(start - 1))) {
|
||||
start--;
|
||||
}
|
||||
for (int i = start; i < end; i++) {
|
||||
sb.setCharAt(i, MASK);
|
||||
}
|
||||
from = kIdx + keyword.length();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** keyword 바로 뒤 공백 이후의 연속된 숫자를 동일 자릿수의 '*'로 마스킹 */
|
||||
private static String maskDigitsAfter(String value, String keyword) {
|
||||
StringBuilder sb = new StringBuilder(value);
|
||||
int from = 0;
|
||||
while (true) {
|
||||
int kIdx = sb.indexOf(keyword, from);
|
||||
if (kIdx < 0) break;
|
||||
int pos = kIdx + keyword.length();
|
||||
while (pos < sb.length() && sb.charAt(pos) == ' ') pos++;
|
||||
int start = pos;
|
||||
while (pos < sb.length() && Character.isDigit(sb.charAt(pos))) pos++;
|
||||
for (int i = start; i < pos; i++) {
|
||||
sb.setCharAt(i, MASK);
|
||||
}
|
||||
from = kIdx + keyword.length();
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.eactive.eai.common.util;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
public class TxFileLogger {
|
||||
|
||||
public static void logTxFile(Properties transactionProp, String message, String prefix) {
|
||||
Logger siftLogger = Logger.getLogger(Logger.LOGGER_SIFT);
|
||||
if (siftLogger.isInfoEnabled()) {
|
||||
siftLogger.info(prefix + message);
|
||||
if (siftLogger.isDebugEnabled())
|
||||
siftLogger.debug(CommonLib.getDumpMessage(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.eactive.eai.inbound.processor;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.dual.DualBucket;
|
||||
import com.eactive.eai.common.inflow.dual.DualCustomBucket;
|
||||
|
||||
/**
|
||||
* 어댑터/인터페이스 유량제어 차단 시 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지
|
||||
* 에러 메시지에 포함하는 RequestProcessor 확장.
|
||||
*
|
||||
* <p>DualInflowControlManager와 함께 사용:
|
||||
* <pre>
|
||||
* inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
|
||||
* </pre>
|
||||
*
|
||||
* <p>Spring Bean 등록 후 기존 RequestProcessor 대신 이 클래스를 어댑터에 설정한다.
|
||||
*/
|
||||
public class DualRequestProcessor extends RequestProcessor {
|
||||
|
||||
@Override
|
||||
protected String checkClientInflow(Bucket bucket, String clientId) {
|
||||
if (StringUtils.isBlank(clientId)) return null;
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).isClientPassDetail(clientId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) {
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).getClientInflowThreshold(clientId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String checkAdapterInflow(Bucket bucket, String adapterGroupName) {
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).isAdapterPassDetail(adapterGroupName);
|
||||
}
|
||||
return super.checkAdapterInflow(bucket, adapterGroupName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String checkInterfaceInflow(Bucket bucket, String eaiSvcCd) {
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).isInterfacePassDetail(eaiSvcCd);
|
||||
}
|
||||
return super.checkInterfaceInflow(bucket, eaiSvcCd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차단 원인에 따라 에러 메시지에 한도 정보를 포함.
|
||||
* <ul>
|
||||
* <li>PER_SECOND: "API 호출 한도 초과 [name: Nreq/sec]"</li>
|
||||
* <li>THRESHOLD: "API 호출 한도 초과 [name: Nreq/UNIT]"</li>
|
||||
* <li>그 외: "API 호출 한도 초과 [name]" (기존 동작)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
protected String buildInflowTargetErrorMsg(InflowTargetVO inflowTargetVO, String blockedType) {
|
||||
if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(blockedType)) {
|
||||
return String.format("API 호출 한도 초과 [%s: %dreq/sec]",
|
||||
inflowTargetVO.getName(),
|
||||
inflowTargetVO.getThresholdPerSecond());
|
||||
}
|
||||
if (DualCustomBucket.RESULT_BLOCKED_THRESHOLD.equals(blockedType)) {
|
||||
return String.format("API 호출 한도 초과 [%s: %dreq/%s]",
|
||||
inflowTargetVO.getName(),
|
||||
inflowTargetVO.getThreshold(),
|
||||
inflowTargetVO.getThresholdTimeUnit());
|
||||
}
|
||||
return super.buildInflowTargetErrorMsg(inflowTargetVO, blockedType);
|
||||
}
|
||||
}
|
||||
@@ -129,8 +129,8 @@ public class RequestProcessor extends RequestProcessorSupport {
|
||||
String testMasterYn = adptGrpVO.getTestMasterYn();
|
||||
|
||||
// UUID 생성 : UUID에서 - 없는 32자리
|
||||
String uuid = "";
|
||||
uuid = UUIDGenerator.getUUID().toString().replaceAll("-", "");
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
uuid = uuid == null ? UUIDGenerator.getUUID().toString().replaceAll("-", "") : uuid;
|
||||
// UUID 생성 : UUID = server구분4자리 + UUID
|
||||
/*
|
||||
String uuid = "";
|
||||
|
||||
@@ -682,11 +682,10 @@ public abstract class DefaultProcess extends Process {
|
||||
AdapterVO inboundAdapterVO = AdapterManager.getInstance().getAdapterVO(inboudnAdapterGroupName, inboudnAdapterName);
|
||||
String errorResponseHandlerClass = AdapterPropManager.getInstance().getProperty(inboundAdapterVO.getPropGroupName(), "ERR_MSG_HANDLER");
|
||||
if(StringUtils.isBlank(errorResponseHandlerClass)) {
|
||||
this.resEaiMsg.setRspErrCd("RECEAIINA001", false);
|
||||
String errorCode = mapper.getErrorCode(resStandardMessage);
|
||||
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
|
||||
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("Message.Msg_Body.OUTPUT_MSG_DESC"));
|
||||
this.resEaiMsg.setRspErrMsg(String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
|
||||
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc"));
|
||||
this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -698,6 +697,8 @@ public abstract class DefaultProcess extends Process {
|
||||
if(responseObj != null) {
|
||||
resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode());
|
||||
}
|
||||
|
||||
this.resEaiMsg.setRspErrCd(EAIMessageKeys.BWK_FAILMSG_CODE, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,17 @@ import java.net.SocketTimeoutException;
|
||||
import java.util.Properties;
|
||||
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.circuitBreaker.CircuitBreakerManager;
|
||||
import com.eactive.eai.common.circuitBreaker.type.TypedCircuitBreakerManager;
|
||||
import com.eactive.eai.util.PropertiesUtil;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
@@ -17,6 +23,7 @@ import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.EAIKeys;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.header.HeaderAction;
|
||||
@@ -98,7 +105,32 @@ public class HTTPProcess extends DefaultProcess {
|
||||
this.timeout = iTimeoutValue * 1000;
|
||||
this.tempProp.put(INTERFACE_TIME_OUT, "" + this.timeout);
|
||||
try {
|
||||
this.resObject = HttpSender.callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp);
|
||||
// API별 이용
|
||||
String apiId = reqEaiMsg.getEAISvcCd();
|
||||
CircuitBreaker circuitBreaker = TypedCircuitBreakerManager.getInstance().getCircuitBreaker(apiId);
|
||||
|
||||
// 디폴트 이용
|
||||
if(circuitBreaker == null) {
|
||||
String circuitBreakerUseYn = PropManager.getInstance().getProperty("CircuitBreaker", "useYn");
|
||||
if ("Y".equals(circuitBreakerUseYn))
|
||||
circuitBreaker = CircuitBreakerManager.getInstance().getCircuitBreaker(apiId);
|
||||
}
|
||||
|
||||
if (circuitBreaker != null) {
|
||||
// ObjectMapper 인스턴스 생성
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 서킷 브레이커의 메트릭 정보와 상태 정보를 직접 Node로 변환
|
||||
ObjectNode combinedNode = objectMapper.createObjectNode();
|
||||
combinedNode.put("state", circuitBreaker.getState().toString());
|
||||
combinedNode.set("metrics", objectMapper.valueToTree(circuitBreaker.getMetrics()));
|
||||
|
||||
logger.info("CircuitBreaker state " + combinedNode.toString());
|
||||
this.resObject = circuitBreaker.executeCallable(() -> HttpSender
|
||||
.callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp));
|
||||
} else {
|
||||
this.resObject = HttpSender.callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp);
|
||||
}
|
||||
// 응답코드에 대한 처리
|
||||
this.resEaiMsg = setResponseForOutbound(this.resEaiMsg);
|
||||
if (!com.eactive.eai.adapter.Keys.IF_STANDARD.equals(this.adptrMsgPtrnCd)) {
|
||||
@@ -352,24 +384,26 @@ public class HTTPProcess extends DefaultProcess {
|
||||
this.tempProp.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
|
||||
}
|
||||
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp());
|
||||
if (eaiServerManager.isTASEnabledEAIServer() && EAIMessageKeys.TRANTYPE_TAS.equals(reqEaiMsg.getTranType())
|
||||
// RESTAdapter는 교체하지 않는다
|
||||
&& !StringUtils.equals(adptGrpVO.getType(), com.eactive.eai.adapter.Keys.TYPE_REST)) {
|
||||
PropManager manager = PropManager.getInstance();
|
||||
Properties prop = manager.getProperties(RouteKeys.SIM);
|
||||
|
||||
if (reqEaiMsg.getCurrentSvcMsg().isSyncItfTp()) {
|
||||
this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_SYNC, "_SIM_OU_HTT_SyC");
|
||||
} else {
|
||||
this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_ASYNC, "_SIM_OU_HTT_AsC");
|
||||
}
|
||||
this.reqEaiMsg.getSvcMsg(0).setPsvSysItfTp(this.adapterGroupName);
|
||||
} else {
|
||||
this.adapterGroupName = reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp();
|
||||
}
|
||||
// EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
// AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
// AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp());
|
||||
// if (eaiServerManager.isTASEnabledEAIServer() && EAIMessageKeys.TRANTYPE_TAS.equals(reqEaiMsg.getTranType())
|
||||
// // RESTAdapter는 교체하지 않는다
|
||||
// && !StringUtils.equals(adptGrpVO.getType(), com.eactive.eai.adapter.Keys.TYPE_REST)) {
|
||||
// PropManager manager = PropManager.getInstance();
|
||||
// Properties prop = manager.getProperties(RouteKeys.SIM);
|
||||
//
|
||||
// if (reqEaiMsg.getCurrentSvcMsg().isSyncItfTp()) {
|
||||
// this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_SYNC, "_SIM_OU_HTT_SyC");
|
||||
// } else {
|
||||
// this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_ASYNC, "_SIM_OU_HTT_AsC");
|
||||
// }
|
||||
// this.reqEaiMsg.getSvcMsg(0).setPsvSysItfTp(this.adapterGroupName);
|
||||
// } else {
|
||||
// this.adapterGroupName = reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp();
|
||||
// }
|
||||
|
||||
this.adapterGroupName = reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp();
|
||||
|
||||
this.svcTsmtUsgTp = this.reqEaiMsg.getSvcTsmtUsgTp(); // Inbound 호출방식
|
||||
this.psvItfTp = this.reqEaiMsg.getCurrentSvcMsg().getPsvItfTp(); // 호출방식
|
||||
@@ -404,6 +438,7 @@ public class HTTPProcess extends DefaultProcess {
|
||||
}
|
||||
|
||||
// Inbound Adapter mesageType
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO inAdptGrpVO = adapterManager.getAdapterGroupVO(this.reqEaiMsg.getSngSysItfTp());
|
||||
this.inAdapterMsgType = MessageType.ASC;
|
||||
|
||||
@@ -592,6 +627,22 @@ public class HTTPProcess extends DefaultProcess {
|
||||
logger.debug(guidLogPrefix + " HTTP NONSTANDARD Request Message : "
|
||||
+ MessageUtil.toAdapterTypeString(this.reqObject, this.inboundCharset));
|
||||
}
|
||||
// TAS 선택시 시뮬레이터 url로 변경, HTT 어댑터도 RST 처럼 방식을 변경하였다.
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
if (eaiServerManager.isTASEnabledEAIServer() && EAIMessageKeys.TRANTYPE_TAS.equals(reqEaiMsg.getTranType())) {
|
||||
PropManager manager = PropManager.getInstance();
|
||||
Properties prop = manager.getProperties(RouteKeys.SIM);
|
||||
String simAddr = prop.getProperty(RouteKeys.SIM_REST_MOCKSERVER);
|
||||
if (StringUtils.isBlank(simAddr)) {
|
||||
throw new Exception(String.format("Properties %s > %s not setted", RouteKeys.SIM,
|
||||
RouteKeys.SIM_REST_MOCKSERVER));
|
||||
}
|
||||
|
||||
String url = StringUtils.removeEnd(simAddr, "/") + "/mockapi/" + this.reqEaiMsg.getEAISvcCd();
|
||||
this.outboundProp.put(HttpClientAdapterServiceKey.URL, url);
|
||||
this.tempProp.remove(HttpAdapterServiceKey.INBOUND_REWRITE_PATH);
|
||||
logger.debug("simUrl=" + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String[] msgArgs = new String[1];
|
||||
msgArgs[0] = this.reqEaiMsg.getEAISvcCd();
|
||||
|
||||
@@ -10,34 +10,55 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
// 해당 필드에 대한 get/set 을 수행
|
||||
public class JsonPathUtil {
|
||||
private JsonPathUtil() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// JsonNode 기반 단일 파싱 API — 연속 get/set 시 파싱 횟수 절감
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** JSON 문자열을 JsonNode 트리로 파싱. */
|
||||
public static JsonNode toTree(String json) throws Exception {
|
||||
return objectMapper.readTree(json);
|
||||
}
|
||||
|
||||
/** 이미 파싱된 트리에서 경로의 값을 추출. */
|
||||
public static String getAt(JsonNode root, String path) {
|
||||
JsonNode node = getNodeAtPath(root, path);
|
||||
return node != null ? node.asText() : null;
|
||||
}
|
||||
|
||||
/** 이미 파싱된 트리의 경로에 값을 설정 (in-place). */
|
||||
public static void setAt(JsonNode root, String path, String value) {
|
||||
JsonNode parent = getParentNode(root, getParentPath(path));
|
||||
if (parent == null) return;
|
||||
String key = getFieldName(path);
|
||||
if (parent.isArray()) {
|
||||
((ArrayNode) parent).set(Integer.parseInt(key), objectMapper.convertValue(value, JsonNode.class));
|
||||
} else if (parent.isObject()) {
|
||||
((ObjectNode) parent).put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/** JsonNode 트리를 JSON 문자열로 직렬화. */
|
||||
public static String fromTree(JsonNode root) throws Exception {
|
||||
return objectMapper.writeValueAsString(root);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 문자열 기반 API (내부적으로 위 메서드에 위임)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// 특정 경로의 값을 설정하는 함수
|
||||
public static String setValueAtPath(String jsonString, String path, String newValue, boolean isPretty) {
|
||||
try {
|
||||
// JSON 문자열을 JsonNode 객체로 파싱
|
||||
JsonNode rootNode = objectMapper.readTree(jsonString);
|
||||
|
||||
// 경로에 해당하는 부모 노드를 가져옴
|
||||
JsonNode parentNode = getParentNode(rootNode, getParentPath(path));
|
||||
|
||||
// 경로의 마지막 노드 이름 또는 배열 인덱스 추출
|
||||
String fieldNameOrIndex = getFieldName(path);
|
||||
|
||||
// 배열 또는 객체에서 값을 설정
|
||||
if (parentNode.isArray()) {
|
||||
((ArrayNode) parentNode).set(Integer.parseInt(fieldNameOrIndex), objectMapper.convertValue(newValue, JsonNode.class));
|
||||
} else if (parentNode.isObject()) {
|
||||
((ObjectNode) parentNode).put(fieldNameOrIndex, newValue);
|
||||
}
|
||||
|
||||
// 수정된 JSON 문자열 반환
|
||||
setAt(rootNode, path, newValue);
|
||||
if(isPretty)
|
||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
|
||||
else
|
||||
else
|
||||
return objectMapper.writeValueAsString(rootNode);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@@ -48,13 +69,7 @@ public class JsonPathUtil {
|
||||
// 특정 경로의 값을 가져오는 함수
|
||||
public static String getValueAtPath(String jsonString, String path) {
|
||||
try {
|
||||
// JSON 문자열을 JsonNode 객체로 파싱
|
||||
JsonNode rootNode = objectMapper.readTree(jsonString);
|
||||
|
||||
// 경로에 해당하는 노드의 값을 반환
|
||||
JsonNode resultNode = getNodeAtPath(rootNode, path);
|
||||
return resultNode != null ? resultNode.asText() : null;
|
||||
|
||||
return getAt(objectMapper.readTree(jsonString), path);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
@@ -101,6 +116,71 @@ public class JsonPathUtil {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미 파싱된 bodyNode에서 removeFromPath 필드를 제거하고 mergeJson을 병합.
|
||||
* body의 재파싱 없이 호출 가능하여 파싱 횟수를 1회 절감.
|
||||
*
|
||||
* body와 mergeJson이 모두 JSON 객체인 경우에만 병합을 수행한다.
|
||||
* 어느 한쪽이 JSON 객체가 아니면 mergeJson을 그대로 반환한다 (전체 교체 fallback).
|
||||
*
|
||||
* @param bodyNode 이미 파싱된 원본 JsonNode
|
||||
* @param removeFromPath 제거할 필드의 경로 (예: /encryptedData)
|
||||
* @param mergeJson 병합할 JSON 문자열
|
||||
* @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환
|
||||
*/
|
||||
public static String mergeAtRoot(JsonNode bodyNode, String removeFromPath, String mergeJson) {
|
||||
try {
|
||||
JsonNode mergeNode = objectMapper.readTree(mergeJson);
|
||||
|
||||
if (!bodyNode.isObject() || !mergeNode.isObject()) {
|
||||
return mergeJson;
|
||||
}
|
||||
|
||||
ObjectNode result = (ObjectNode) bodyNode.deepCopy();
|
||||
|
||||
// removeFromPath 필드 제거
|
||||
int lastSlash = removeFromPath.lastIndexOf('/');
|
||||
String fieldName = removeFromPath.substring(lastSlash + 1);
|
||||
if (lastSlash == 0) {
|
||||
result.remove(fieldName);
|
||||
} else {
|
||||
JsonNode parent = getNodeAtPath(result, removeFromPath.substring(0, lastSlash));
|
||||
if (parent instanceof ObjectNode) {
|
||||
((ObjectNode) parent).remove(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
// mergeJson 필드를 body에 병합 (기존 필드 덮어쓰기)
|
||||
result.setAll((ObjectNode) mergeNode);
|
||||
return objectMapper.writeValueAsString(result);
|
||||
} catch (Exception e) {
|
||||
return mergeJson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* body에서 removeFromPath 필드를 제거한 뒤, mergeJson의 필드를 body에 병합하여 반환한다.
|
||||
* (문자열 기반 overload — 내부적으로 body를 1회 파싱하여 위 메서드에 위임)
|
||||
*
|
||||
* 사용 예 (toPath="/" 복호화):
|
||||
* body = {"encryptedData":"...","requestId":"REQ001"}
|
||||
* removeFrom = /encryptedData
|
||||
* mergeJson = {"userId":"U001","name":"test"}
|
||||
* 결과 = {"requestId":"REQ001","userId":"U001","name":"test"}
|
||||
*
|
||||
* @param body 원본 JSON 문자열
|
||||
* @param removeFromPath 제거할 필드의 경로 (예: /encryptedData)
|
||||
* @param mergeJson 병합할 JSON 문자열
|
||||
* @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환
|
||||
*/
|
||||
public static String mergeAtRoot(String body, String removeFromPath, String mergeJson) {
|
||||
try {
|
||||
return mergeAtRoot(objectMapper.readTree(body), removeFromPath, mergeJson);
|
||||
} catch (Exception e) {
|
||||
return mergeJson;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String jsonString = "{ \"person\": { \"name\": \"John\", \"age\": 30, \"groups\": [ { \"id\": 1, \"name\": \"Group A\" }, { \"id\": 2, \"name\": \"Group B\" }, { \"id\": 3, \"name\": \"Group C\" } ] } }";
|
||||
|
||||
|
||||
+725
@@ -0,0 +1,725 @@
|
||||
package com.eactive.eai.adapter.handler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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;
|
||||
import com.eactive.eai.message.StandardItem;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
import com.eactive.eai.message.StandardType;
|
||||
|
||||
/**
|
||||
* TemplateAdapterErrorMsgHandler 단위테스트
|
||||
*
|
||||
* - render() : package-private 이므로 같은 패키지에서 직접 호출
|
||||
* - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에
|
||||
* mock ApplicationContext 를 리플렉션으로 주입하여 PropManager 격리
|
||||
*/
|
||||
class TemplateAdapterErrorMsgHandlerTest {
|
||||
|
||||
private TemplateAdapterErrorMsgHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
handler = new TemplateAdapterErrorMsgHandler();
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// StandardMessage 빌더 헬퍼
|
||||
// ================================================================
|
||||
|
||||
/** FIELD 아이템 (type=2) */
|
||||
private static StandardItem field(String name, String value) {
|
||||
return new StandardItem(name, 3, StandardType.FIELD, 1, 0, 300, 1, "", "", value, name);
|
||||
}
|
||||
|
||||
/** GROUP 아이템 (type=3, size=1 → 활성) */
|
||||
private static StandardItem group(String name) {
|
||||
return new StandardItem(name, 2, StandardType.GROUP, 1, 0, 0, 1, "", "", null, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* DJB 표준전문 MSG 영역을 모사한 StandardMessage 생성.
|
||||
*
|
||||
* 구조:
|
||||
* MSG (GROUP)
|
||||
* MAIN_MSG (GROUP)
|
||||
* outp_msg_cd, outp_msg_ctnt, outp_msg_desc
|
||||
* MSG_LIST (GRID)
|
||||
* row[n]: outp_msg_cd, outp_msg_ctnt, outp_msg_desc, err_occu_item_nm
|
||||
*
|
||||
* @param listRows null 또는 빈 배열이면 MSG_LIST 행 없음
|
||||
*/
|
||||
private StandardMessage buildMsg(String errCode, String errMsg, String errDesc,
|
||||
String[][] listRows) {
|
||||
StandardMessage msg = new StandardMessage();
|
||||
|
||||
StandardItem msgGroup = group("MSG");
|
||||
|
||||
StandardItem mainMsg = group("MAIN_MSG");
|
||||
mainMsg.addItem(field("outp_msg_cd", errCode));
|
||||
mainMsg.addItem(field("outp_msg_ctnt", errMsg));
|
||||
mainMsg.addItem(field("outp_msg_desc", errDesc));
|
||||
msgGroup.addItem(mainMsg);
|
||||
|
||||
StandardItem msgList = new StandardItem(
|
||||
"MSG_LIST", 2, StandardType.GRID, 0, 0, 0, 1, "", "", null, "MSG_LIST");
|
||||
msgList.addItem(field("outp_msg_cd", ""));
|
||||
msgList.addItem(field("outp_msg_ctnt", ""));
|
||||
msgList.addItem(field("outp_msg_desc", ""));
|
||||
msgList.addItem(field("err_occu_item_nm", ""));
|
||||
|
||||
if (listRows != null) {
|
||||
for (String[] row : listRows) {
|
||||
LinkedHashMap<String, StandardItem> rowMap = new LinkedHashMap<>();
|
||||
rowMap.put("outp_msg_cd", field("outp_msg_cd", get(row, 0)));
|
||||
rowMap.put("outp_msg_ctnt", field("outp_msg_ctnt", get(row, 1)));
|
||||
rowMap.put("outp_msg_desc", field("outp_msg_desc", get(row, 2)));
|
||||
rowMap.put("err_occu_item_nm", field("err_occu_item_nm", get(row, 3)));
|
||||
msgList.addArray(rowMap);
|
||||
}
|
||||
}
|
||||
msgGroup.addItem(msgList);
|
||||
msg.addItem(msgGroup);
|
||||
return msg;
|
||||
}
|
||||
|
||||
private static String get(String[] arr, int idx) {
|
||||
return (arr != null && arr.length > idx) ? arr[idx] : "";
|
||||
}
|
||||
|
||||
private static Properties props(String... keyValues) {
|
||||
Properties p = new Properties();
|
||||
for (int i = 0; i < keyValues.length - 1; i += 2) {
|
||||
p.setProperty(keyValues[i], keyValues[i + 1]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 스칼라 변수 치환 ${path}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("StandardMessage 스칼라 변수 치환 ${path}")
|
||||
class ScalarSubstitution {
|
||||
|
||||
@Test
|
||||
@DisplayName("단순 JSON 템플릿 치환")
|
||||
void 단순_치환() {
|
||||
StandardMessage msg = buildMsg("E001", "오류발생", "상세설명", null);
|
||||
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
|
||||
|
||||
String result = handler.render(template, msg, null);
|
||||
|
||||
assertEquals("{\"code\":\"E001\",\"msg\":\"오류발생\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일 변수 중복 치환")
|
||||
void 동일_변수_중복_치환() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
String template = "${MSG.MAIN_MSG.outp_msg_cd}/${MSG.MAIN_MSG.outp_msg_cd}";
|
||||
|
||||
assertEquals("E001/E001", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 경로는 빈 문자열로 치환")
|
||||
void 없는_경로_빈_문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("", handler.render("${NONE.PATH}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("변수 없는 템플릿은 원본 그대로 반환")
|
||||
void 변수_없는_템플릿_원본() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("{\"static\":\"value\"}", handler.render("{\"static\":\"value\"}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("outp_msg_desc 필드 치환")
|
||||
void outp_msg_desc_치환() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "상세설명입니다", null);
|
||||
|
||||
assertEquals("상세설명입니다", handler.render("${MSG.MAIN_MSG.outp_msg_desc}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("경로:기본값 – 경로 존재 시 값 반환")
|
||||
void 표준전문_기본값_경로존재() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("E001", handler.render("${MSG.MAIN_MSG.outp_msg_cd:DEFAULT}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("경로:기본값 – 경로 없으면 기본값 반환")
|
||||
void 표준전문_기본값_경로없음() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("UNKNOWN", handler.render("${NONE.PATH:UNKNOWN}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("경로:기본값 – 기본값이 빈 문자열")
|
||||
void 표준전문_기본값_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("", handler.render("${NONE.PATH:}", msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// callProp 변수 치환 ${callprop.키}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("callProp 변수 치환 ${callprop.키}")
|
||||
class CallPropSubstitution {
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 값을 템플릿에서 참조")
|
||||
void callProp_단순_참조() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER", "IF_ID", "SVC001");
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.IF_ID}\"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 과 StandardMessage 혼합 참조")
|
||||
void callProp_stdmsg_혼합() {
|
||||
StandardMessage msg = buildMsg("E001", "오류발생", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"code\":\"E001\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 에 없는 키는 빈 문자열")
|
||||
void callProp_없는_키_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = new Properties();
|
||||
|
||||
assertEquals("", handler.render("${callprop.NONE_KEY}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 이 null 이면 빈 문자열")
|
||||
void callProp_null_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("", handler.render("${callprop.ADAPTER_NAME}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 값이 Properties 인 경우 중첩 탐색")
|
||||
void callProp_중첩_Properties() {
|
||||
Properties sub = new Properties();
|
||||
sub.setProperty("IF_ID", "SVC001");
|
||||
sub.setProperty("SVC_NM", "조회서비스");
|
||||
|
||||
Properties callProp = new Properties();
|
||||
callProp.put("OUTBOUND", sub);
|
||||
callProp.setProperty("ADAPTER_NAME", "MY_ADAPTER");
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.OUTBOUND.IF_ID}\",\"svcNm\":\"${callprop.OUTBOUND.SVC_NM}\"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\",\"svcNm\":\"조회서비스\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("중첩 Properties 3단계 탐색")
|
||||
void callProp_3단계_중첩() {
|
||||
Properties level2 = new Properties();
|
||||
level2.setProperty("CODE", "L2CODE");
|
||||
|
||||
Properties level1 = new Properties();
|
||||
level1.put("LEVEL2", level2);
|
||||
|
||||
Properties callProp = new Properties();
|
||||
callProp.put("LEVEL1", level1);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("L2CODE", handler.render("${callprop.LEVEL1.LEVEL2.CODE}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("중간 값이 Properties 가 아니면 전체 keyPath 로 폴백 조회")
|
||||
void callProp_중간값_비Properties_폴백() {
|
||||
Properties callProp = new Properties();
|
||||
callProp.setProperty("A.B", "FALLBACK_VALUE"); // 점 포함 키
|
||||
callProp.setProperty("A", "NOT_PROPS"); // String 값
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
// A 는 String 이므로 Properties 가 아님 → "A.B" 전체 키로 폴백
|
||||
assertEquals("FALLBACK_VALUE", handler.render("${callprop.A.B}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("중첩 Properties 에서 없는 키는 빈 문자열")
|
||||
void callProp_중첩_없는키() {
|
||||
Properties sub = new Properties();
|
||||
sub.setProperty("EXIST", "VALUE");
|
||||
|
||||
Properties callProp = new Properties();
|
||||
callProp.put("SUB", sub);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("", handler.render("${callprop.SUB.NONE_KEY}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callprop.키:기본값 – 키 존재 시 값 반환")
|
||||
void callProp_직접_기본값_키존재() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
|
||||
|
||||
assertEquals("MY_ADAPTER", handler.render("${callprop.ADAPTER_NAME:DEFAULT}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callprop.키:기본값 – 키 없으면 기본값 반환")
|
||||
void callProp_직접_기본값_키없음() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = new Properties();
|
||||
|
||||
assertEquals("DEFAULT_VAL", handler.render("${callprop.NONE_KEY:DEFAULT_VAL}", msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callprop.키:기본값 – callProp null 이면 기본값 반환")
|
||||
void callProp_직접_기본값_null() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("FALLBACK", handler.render("${callprop.ADAPTER_NAME:FALLBACK}", msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// callProp 간접 참조 ${callprop[표준전문경로]} / ${callprop[경로]:기본값}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("callProp 간접 참조 ${callprop[경로]}")
|
||||
class IndirectCallPropSubstitution {
|
||||
|
||||
/** 표준전문에 cp_key 필드가 있는 메시지 */
|
||||
private StandardMessage buildMsgWithKey(String cpKey, String errCode) {
|
||||
StandardMessage msg = buildMsg(errCode, "오류", "", null);
|
||||
// MSG 그룹 아래 cp_key 필드 추가
|
||||
StandardItem msgGroup = msg.findItem("MSG");
|
||||
if (msgGroup != null) {
|
||||
msgGroup.addItem(field("cp_key", cpKey));
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("표준전문 경로 값으로 callProp 키를 결정해 값 조회")
|
||||
void 간접_참조_정상() {
|
||||
StandardMessage msg = buildMsgWithKey("ADAPTER_NAME", "E001");
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
|
||||
String template = "${callprop[MSG.cp_key]}";
|
||||
|
||||
assertEquals("MY_ADAPTER", handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("표준전문 경로 값이 없으면 기본값 반환")
|
||||
void 표준전문_경로_없으면_기본값() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
|
||||
String template = "${callprop[MSG.NONE_KEY]:DEFAULT_ADAPTER}";
|
||||
|
||||
assertEquals("DEFAULT_ADAPTER", handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 에 해당 키 없으면 기본값 반환")
|
||||
void callProp_키_없으면_기본값() {
|
||||
StandardMessage msg = buildMsgWithKey("MISSING_KEY", "E001");
|
||||
Properties callProp = props("OTHER_KEY", "OTHER_VALUE");
|
||||
String template = "${callprop[MSG.cp_key]:FALLBACK}";
|
||||
|
||||
assertEquals("FALLBACK", handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("기본값 미지정 시 빈 문자열")
|
||||
void 기본값_없으면_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER");
|
||||
String template = "${callprop[MSG.NONE_KEY]}";
|
||||
|
||||
assertEquals("", handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 이 null 이면 기본값 반환")
|
||||
void callProp_null_기본값() {
|
||||
StandardMessage msg = buildMsgWithKey("ADAPTER_NAME", "E001");
|
||||
String template = "${callprop[MSG.cp_key]:NO_PROP}";
|
||||
|
||||
assertEquals("NO_PROP", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("기본값이 빈 문자열인 경우")
|
||||
void 기본값이_빈문자열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Properties callProp = new Properties();
|
||||
String template = "${callprop[MSG.NONE_KEY]:}";
|
||||
|
||||
assertEquals("", handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("간접 참조와 직접 참조 혼합")
|
||||
void 간접_직접_혼합() {
|
||||
StandardMessage msg = buildMsgWithKey("IF_ID_KEY", "E001");
|
||||
Properties callProp = props("IF_ID_KEY", "SVC_001", "ADAPTER_NAME", "MY_ADAPTER");
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop[MSG.cp_key]}\"}";
|
||||
|
||||
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC_001\"}", handler.render(template, msg, callProp));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 배열 반복 {{#foreach path}}...{{/foreach}}
|
||||
// ================================================================
|
||||
|
||||
// ================================================================
|
||||
// DATA 영역 변수 치환 ${DATA.xxx}
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("DATA 영역 변수 치환 ${DATA.xxx}")
|
||||
class DataSectionSubstitution {
|
||||
|
||||
/**
|
||||
* DATA > BIZDATA(FIELD, JSON String value) 구조의 StandardMessage 생성.
|
||||
* BIZDATA 는 파싱 없이 JSON 문자열 그대로 세팅되는 형태.
|
||||
*/
|
||||
private StandardMessage buildMsgWithBizData(String bizDataJson) {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
StandardItem dataGroup = group("DATA");
|
||||
dataGroup.addItem(field("BIZDATA", bizDataJson));
|
||||
msg.addItem(dataGroup);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA.BIZDATA JSON 필드 치환 – 단일 depth")
|
||||
void DATA_BIZDATA_JSON_필드_치환() {
|
||||
StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"1234567890\",\"custNm\":\"홍길동\"}");
|
||||
String template = "{\"acctNo\":\"${DATA.BIZDATA.acctNo}\",\"custNm\":\"${DATA.BIZDATA.custNm}\"}";
|
||||
|
||||
assertEquals("{\"acctNo\":\"1234567890\",\"custNm\":\"홍길동\"}",
|
||||
handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA.BIZDATA JSON 필드 치환 – 중첩 depth")
|
||||
void DATA_BIZDATA_JSON_중첩_필드_치환() {
|
||||
StandardMessage msg = buildMsgWithBizData("{\"addr\":{\"city\":\"서울\",\"zip\":\"12345\"}}");
|
||||
|
||||
assertEquals("서울", handler.render("${DATA.BIZDATA.addr.city}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA.BIZDATA 원본 JSON 문자열 치환")
|
||||
void DATA_BIZDATA_원본_치환() {
|
||||
String rawJson = "{\"acctNo\":\"1234567890\"}";
|
||||
StandardMessage msg = buildMsgWithBizData(rawJson);
|
||||
|
||||
assertEquals(rawJson, handler.render("${DATA.BIZDATA}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA.BIZDATA 와 MSG 영역 혼합 치환")
|
||||
void DATA_MSG_혼합_치환() {
|
||||
StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"9876543210\"}");
|
||||
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"acctNo\":\"${DATA.BIZDATA.acctNo}\"}";
|
||||
|
||||
assertEquals("{\"code\":\"E001\",\"acctNo\":\"9876543210\"}",
|
||||
handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA.BIZDATA JSON 에 없는 필드는 빈 문자열")
|
||||
void DATA_BIZDATA_없는_JSON_필드_빈문자열() {
|
||||
StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"1234567890\"}");
|
||||
|
||||
assertEquals("", handler.render("${DATA.BIZDATA.noneField}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA.BIZDATA JSON 에 없는 필드에 기본값 지정")
|
||||
void DATA_BIZDATA_없는_JSON_필드_기본값() {
|
||||
StandardMessage msg = buildMsgWithBizData("{\"acctNo\":\"1234567890\"}");
|
||||
|
||||
assertEquals("UNKNOWN", handler.render("${DATA.BIZDATA.noneField:UNKNOWN}", msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATA 영역 미설정 시 기본값 반환")
|
||||
void DATA_없으면_기본값() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
|
||||
assertEquals("{}", handler.render("${DATA.BIZDATA:{}}", msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("배열 반복 블록 {{#foreach}}")
|
||||
class ForeachBlock {
|
||||
|
||||
@Test
|
||||
@DisplayName("MSG_LIST 1건 렌더링")
|
||||
void 단건_렌더링() {
|
||||
StandardMessage msg = buildMsg("E001", "대표오류", "",
|
||||
new String[][]{{"E001", "메시지A", "설명A", "fieldA"}});
|
||||
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\",\"m\":\"${outp_msg_ctnt}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[{\"c\":\"E001\",\"m\":\"메시지A\"}]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MSG_LIST 2건 → 쉼표로 연결")
|
||||
void 다건_쉼표_연결() {
|
||||
StandardMessage msg = buildMsg("E001", "대표오류", "",
|
||||
new String[][]{
|
||||
{"E001", "오류A", "설명A", "fieldA"},
|
||||
{"E002", "오류B", "설명B", "fieldB"}
|
||||
});
|
||||
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[{\"c\":\"E001\"},{\"c\":\"E002\"}]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MSG_LIST 가 비어있으면 foreach 결과는 빈 문자열")
|
||||
void 빈_배열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", new String[][]{});
|
||||
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("foreach 경로가 없으면 빈 문자열")
|
||||
void 없는_경로() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
String template = "[{{#foreach NONE.LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
|
||||
|
||||
assertEquals("[]", handler.render(template, msg, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("err_occu_item_nm 필드 포함 렌더링")
|
||||
void err_occu_item_nm_포함() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "",
|
||||
new String[][]{{"E001", "오류A", "설명A", "accountNo"}});
|
||||
String template = "{{#foreach MSG.MSG_LIST}}{\"field\":\"${err_occu_item_nm}\"}{{/foreach}}";
|
||||
|
||||
assertEquals("{\"field\":\"accountNo\"}", handler.render(template, msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 혼합 템플릿 (스칼라 + callProp + 배열)
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("혼합 템플릿")
|
||||
class MixedTemplate {
|
||||
|
||||
@Test
|
||||
@DisplayName("JSON – MAIN_MSG + callProp + MSG_LIST 2건")
|
||||
void JSON_전체_혼합() {
|
||||
StandardMessage msg = buildMsg("E001", "대표오류", "대표설명",
|
||||
new String[][]{
|
||||
{"E001", "오류A", "설명A", "fieldA"},
|
||||
{"E002", "오류B", "설명B", "fieldB"}
|
||||
});
|
||||
Properties callProp = props("ADAPTER_NAME", "TEST_ADAPTER");
|
||||
|
||||
String template =
|
||||
"{" +
|
||||
"\"adapter\":\"${callprop.ADAPTER_NAME}\"," +
|
||||
"\"resultCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
|
||||
"\"resultMsg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"," +
|
||||
"\"errors\":[{{#foreach MSG.MSG_LIST}}" +
|
||||
"{\"code\":\"${outp_msg_cd}\",\"msg\":\"${outp_msg_ctnt}\",\"field\":\"${err_occu_item_nm}\"}" +
|
||||
"{{/foreach}}]" +
|
||||
"}";
|
||||
|
||||
String result = handler.render(template, msg, callProp);
|
||||
|
||||
assertEquals(
|
||||
"{\"adapter\":\"TEST_ADAPTER\"," +
|
||||
"\"resultCode\":\"E001\",\"resultMsg\":\"대표오류\"," +
|
||||
"\"errors\":[{\"code\":\"E001\",\"msg\":\"오류A\",\"field\":\"fieldA\"}," +
|
||||
"{\"code\":\"E002\",\"msg\":\"오류B\",\"field\":\"fieldB\"}]}",
|
||||
result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("XML – 스칼라 + callProp 치환")
|
||||
void XML_스칼라_callProp() {
|
||||
StandardMessage msg = buildMsg("E001", "오류발생", "", null);
|
||||
Properties callProp = props("IF_ID", "IF_001");
|
||||
String template = "<error><ifId>${callprop.IF_ID}</ifId><code>${MSG.MAIN_MSG.outp_msg_cd}</code></error>";
|
||||
|
||||
assertEquals("<error><ifId>IF_001</ifId><code>E001</code></error>",
|
||||
handler.render(template, msg, callProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("XML – foreach 배열")
|
||||
void XML_foreach_배열() {
|
||||
StandardMessage msg = buildMsg("E001", "오류", "",
|
||||
new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}});
|
||||
String template =
|
||||
"<errors>{{#foreach MSG.MSG_LIST}}" +
|
||||
"<item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>" +
|
||||
"{{/foreach}}</errors>";
|
||||
|
||||
assertEquals(
|
||||
"<errors>" +
|
||||
"<item><code>E001</code><msg>오류A</msg></item>," +
|
||||
"<item><code>E002</code><msg>오류B</msg></item>" +
|
||||
"</errors>",
|
||||
handler.render(template, msg, null));
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// generateNonStandardErrorResponseMessage – PropManager mock
|
||||
// ================================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("generateNonStandardErrorResponseMessage")
|
||||
class GenerateMethod {
|
||||
|
||||
private void injectMockPropManager(PropManager mockPropManager) throws Exception {
|
||||
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
|
||||
Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager);
|
||||
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
|
||||
ctxField.setAccessible(true);
|
||||
ctxField.set(null, mockCtx);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("템플릿 미설정 시 null 반환")
|
||||
void 템플릿_없으면_null() throws Exception {
|
||||
PropManager mockProp = Mockito.mock(PropManager.class);
|
||||
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("빈 템플릿 시 null 반환")
|
||||
void 빈_템플릿_null() throws Exception {
|
||||
PropManager mockProp = Mockito.mock(PropManager.class);
|
||||
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" ");
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("템플릿 설정 시 StandardMessage 치환 결과 반환")
|
||||
void 템플릿_stdmsg_치환() throws Exception {
|
||||
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
|
||||
PropManager mockProp = Mockito.mock(PropManager.class);
|
||||
Mockito.when(mockProp.getProperty(
|
||||
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
|
||||
eq("MY_GROUP.template"))).thenReturn(template);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
assertEquals("{\"code\":\"E999\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("callProp 값이 템플릿에 치환되어 반환")
|
||||
void 템플릿_callProp_치환() throws Exception {
|
||||
String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}";
|
||||
PropManager mockProp = Mockito.mock(PropManager.class);
|
||||
Mockito.when(mockProp.getProperty(
|
||||
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
|
||||
eq("MY_GROUP.template"))).thenReturn(template);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
|
||||
Properties callProp = props("ADAPTER_NAME", "REAL_ADAPTER");
|
||||
|
||||
Object result = handler.generateNonStandardErrorResponseMessage(
|
||||
"MY_GROUP", "MY_ADAPTER", callProp, null, msg);
|
||||
|
||||
assertEquals("{\"adapter\":\"REAL_ADAPTER\",\"code\":\"E999\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("템플릿 키는 {adapterGroupId}.template 형식으로 조회")
|
||||
void 템플릿_키_형식_검증() throws Exception {
|
||||
PropManager mockProp = Mockito.mock(PropManager.class);
|
||||
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
|
||||
injectMockPropManager(mockProp);
|
||||
|
||||
StandardMessage msg = buildMsg("E001", "오류", "", null);
|
||||
handler.generateNonStandardErrorResponseMessage(
|
||||
"SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, msg);
|
||||
|
||||
Mockito.verify(mockProp).getProperty(
|
||||
TemplateAdapterErrorMsgHandler.PROP_GROUP,
|
||||
"SAMPLE_GROUP.template");
|
||||
}
|
||||
}
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
import com.eactive.eai.common.security.CryptoModuleService;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* OutCryptoFilter 단위 테스트.
|
||||
*
|
||||
* doPreFilter = 외부로 보내는 요청 암호화 (InCryptoFilter와 반대 방향)
|
||||
* doPostFilter = 외부에서 받은 응답 복호화
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class OutCryptoFilterTest {
|
||||
|
||||
private static final String MODULE = "TEST_MODULE";
|
||||
private static final String PLAIN_STR = "plain-request-body";
|
||||
private static final byte[] PLAIN = PLAIN_STR.getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CIPHER = new byte[]{0x10, 0x20, 0x30, 0x40, 0x50, 0x60};
|
||||
private static final String CIPHER_B64 = Base64.getEncoder().encodeToString(CIPHER);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static CryptoModuleService mockService;
|
||||
private static OutCryptoFilter filter;
|
||||
|
||||
private Properties tempProp;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockService = mock(CryptoModuleService.class);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
filter = new OutCryptoFilter();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
reset(mockService);
|
||||
tempProp = new Properties();
|
||||
|
||||
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(PLAIN);
|
||||
when(mockService.encrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(CIPHER);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. buildAad
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. buildAad — CRYPTO_AAD_PROP 미설정 → null")
|
||||
void testBuildAad_noProp_returnsNull() {
|
||||
assertNull(filter.buildAad(new Properties(), tempProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. buildAad — CRYPTO_AAD_PROP 설정, tempProp에 값 없음 → null")
|
||||
void testBuildAad_propSet_noTempValue_returnsNull() {
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(OutCryptoFilter.PROP_AAD_PROP, "aadKey");
|
||||
|
||||
assertNull(filter.buildAad(prop, tempProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. buildAad — CRYPTO_AAD_PROP 설정, tempProp에 값 있음 → UTF-8 바이트 반환")
|
||||
void testBuildAad_propSet_tempValuePresent_returnsByte() {
|
||||
String aadVal = "aad-value-xyz";
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(OutCryptoFilter.PROP_AAD_PROP, "aadKey");
|
||||
tempProp.setProperty("aadKey", aadVal);
|
||||
|
||||
assertArrayEquals(aadVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, tempProp));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. doPreFilter — 암호화 (요청을 외부로 보내기 전)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. BODY scope — 평문 암호화 → Base64 반환")
|
||||
void testPreFilter_body_encrypt() throws Exception {
|
||||
Object result = filter.doPreFilter("g", "a", bodyEncryptProps(), PLAIN_STR, tempProp);
|
||||
|
||||
assertEquals(CIPHER_B64, result);
|
||||
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(PLAIN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. FIELD scope, fromPath='/' — body 전체 암호화 → toPath 필드명으로 래핑")
|
||||
void testPreFilter_field_encryptFromRoot() throws Exception {
|
||||
String body = "{\"userId\":\"U001\"}";
|
||||
Properties prop = fieldEncryptProps("/", "/encryptedData");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", prop, body, tempProp);
|
||||
|
||||
assertEquals("{\"encryptedData\":\"" + CIPHER_B64 + "\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
|
||||
void testPreFilter_field_encryptSpecificPath() throws Exception {
|
||||
String body = "{\"data\":\"secret\",\"enc\":\"\"}";
|
||||
Properties prop = fieldEncryptProps("/data", "/enc");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", prop, body, tempProp);
|
||||
|
||||
assertTrue(result.toString().contains("\"enc\":\"" + CIPHER_B64 + "\""),
|
||||
"toPath에 Base64 암호문이 반영되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4. FIELD scope — fromPath 값 없음 → body 그대로 반환")
|
||||
void testPreFilter_field_missingFromPath_passThrough() throws Exception {
|
||||
String body = "{\"other\":\"value\"}";
|
||||
Properties prop = fieldEncryptProps("/data", "/enc");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", prop, body, tempProp);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-5. CRYPTO_MODULE_NAME 누락 — IllegalArgumentException")
|
||||
void testPreFilter_missingModuleName_throwsException() {
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> filter.doPreFilter("g", "a", prop, PLAIN_STR, tempProp));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. doPostFilter — 복호화 (외부로부터 응답 받은 후)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. BODY scope — Base64 암호문 복호화 → 평문 반환")
|
||||
void testPostFilter_body_decrypt() throws Exception {
|
||||
Object result = filter.doPostFilter("g", "a", bodyDecryptProps(), CIPHER_B64, tempProp);
|
||||
|
||||
assertEquals(PLAIN_STR, result);
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. FIELD scope — fromPath 복호화 → toPath 반영")
|
||||
void testPostFilter_field_decryptToPath() throws Exception {
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/result");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", prop, body, tempProp);
|
||||
|
||||
assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
|
||||
"toPath에 복호화된 값이 반영되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. FIELD scope, toPath='/' — 복호화된 텍스트로 body 전체 교체")
|
||||
void testPostFilter_field_decryptToRoot() throws Exception {
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", prop, body, tempProp);
|
||||
|
||||
assertEquals(PLAIN_STR, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-4. FIELD scope — fromPath 값 없음 → body 그대로 반환")
|
||||
void testPostFilter_field_missingFromPath_passThrough() throws Exception {
|
||||
String body = "{\"other\":\"value\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/result");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", prop, body, tempProp);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. AAD (tempProp 경유)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. doPreFilter BODY — CRYPTO_AAD_PROP 설정 시 tempProp 값을 AAD로 전달")
|
||||
void testPreFilter_body_aadFromTempProp() throws Exception {
|
||||
String aadVal = "out-aad-value";
|
||||
Properties prop = bodyEncryptProps();
|
||||
prop.setProperty(OutCryptoFilter.PROP_AAD_PROP, "myAadKey");
|
||||
tempProp.setProperty("myAadKey", aadVal);
|
||||
|
||||
filter.doPreFilter("g", "a", prop, PLAIN_STR, tempProp);
|
||||
|
||||
verify(mockService).encrypt(eq(MODULE), anyMap(),
|
||||
eq(aadVal.getBytes(StandardCharsets.UTF_8)), any(byte[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. doPostFilter BODY — CRYPTO_AAD_PROP 미설정 시 aad=null로 전달")
|
||||
void testPostFilter_body_noAadProp_nullAad() throws Exception {
|
||||
filter.doPostFilter("g", "a", bodyDecryptProps(), CIPHER_B64, tempProp);
|
||||
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), any(byte[].class));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private Properties bodyEncryptProps() {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties bodyDecryptProps() {
|
||||
return bodyEncryptProps();
|
||||
}
|
||||
|
||||
private Properties fieldEncryptProps(String fromPath, String toPath) {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
|
||||
p.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, fromPath);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, toPath);
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties fieldDecryptProps(String fromPath, String toPath) {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
|
||||
p.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, fromPath);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, toPath);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.InCryptoFilter;
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.hsm.HsmManager;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.security.CryptoModuleConfigVO;
|
||||
import com.eactive.eai.common.security.CryptoModuleManager;
|
||||
import com.eactive.eai.common.security.CryptoModuleService;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy;
|
||||
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
|
||||
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
|
||||
/**
|
||||
* CryptoFilter HSM 연동 통합 테스트 (SoftHSM2 / SunPKCS11)
|
||||
*
|
||||
* SoftHSM2에서 MASTER_KEY를 조회하여 키를 도출한 뒤
|
||||
* InCryptoFilter 의 암복호화 전체 흐름을 검증한다.
|
||||
*
|
||||
* 사전 조건:
|
||||
* C:\SoftHSM2 에 SoftHSM2 설치 (미설치 시 자동 건너뜀)
|
||||
* MASTER_KEY alias는 없으면 자동 생성됨
|
||||
*
|
||||
* 검증 전략:
|
||||
* HSM → 키 도출 → CryptoModuleExtension → InCryptoFilter doPostFilter(암호화) → doPreFilter(복호화) → 원문 일치
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
public class CryptoFilterHsmIntegrationTest {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 상수
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
|
||||
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
|
||||
private static final String TOKEN_LABEL = "eapim-test";
|
||||
private static final String PIN = "1234";
|
||||
private static final String MASTER_KEY_ALIAS = "MASTER_KEY";
|
||||
|
||||
/** HsmKeyDerivationStrategy: HSM 마스터키를 파생 없이 직접 사용 */
|
||||
private static final String MOD_HSM_GCM = "HSM_GCM";
|
||||
/** HsmContextSha256KeyDerivationStrategy: HSM 마스터키 + 컨텍스트값 SHA-256 파생 */
|
||||
private static final String MOD_CTX_SHA_GCM = "CTX_SHA256_GCM";
|
||||
|
||||
private static final String CTX_KEY = "X-Api-Group-Seq";
|
||||
private static final String CTX_VAL_A = "GROUP-A";
|
||||
private static final String CTX_VAL_B = "GROUP-B";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 정적 필드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static GenericApplicationContext springCtx;
|
||||
private static HsmManager hsmManager;
|
||||
private static File tempCfgFile;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 내부 클래스: runtimeContext를 고정값으로 반환하는 필터
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static class ContextAwareCryptoFilter extends InCryptoFilter {
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
ContextAwareCryptoFilter(String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
|
||||
Map<String, String> ctx = new HashMap<>();
|
||||
ctx.put(key, value);
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 인스턴스 필드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private HttpServletRequest mockReq;
|
||||
private HttpServletResponse mockRes;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 전체 설정 / 해제
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@BeforeAll
|
||||
static void setUpAll() throws Exception {
|
||||
assumeTrue(new File(SOFTHSM2_DLL).exists(),
|
||||
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
|
||||
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
|
||||
|
||||
// 1. 토큰 초기화
|
||||
ensureTokenInitialized();
|
||||
|
||||
// 2. PKCS11 설정 문자열
|
||||
// attributes(*,...): generate/import 모두 CKA_SENSITIVE=false → getEncoded() 사용 가능
|
||||
String cfgContent = "name = SoftHSM\n"
|
||||
+ "library = " + SOFTHSM2_DLL + "\n"
|
||||
+ "slotListIndex = 0\n"
|
||||
+ "attributes(*, CKO_SECRET_KEY, CKK_AES) = {\n"
|
||||
+ " CKA_SENSITIVE = false\n"
|
||||
+ " CKA_EXTRACTABLE = true\n"
|
||||
+ "}\n";
|
||||
tempCfgFile = File.createTempFile("pkcs11-filter-it-", ".cfg");
|
||||
Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));
|
||||
|
||||
// 3. Mock PropManager (DB 없이 HSM 설정값 제공)
|
||||
PropManager mockPropManager = mock(PropManager.class);
|
||||
when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent);
|
||||
when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN);
|
||||
when(mockPropManager.getProperty(eq("HSM"), eq("CACHE_RELOAD_YN"), anyString())).thenReturn("N");
|
||||
|
||||
// 4. HsmManager (private 생성자 → 리플렉션)
|
||||
Constructor<HsmManager> hsmCtor = HsmManager.class.getDeclaredConstructor();
|
||||
hsmCtor.setAccessible(true);
|
||||
hsmManager = hsmCtor.newInstance();
|
||||
|
||||
// 5. CryptoModuleManager (private 생성자 → 리플렉션)
|
||||
CryptoModuleConfigLoader mockLoader = mock(CryptoModuleConfigLoader.class);
|
||||
when(mockLoader.findAll()).thenReturn(buildModuleConfigs());
|
||||
|
||||
Constructor<CryptoModuleManager> managerCtor = CryptoModuleManager.class.getDeclaredConstructor();
|
||||
managerCtor.setAccessible(true);
|
||||
CryptoModuleManager manager = managerCtor.newInstance();
|
||||
|
||||
CryptoModuleService cryptoService = new CryptoModuleService();
|
||||
HsmCryptoService hsmCryptoService = new HsmCryptoService();
|
||||
|
||||
// 6. Spring ApplicationContext 구성
|
||||
springCtx = new GenericApplicationContext();
|
||||
springCtx.getBeanFactory().registerSingleton("propManager", mockPropManager);
|
||||
springCtx.getBeanFactory().registerSingleton("hsmManager", hsmManager);
|
||||
springCtx.getBeanFactory().registerSingleton("hsmCryptoService", hsmCryptoService);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleService", cryptoService);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
|
||||
springCtx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
springCtx.refresh();
|
||||
|
||||
// 7. HsmManager 시작 (SunPKCS11 Provider 초기화 + KeyStore 오픈)
|
||||
hsmManager.start();
|
||||
assertTrue(hsmManager.isReady(), "HsmManager 초기화 실패");
|
||||
System.out.println("[HSM] Provider: " + hsmManager.getPkcs11Provider().getName());
|
||||
|
||||
// 8. MASTER_KEY 등록 (없으면 자동 생성)
|
||||
ensureMasterKey();
|
||||
|
||||
// 9. CryptoModuleManager 시작 (모듈 설정 로드)
|
||||
manager.start();
|
||||
System.out.println("[Crypto] 모듈 로드 완료");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownAll() throws Exception {
|
||||
if (hsmManager != null && hsmManager.isStarted()) hsmManager.stop();
|
||||
if (springCtx != null) springCtx.close();
|
||||
if (tempCfgFile != null) tempCfgFile.delete();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockReq = mock(HttpServletRequest.class);
|
||||
mockRes = mock(HttpServletResponse.class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. HsmKeyDerivationStrategy — HSM 마스터키 직접 사용 (InCryptoFilter)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. FIELD/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
|
||||
void testHsmGcm_field_roundTrip() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
// SCOPE 기본값 FIELD, 경로 기본값 사용
|
||||
|
||||
// 암호화: body 전체 → /encrypted_data 필드
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
System.out.println("[1-1] encrypted: " + encrypted);
|
||||
assertTrue(encrypted.contains("encrypted_data"), "/encrypted_data 필드가 있어야 한다");
|
||||
|
||||
// 복호화: /encrypted_data 필드 → body 전체 교체
|
||||
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[1-1] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. BODY/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
|
||||
void testHsmGcm_body_roundTrip() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
|
||||
|
||||
// 암호화: body 전체 → Base64 암호문
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
System.out.println("[1-2] encrypted(Base64): " + encrypted);
|
||||
assertNotEquals(original, encrypted, "암호문은 평문과 달라야 한다");
|
||||
|
||||
// 복호화: Base64 암호문 → 원문
|
||||
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[1-2] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. FIELD/GCM HsmKey + AAD 헤더 — 동일 요청 ID로 암복호화 성공")
|
||||
void testHsmGcm_field_withAad_success() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"amount\":\"500000\"}";
|
||||
String requestId = "REQ-20240101-001";
|
||||
|
||||
when(mockReq.getHeader("X-Request-Id")).thenReturn(requestId);
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id");
|
||||
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
String decrypted = (String) filter.doPreFilter( "G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[1-3] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. FIELD/GCM HsmKey + AAD 헤더 — 다른 요청 ID로 복호화 시 GCM 인증 실패")
|
||||
void testHsmGcm_field_withAad_wrongAad_fails() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"amount\":\"500000\"}";
|
||||
|
||||
// 암호화 시 요청 ID
|
||||
HttpServletRequest encReq = mock(HttpServletRequest.class);
|
||||
when(encReq.getHeader("X-Request-Id")).thenReturn("REQ-CORRECT");
|
||||
|
||||
// 복호화 시 다른 요청 ID
|
||||
HttpServletRequest decReq = mock(HttpServletRequest.class);
|
||||
when(decReq.getHeader("X-Request-Id")).thenReturn("REQ-WRONG");
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id");
|
||||
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, encReq, mockRes);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> filter.doPreFilter("G", "A", encrypted, prop, decReq, mockRes),
|
||||
"잘못된 AAD로 GCM 복호화 시 예외가 발생해야 한다");
|
||||
System.out.println("[1-4] 다른 AAD 복호화 예외 확인 완료");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. HsmContextSha256KeyDerivationStrategy — 컨텍스트 기반 SHA-256 파생 키
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. FIELD/GCM ContextSha256 — 동일 컨텍스트로 암복호화 라운드트립")
|
||||
void testCtxSha256Gcm_field_roundTrip() throws Exception {
|
||||
InCryptoFilter filter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
|
||||
String original = "{\"accNo\":\"0011234567890\",\"amount\":\"50000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
|
||||
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
System.out.println("[2-1] encrypted: " + encrypted);
|
||||
assertTrue(encrypted.contains("encrypted_data"));
|
||||
|
||||
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[2-1] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. FIELD/GCM ContextSha256 — 암호화와 다른 컨텍스트로 복호화 시 GCM 인증 실패")
|
||||
void testCtxSha256Gcm_differentContext_throwsException() throws Exception {
|
||||
InCryptoFilter encFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
|
||||
InCryptoFilter decFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
|
||||
String original = "{\"data\":\"sensitive\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
|
||||
|
||||
String encrypted = (String) encFilter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> decFilter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes),
|
||||
"다른 컨텍스트 키로 파생된 키는 복호화에 실패해야 한다");
|
||||
System.out.println("[2-2] 다른 컨텍스트 복호화 예외 확인 완료");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. FIELD/GCM ContextSha256 — 컨텍스트별 독립 암복호화 (GROUP-A, GROUP-B 각각 성공)")
|
||||
void testCtxSha256Gcm_perGroupRoundTrip() throws Exception {
|
||||
InCryptoFilter filterA = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
|
||||
InCryptoFilter filterB = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
|
||||
String plainA = "{\"group\":\"A\",\"amount\":\"1000\"}";
|
||||
String plainB = "{\"group\":\"B\",\"amount\":\"2000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
|
||||
|
||||
String encA = (String) filterA.doPostFilter("G", "A", plainA, prop, mockReq, mockRes);
|
||||
String encB = (String) filterB.doPostFilter("G", "B", plainB, prop, mockReq, mockRes);
|
||||
|
||||
assertEquals(plainA, (String) filterA.doPreFilter("G", "A", encA, prop, mockReq, mockRes));
|
||||
assertEquals(plainB, (String) filterB.doPreFilter("G", "B", encB, prop, mockReq, mockRes));
|
||||
System.out.println("[2-3] GROUP-A, GROUP-B 각각 독립 암복호화 성공");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private static List<CryptoModuleConfig> buildModuleConfigs() {
|
||||
return Arrays.asList(
|
||||
buildDynamic(MOD_HSM_GCM, "AES", "GCM", "NoPadding",
|
||||
HsmKeyDerivationStrategy.class.getName(),
|
||||
"{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\"}"),
|
||||
buildDynamic(MOD_CTX_SHA_GCM, "AES", "GCM", "NoPadding",
|
||||
HsmContextSha256KeyDerivationStrategy.class.getName(),
|
||||
"{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\",\"contextKey\":\"" + CTX_KEY + "\"}")
|
||||
);
|
||||
}
|
||||
|
||||
private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding,
|
||||
String strategy, String params) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setKeySourceType("DYNAMIC");
|
||||
c.setKeyDerivStrategy(strategy);
|
||||
c.setKeyDerivParams(params);
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(60);
|
||||
c.setUseYn("Y");
|
||||
// ivHex = null: GCM에서 IV를 AAD 대체값으로 사용하지 않도록 의도적으로 미설정
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void ensureMasterKey() throws Exception {
|
||||
java.security.KeyStore ks = hsmManager.getKeyStore();
|
||||
|
||||
// 이전 실행 키 삭제 (sensitive 여부 무관하게 항상 고정키로 재등록)
|
||||
if (ks.containsAlias(MASTER_KEY_ALIAS)) {
|
||||
ks.deleteEntry(MASTER_KEY_ALIAS);
|
||||
System.out.println("[HSM] 기존 MASTER_KEY 삭제");
|
||||
}
|
||||
|
||||
// CryptoModuleServiceTest.KEY_128 과 동일한 고정 키 → 두 테스트 간 암호문 비교 가능
|
||||
// attributes(*, CKO_SECRET_KEY, CKK_AES) 지시어로 임포트 시 CKA_SENSITIVE=false 적용
|
||||
byte[] keyBytes = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
SecretKey fixedKey = new SecretKeySpec(keyBytes, "AES");
|
||||
ks.setKeyEntry(MASTER_KEY_ALIAS, fixedKey, null, null);
|
||||
|
||||
// 임포트 후 getEncoded() 검증
|
||||
SecretKey stored = (SecretKey) ks.getKey(MASTER_KEY_ALIAS, null);
|
||||
byte[] encoded = stored.getEncoded();
|
||||
System.out.println("[HSM] MASTER_KEY 등록 완료: alias=" + MASTER_KEY_ALIAS
|
||||
+ " / getEncoded()=" + (encoded != null ? encoded.length + "bytes, " + new String(encoded) : "null (추출 불가!)"));
|
||||
}
|
||||
|
||||
private static void ensureTokenInitialized() throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
SOFTHSM2_UTIL, "--init-token", "--free",
|
||||
"--label", TOKEN_LABEL, "--pin", PIN, "--so-pin", PIN);
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
String out = readOutput(p);
|
||||
p.waitFor(10, TimeUnit.SECONDS);
|
||||
System.out.println("[SoftHSM2] init-token: " + out.trim());
|
||||
}
|
||||
|
||||
private static CryptoModuleConfigMapper testMapper() {
|
||||
return new CryptoModuleConfigMapper() {
|
||||
@Override
|
||||
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
|
||||
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
|
||||
vo.setCryptoId(e.getCryptoId());
|
||||
vo.setCryptoName(e.getCryptoName());
|
||||
vo.setCryptoDesc(e.getCryptoDesc());
|
||||
vo.setAlgType(e.getAlgType());
|
||||
vo.setCipherMode(e.getCipherMode());
|
||||
vo.setPadding(e.getPadding());
|
||||
vo.setIvHex(e.getIvHex());
|
||||
vo.setKeySourceType(e.getKeySourceType());
|
||||
vo.setEncKeyHex(e.getEncKeyHex());
|
||||
vo.setDecKeyHex(e.getDecKeyHex());
|
||||
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
|
||||
vo.setKeyDerivParams(e.getKeyDerivParams());
|
||||
vo.setCacheYn(e.getCacheYn());
|
||||
vo.setCacheTtlSec(e.getCacheTtlSec());
|
||||
vo.setUseYn(e.getUseYn());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String readOutput(Process p) throws Exception {
|
||||
InputStream is = p.getInputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len;
|
||||
while ((len = is.read(buf)) != -1) {
|
||||
sb.append(new String(buf, 0, len, "UTF-8"));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
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.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
import com.eactive.eai.common.security.CryptoModuleService;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* InCryptoFilter / AbstractCryptoFilter 단위 테스트.
|
||||
* CryptoModuleService는 Mock으로 대체하여 필터 로직(Base64, JSON 경로, 프로퍼티 파싱)만 검증한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class InCryptoFilterTest {
|
||||
|
||||
private static final String MODULE = "TEST_MODULE";
|
||||
private static final String PLAIN_STR = "decrypted-plain-text";
|
||||
private static final byte[] PLAIN = PLAIN_STR.getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CIPHER = new byte[]{0x10, 0x20, 0x30, 0x40, 0x50, 0x60};
|
||||
private static final String CIPHER_B64 = Base64.getEncoder().encodeToString(CIPHER);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static CryptoModuleService mockService;
|
||||
private static InCryptoFilter filter;
|
||||
|
||||
private HttpServletRequest mockRequest;
|
||||
private HttpServletResponse mockResponse;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockService = mock(CryptoModuleService.class);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
filter = new InCryptoFilter();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
reset(mockService);
|
||||
mockRequest = mock(HttpServletRequest.class);
|
||||
mockResponse = mock(HttpServletResponse.class);
|
||||
|
||||
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(PLAIN);
|
||||
when(mockService.encrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(CIPHER);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. buildRuntimeContext
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. buildRuntimeContext — 항상 빈 Map 반환")
|
||||
void testBuildRuntimeContext_returnsEmptyMap() {
|
||||
Map<String, String> result = filter.buildRuntimeContext(new Properties(), mockRequest);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. buildAad
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. buildAad — CRYPTO_AAD_HEADER 미설정 → null")
|
||||
void testBuildAad_noProp_returnsNull() {
|
||||
assertNull(filter.buildAad(new Properties(), mockRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. buildAad — 헤더명 설정, 헤더값 없음 → null")
|
||||
void testBuildAad_headerNameSet_noValue_returnsNull() {
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
|
||||
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(null);
|
||||
|
||||
assertNull(filter.buildAad(prop, mockRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. buildAad — 헤더명 설정, 헤더값 있음 → UTF-8 바이트 반환")
|
||||
void testBuildAad_headerNameSet_valuePresent_returnsByte() {
|
||||
String headerVal = "req-id-abc123";
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
|
||||
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(headerVal);
|
||||
|
||||
assertArrayEquals(headerVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, mockRequest));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. doPreFilter — BODY scope 복호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. BODY scope — Base64 암호문 복호화 → 평문 반환")
|
||||
void testPreFilter_body_decrypt_roundTrip() throws Exception {
|
||||
Object result = filter.doPreFilter("g", "a", CIPHER_B64, bodyDecryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(PLAIN_STR, result);
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. CRYPTO_MODULE_NAME 누락 — IllegalArgumentException")
|
||||
void testPreFilter_missingModuleName_throwsException() {
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. message가 byte[] — UTF-8 디코딩 후 처리")
|
||||
void testPreFilter_body_byteArrayMessage() throws Exception {
|
||||
Object result = filter.doPreFilter("g", "a", CIPHER_B64.getBytes(StandardCharsets.UTF_8),
|
||||
bodyDecryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(PLAIN_STR, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. doPreFilter — FIELD scope 복호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. FIELD scope — fromPath 복호화 → toPath 반영")
|
||||
void testPreFilter_field_decryptToPath() throws Exception {
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/result");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
|
||||
"toPath에 복호화된 값이 반영되어야 한다");
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. FIELD scope, toPath='/' — 복호화 결과가 비-JSON이면 텍스트 그대로 반환 (fallback)")
|
||||
void testPreFilter_field_decryptToRoot_nonJsonFallback() throws Exception {
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(PLAIN_STR, result, "복호화 결과가 JSON 객체가 아니면 텍스트 그대로 반환");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4. FIELD scope, toPath='/' — 복호화된 JSON을 body에 병합 (fromPath 필드 제거)")
|
||||
void testPreFilter_field_decryptToRoot_mergesJson() throws Exception {
|
||||
String decryptedJson = "{\"userId\":\"U001\",\"name\":\"test\"}";
|
||||
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class)))
|
||||
.thenReturn(decryptedJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String body = "{\"encryptedData\":\"" + CIPHER_B64 + "\",\"requestId\":\"REQ001\"}";
|
||||
Properties prop = fieldDecryptProps("/encryptedData", "/");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
String r = result.toString();
|
||||
assertFalse(r.contains("encryptedData"), "암호화 필드(encryptedData)는 제거되어야 한다");
|
||||
assertTrue(r.contains("\"requestId\":\"REQ001\""), "원본 body의 다른 필드는 유지되어야 한다");
|
||||
assertTrue(r.contains("\"userId\":\"U001\""), "복호화된 JSON 필드가 병합되어야 한다");
|
||||
assertTrue(r.contains("\"name\":\"test\""), "복호화된 JSON 필드가 병합되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
|
||||
void testPreFilter_field_missingFromPathValue_passThrough() throws Exception {
|
||||
String body = "{\"other\":\"value\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/other");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. doPostFilter — BODY scope 암호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. BODY scope — 평문 암호화 → Base64 반환")
|
||||
void testPostFilter_body_encrypt() throws Exception {
|
||||
String body = "plain-response-body";
|
||||
Object result = filter.doPostFilter("g", "a", body, bodyEncryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(CIPHER_B64, result);
|
||||
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(body.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 6. doPostFilter — FIELD scope 암호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("6-1. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
|
||||
void testPostFilter_field_encryptToPath() throws Exception {
|
||||
String body = "{\"plain\":\"hello\",\"enc\":\"\"}";
|
||||
Properties prop = fieldEncryptProps("/plain", "/enc");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertTrue(result.toString().contains("\"enc\":\"" + CIPHER_B64 + "\""),
|
||||
"toPath에 Base64 암호문이 반영되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-2. FIELD scope, fromPath='/' — body 전체 암호화 → toPath 필드명으로 래핑")
|
||||
void testPostFilter_field_encryptFromRoot() throws Exception {
|
||||
String body = "{\"plain\":\"hello\"}";
|
||||
Properties prop = fieldEncryptProps("/", "/encrypted");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals("{\"encrypted\":\"" + CIPHER_B64 + "\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
|
||||
void testPostFilter_field_missingFromPathValue_passThrough() throws Exception {
|
||||
String body = "{\"other\":\"value\"}";
|
||||
Properties prop = fieldEncryptProps("/plain", "/enc");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 7. AAD 헤더 연동
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("7-1. doPreFilter BODY — CRYPTO_AAD_HEADER 설정 시 헤더값을 AAD로 전달")
|
||||
void testPreFilter_body_aadFromHeader_passedToService() throws Exception {
|
||||
String aadVal = "request-id-xyz";
|
||||
Properties prop = bodyDecryptProps();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
|
||||
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(aadVal);
|
||||
|
||||
filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(),
|
||||
eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-2. doPostFilter BODY — CRYPTO_AAD_HEADER 미설정 시 aad=null로 전달")
|
||||
void testPostFilter_body_noAadHeader_nullAadPassedToService() throws Exception {
|
||||
filter.doPostFilter("g", "a", "plain", bodyEncryptProps(), mockRequest, mockResponse);
|
||||
|
||||
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), any(byte[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-3. doPreFilter FIELD — CRYPTO_AAD_HEADER 설정 시 헤더값을 AAD로 전달")
|
||||
void testPreFilter_field_aadFromHeader_passedToService() throws Exception {
|
||||
String aadVal = "field-aad-value";
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/result");
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Aad");
|
||||
when(mockRequest.getHeader("X-Api-Aad")).thenReturn(aadVal);
|
||||
|
||||
filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(),
|
||||
eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private Properties bodyDecryptProps() {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties bodyEncryptProps() {
|
||||
return bodyDecryptProps();
|
||||
}
|
||||
|
||||
private Properties fieldDecryptProps(String fromPath, String toPath) {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
|
||||
p.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, fromPath);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, toPath);
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties fieldEncryptProps(String fromPath, String toPath) {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
|
||||
p.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, fromPath);
|
||||
p.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, toPath);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.eactive.eai.agent.inflow;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
|
||||
/**
|
||||
* ReloadInflowClientControlCommand 단위 테스트.
|
||||
*
|
||||
* <p>InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여
|
||||
* PropManager/ApplicationContext 없이 동작을 검증한다.
|
||||
*/
|
||||
class ReloadInflowClientControlCommandTest {
|
||||
|
||||
private ClientDualInflowControlManager mockDualManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockDualManager = mock(ClientDualInflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
|
||||
}
|
||||
|
||||
private ReloadInflowClientControlCommand commandWith(Object args) {
|
||||
ReloadInflowClientControlCommand cmd = new ReloadInflowClientControlCommand();
|
||||
cmd.setArgs(args);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// args 타입 검증 실패
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_argsIsInteger_throwsCommandException() {
|
||||
// args 검증이 getInflowControlManager() 호출 이전에 발생하므로 캐시 설정 불필요
|
||||
assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ClientDualInflowControlManager 비활성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_notClientDualManager_throwsCommandException() {
|
||||
InflowControlManager baseMgr = mock(InflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
|
||||
|
||||
assertThrows(CommandException.class, commandWith("CLIENT_A")::execute);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 전체 재로드 (ALL)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_argsIsALL_callsReloadClientNoArgs() throws Exception {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
|
||||
|
||||
Object result = commandWith("ALL").execute();
|
||||
|
||||
assertEquals("success", result);
|
||||
verify(mockDualManager).reloadClient();
|
||||
verify(mockDualManager, never()).reloadClient(anyString());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 개별 재로드 (ClientId)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_argsIsClientId_callsReloadClientWithId() throws Exception {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
|
||||
|
||||
Object result = commandWith("CLIENT_A").execute();
|
||||
|
||||
assertEquals("success", result);
|
||||
verify(mockDualManager).reloadClient("CLIENT_A");
|
||||
verify(mockDualManager, never()).reloadClient();
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_differentClientId_callsReloadClientWithThatId() throws Exception {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
|
||||
|
||||
commandWith("CLIENT_B").execute();
|
||||
|
||||
verify(mockDualManager).reloadClient("CLIENT_B");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.eactive.eai.agent.inflow;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
|
||||
/**
|
||||
* RemoveInflowClientControlCommand 단위 테스트.
|
||||
*
|
||||
* <p>InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여
|
||||
* PropManager/ApplicationContext 없이 동작을 검증한다.
|
||||
*/
|
||||
class RemoveInflowClientControlCommandTest {
|
||||
|
||||
private ClientDualInflowControlManager mockDualManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockDualManager = mock(ClientDualInflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
|
||||
}
|
||||
|
||||
private RemoveInflowClientControlCommand commandWith(Object args) {
|
||||
RemoveInflowClientControlCommand cmd = new RemoveInflowClientControlCommand();
|
||||
cmd.setArgs(args);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// args 타입 검증 실패
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_argsIsInteger_throwsCommandException() {
|
||||
assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ClientDualInflowControlManager 비활성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_notClientDualManager_throwsCommandException() {
|
||||
InflowControlManager baseMgr = mock(InflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
|
||||
|
||||
assertThrows(CommandException.class, commandWith("CLIENT_A")::execute);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 정상 제거
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_validClientId_callsRemoveClient() throws CommandException {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
|
||||
|
||||
Object result = commandWith("CLIENT_A").execute();
|
||||
|
||||
assertEquals("success", result);
|
||||
verify(mockDualManager).removeClient("CLIENT_A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_differentClientId_callsRemoveClientWithThatId() throws Exception {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
|
||||
|
||||
commandWith("CLIENT_B").execute();
|
||||
|
||||
verify(mockDualManager).removeClient("CLIENT_B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_validClientId_doesNotCallReload() throws Exception {
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
|
||||
|
||||
commandWith("CLIENT_A").execute();
|
||||
|
||||
verify(mockDualManager, never()).reloadClient();
|
||||
verify(mockDualManager, never()).reloadClient(anyString());
|
||||
}
|
||||
}
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
package com.eactive.eai.common.circuitBreaker.type;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
|
||||
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
|
||||
|
||||
/**
|
||||
* TypedCircuitBreakerManager 단위 테스트
|
||||
*
|
||||
* DB / 실 Spring 컨텍스트 없이 동작한다.
|
||||
* - PropManager : Mockito Mock (DB 없이 설정값 반환)
|
||||
* - TypedCircuitBreakerDAO : Mockito Mock (JPA 없이 VO 반환)
|
||||
* - Spring Bean : GenericApplicationContext 로 수동 구성
|
||||
*
|
||||
* 테스트 격리:
|
||||
* 각 테스트는 고유한 apiId 를 사용하거나 manager.reload() 로 레지스트리를 초기화한다.
|
||||
* manager.reload() 는 CircuitBreakerRegistry 를 새로 생성하므로 이전 상태가 제거된다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class TypedCircuitBreakerManagerTest {
|
||||
|
||||
// 슬라이딩 윈도우·최소 호출을 4로 설정해 빠른 상태 전이를 유도한다.
|
||||
private static final int DEFAULT_FAILURE_RATE = 50;
|
||||
private static final int DEFAULT_SLIDING_WINDOW = 4;
|
||||
private static final int DEFAULT_MIN_CALLS = 4;
|
||||
private static final int DEFAULT_WAIT_DURATION_SEC = 1; // OPEN 대기 1초 (테스트용)
|
||||
private static final int DEFAULT_HALF_OPEN_PERMITS = 2;
|
||||
private static final int DEFAULT_SLOW_RATE = 100;
|
||||
private static final int DEFAULT_SLOW_DURATION_SEC = 5;
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static PropManager mockPropManager;
|
||||
private static TypedCircuitBreakerDAO mockDao;
|
||||
private static TypedCircuitBreakerManager manager;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 픽스처 설정 / 해제
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockPropManager = mock(PropManager.class);
|
||||
mockDao = mock(TypedCircuitBreakerDAO.class);
|
||||
|
||||
setupDefaultPropManagerMock();
|
||||
when(mockDao.getList()).thenReturn(Collections.emptyList());
|
||||
|
||||
// TypedCircuitBreakerManager 인스턴스 생성 후 DAO 리플렉션 주입
|
||||
manager = new TypedCircuitBreakerManager();
|
||||
injectField(manager, "dao", mockDao);
|
||||
|
||||
// DB / 실 Spring 없이 최소 컨텍스트 구성
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("propManager", mockPropManager);
|
||||
ctx.getBeanFactory().registerSingleton("typedCircuitBreakerManager", manager);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
manager.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void resetMocks() throws Exception {
|
||||
reset(mockDao);
|
||||
setupDefaultPropManagerMock();
|
||||
when(mockDao.getList()).thenReturn(Collections.emptyList());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 헬퍼
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static void setupDefaultPropManagerMock() {
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "failureRateThreshold"))
|
||||
.thenReturn(String.valueOf(DEFAULT_FAILURE_RATE));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "slidingWindowSize"))
|
||||
.thenReturn(String.valueOf(DEFAULT_SLIDING_WINDOW));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "minimumNumberOfCalls"))
|
||||
.thenReturn(String.valueOf(DEFAULT_MIN_CALLS));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "waitDurationInOpenState"))
|
||||
.thenReturn(String.valueOf(DEFAULT_WAIT_DURATION_SEC));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "permittedNumberOfCallsInHalfOpenState"))
|
||||
.thenReturn(String.valueOf(DEFAULT_HALF_OPEN_PERMITS));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "slowCallRateThreshold"))
|
||||
.thenReturn(String.valueOf(DEFAULT_SLOW_RATE));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "slowCallDurationThreshold"))
|
||||
.thenReturn(String.valueOf(DEFAULT_SLOW_DURATION_SEC));
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("1");
|
||||
}
|
||||
|
||||
private static void injectField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
/** 테스트용 TypedCircuitBreakerVO 생성 (기본 설정값 사용) */
|
||||
private TypedCircuitBreakerVO buildVo(String id, String apiId, String useYn) {
|
||||
return TypedCircuitBreakerVO.builder()
|
||||
.id(id).name(apiId).type("COUNT_BASED").desc(apiId + " 테스트 설정")
|
||||
.failureRateThreshold(DEFAULT_FAILURE_RATE)
|
||||
.slidingWindowSize(DEFAULT_SLIDING_WINDOW)
|
||||
.minimumNumberOfCalls(DEFAULT_MIN_CALLS)
|
||||
.waitDurationInOpenState(DEFAULT_WAIT_DURATION_SEC)
|
||||
.permittedNumberOfCallsInHalfOpenState(DEFAULT_HALF_OPEN_PERMITS)
|
||||
.slowCallRateThreshold(DEFAULT_SLOW_RATE)
|
||||
.slowCallDurationThreshold(DEFAULT_SLOW_DURATION_SEC)
|
||||
.useYn(useYn)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** CB를 count 회 실패 처리한다 */
|
||||
private void recordFailures(CircuitBreaker cb, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
try {
|
||||
cb.executeCallable(() -> { throw new RuntimeException("테스트 강제 오류"); });
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. getCircuitBreaker - 기본 조회 동작
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. 등록된 apiId → 해당 설정의 CircuitBreaker 반환")
|
||||
void testGetCircuitBreaker_registeredApiId_returnsTypedCircuitBreaker() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-001", "API_REG", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_REG");
|
||||
|
||||
assertNotNull(cb);
|
||||
assertEquals("API_REG", cb.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. 미등록 apiId → default CircuitBreaker 로 fallback")
|
||||
void testGetCircuitBreaker_unregisteredApiId_returnsDefaultCircuitBreaker() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Collections.emptyList());
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_UNKNOWN");
|
||||
|
||||
assertNotNull(cb, "미등록 apiId 는 default CB 를 반환해야 한다");
|
||||
assertEquals("default", cb.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. useYn=0 인 apiId → default CircuitBreaker 로 fallback")
|
||||
void testGetCircuitBreaker_disabledApiId_returnsDefaultCircuitBreaker() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-002", "API_DISABLED", "0")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_DISABLED");
|
||||
|
||||
assertNotNull(cb, "비활성 apiId 는 default CB 로 fallback 되어야 한다");
|
||||
assertEquals("default", cb.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. apiId 도 default 도 비활성(useYn=0) → null 반환")
|
||||
void testGetCircuitBreaker_defaultDisabled_returnsNull() throws Exception {
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("0");
|
||||
when(mockDao.getList()).thenReturn(Collections.emptyList());
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("ANY_API");
|
||||
|
||||
assertNull(cb, "default 도 비활성이면 null 을 반환해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. PropManager useYn=Y → normalizeUseYn 으로 '1' 정규화 → default 활성")
|
||||
void testGetCircuitBreaker_defaultUseYn_Y_isNormalizedToEnabled() throws Exception {
|
||||
when(mockPropManager.getProperty("CircuitBreaker", "useYn", "N")).thenReturn("Y");
|
||||
when(mockDao.getList()).thenReturn(Collections.emptyList());
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("ANY_API");
|
||||
|
||||
assertNotNull(cb, "PropManager useYn=Y 는 활성으로 정규화되어야 한다");
|
||||
assertEquals("default", cb.getName());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. apiId 별 독립성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. 서로 다른 apiId → 독립적인 CircuitBreaker 인스턴스 반환")
|
||||
void testDifferentApiIds_haveIndependentCircuitBreakerInstances() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(
|
||||
buildVo("uuid-A", "API_INDEP_A", "1"),
|
||||
buildVo("uuid-B", "API_INDEP_B", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cbA = manager.getCircuitBreaker("API_INDEP_A");
|
||||
CircuitBreaker cbB = manager.getCircuitBreaker("API_INDEP_B");
|
||||
|
||||
assertNotNull(cbA);
|
||||
assertNotNull(cbB);
|
||||
assertNotSame(cbA, cbB, "서로 다른 apiId 는 다른 CB 인스턴스여야 한다");
|
||||
assertEquals("API_INDEP_A", cbA.getName());
|
||||
assertEquals("API_INDEP_B", cbB.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. API_A 실패 → OPEN, API_B 는 CLOSED 유지 (상태 격리)")
|
||||
void testDifferentApiIds_statesAreIsolated() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(
|
||||
buildVo("uuid-ISO-A", "API_ISO_A", "1"),
|
||||
buildVo("uuid-ISO-B", "API_ISO_B", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cbA = manager.getCircuitBreaker("API_ISO_A");
|
||||
CircuitBreaker cbB = manager.getCircuitBreaker("API_ISO_B");
|
||||
|
||||
// API_ISO_A 만 minimumNumberOfCalls=4 회 실패
|
||||
recordFailures(cbA, 4);
|
||||
|
||||
assertEquals(CircuitBreaker.State.OPEN, cbA.getState(), "API_ISO_A 는 OPEN 이어야 한다");
|
||||
assertEquals(CircuitBreaker.State.CLOSED, cbB.getState(), "API_ISO_B 는 CLOSED 를 유지해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. 상태 전이
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. 실패율 50% 초과(4/4 실패) → CLOSED → OPEN")
|
||||
void testCircuitBreaker_closedToOpen_afterFailureThresholdExceeded() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-001", "API_ST_OPEN", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_ST_OPEN");
|
||||
assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), "초기 상태는 CLOSED 여야 한다");
|
||||
|
||||
// minimumNumberOfCalls=4, failureRateThreshold=50% → 4/4 실패(100%) → OPEN
|
||||
recordFailures(cb, 4);
|
||||
|
||||
assertEquals(CircuitBreaker.State.OPEN, cb.getState(), "실패율 초과 시 OPEN 상태여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. 실패율 미달(1/4 실패, 25%) → CLOSED 유지")
|
||||
void testCircuitBreaker_belowFailureThreshold_remainsClosed() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-002", "API_ST_BELOW", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_ST_BELOW");
|
||||
|
||||
// 4회 중 3회 성공, 1회 실패 → 실패율 25% < 50% → CLOSED 유지
|
||||
// Resilience4j 는 >= 비교: 50% >= 50% 는 OPEN 이므로 25% 로 낮춰야 CLOSED 유지
|
||||
try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {}
|
||||
try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {}
|
||||
try { cb.executeCallable(() -> "성공"); } catch (Exception ignored) {}
|
||||
recordFailures(cb, 1);
|
||||
|
||||
assertEquals(CircuitBreaker.State.CLOSED, cb.getState(), "실패율 25% (< threshold 50%) 이면 CLOSED 를 유지해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. CB OPEN 상태 → 모든 요청 즉시 차단 (CallNotPermittedException)")
|
||||
void testCircuitBreaker_open_blocksAllRequests() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-003", "API_ST_BLOCK", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_ST_BLOCK");
|
||||
recordFailures(cb, 4);
|
||||
assertEquals(CircuitBreaker.State.OPEN, cb.getState());
|
||||
|
||||
assertThrows(CallNotPermittedException.class,
|
||||
() -> cb.executeCallable(() -> "도달하면 안 됨"),
|
||||
"OPEN 상태에서 CallNotPermittedException 이 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-4. OPEN → waitDuration(1초) 경과 → 첫 요청 시 HALF_OPEN 전이")
|
||||
void testCircuitBreaker_open_transitionsToHalfOpenAfterWaitDuration() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-004", "API_ST_HALFOPEN", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_ST_HALFOPEN");
|
||||
recordFailures(cb, 4);
|
||||
assertEquals(CircuitBreaker.State.OPEN, cb.getState());
|
||||
|
||||
// waitDurationInOpenState = 1초 대기 후 요청 → HALF_OPEN 전이
|
||||
Thread.sleep(1200);
|
||||
try { cb.executeCallable(() -> "HALF_OPEN 트리거"); } catch (Exception ignored) {}
|
||||
|
||||
assertEquals(CircuitBreaker.State.HALF_OPEN, cb.getState(),
|
||||
"waitDuration 경과 후 첫 요청 시 HALF_OPEN 상태여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-5. HALF_OPEN → 성공(permittedCalls=2회) → CLOSED 복구")
|
||||
void testCircuitBreaker_halfOpen_transitionsToClosedOnSuccess() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-005", "API_ST_RECOVER", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_ST_RECOVER");
|
||||
recordFailures(cb, 4);
|
||||
|
||||
Thread.sleep(1200);
|
||||
|
||||
// HALF_OPEN 에서 permittedNumberOfCallsInHalfOpenState=2 회 성공
|
||||
for (int i = 0; i < DEFAULT_HALF_OPEN_PERMITS; i++) {
|
||||
final String result = cb.executeCallable(() -> "성공");
|
||||
assertEquals("성공", result);
|
||||
}
|
||||
|
||||
assertEquals(CircuitBreaker.State.CLOSED, cb.getState(),
|
||||
"HALF_OPEN 에서 허용 호출 수 이상 성공하면 CLOSED 로 복구되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-6. HALF_OPEN → 실패 → 다시 OPEN 재전이")
|
||||
void testCircuitBreaker_halfOpen_transitionsBackToOpenOnFailure() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-ST-006", "API_ST_REOPEN", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_ST_REOPEN");
|
||||
recordFailures(cb, 4);
|
||||
|
||||
Thread.sleep(1200);
|
||||
|
||||
// HALF_OPEN 에서 permittedNumberOfCallsInHalfOpenState=2 회 모두 실패해야 OPEN 으로 재전이된다.
|
||||
// Resilience4j 는 허용 호출 수(2회)를 모두 소진한 후에 실패율을 평가하므로
|
||||
// 1회만 실패하면 HALF_OPEN 상태가 유지된다.
|
||||
recordFailures(cb, DEFAULT_HALF_OPEN_PERMITS);
|
||||
|
||||
assertEquals(CircuitBreaker.State.OPEN, cb.getState(),
|
||||
"HALF_OPEN 에서 허용 호출 수(2회) 모두 실패하면 다시 OPEN 으로 전이되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. reload
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. reload(key) → CircuitBreakerConfig 갱신 (waitDuration 1초 → 60초)")
|
||||
void testReload_key_updatesCircuitBreakerConfig() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-RLD-001", "API_RELOAD", "1")));
|
||||
manager.reload();
|
||||
|
||||
// waitDurationInOpenState 60초로 변경
|
||||
TypedCircuitBreakerVO updated = TypedCircuitBreakerVO.builder()
|
||||
.id("uuid-RLD-001").name("API_RELOAD").type("COUNT_BASED")
|
||||
.failureRateThreshold(50).slidingWindowSize(4).minimumNumberOfCalls(4)
|
||||
.waitDurationInOpenState(60)
|
||||
.permittedNumberOfCallsInHalfOpenState(2)
|
||||
.slowCallRateThreshold(100).slowCallDurationThreshold(5)
|
||||
.useYn("1").build();
|
||||
when(mockDao.get("uuid-RLD-001")).thenReturn(updated);
|
||||
manager.reload("uuid-RLD-001");
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_RELOAD");
|
||||
assertEquals(60,
|
||||
cb.getCircuitBreakerConfig().getWaitDurationInOpenState().getSeconds(),
|
||||
"reload 후 waitDurationInOpenState 가 60초로 갱신되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. reload(key) useYn=0 → 맵에서 제거 → default fallback")
|
||||
void testReload_key_disabledVo_removesFromMap() throws Exception {
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-RLD-002", "API_RM", "1")));
|
||||
manager.reload();
|
||||
assertEquals("API_RM", manager.getCircuitBreaker("API_RM").getName());
|
||||
|
||||
// useYn=0 으로 비활성화
|
||||
TypedCircuitBreakerVO disabled = TypedCircuitBreakerVO.builder()
|
||||
.id("uuid-RLD-002").name("API_RM").type("COUNT_BASED")
|
||||
.failureRateThreshold(50).slidingWindowSize(4).minimumNumberOfCalls(4)
|
||||
.waitDurationInOpenState(1).permittedNumberOfCallsInHalfOpenState(2)
|
||||
.slowCallRateThreshold(100).slowCallDurationThreshold(5)
|
||||
.useYn("0").build();
|
||||
when(mockDao.get("uuid-RLD-002")).thenReturn(disabled);
|
||||
manager.reload("uuid-RLD-002");
|
||||
|
||||
CircuitBreaker cb = manager.getCircuitBreaker("API_RM");
|
||||
assertEquals("default", cb.getName(),
|
||||
"비활성화된 CB 는 맵에서 제거 후 default 로 fallback 되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3. reload() 전체 → CircuitBreakerRegistry 재생성으로 이전 CB 상태 초기화")
|
||||
void testReload_all_refreshesAllCircuitBreakers() throws Exception {
|
||||
// 초기: API_FULL_A 등록 후 OPEN 상태로 만들기
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(buildVo("uuid-FULL-A", "API_FULL_A", "1")));
|
||||
manager.reload();
|
||||
|
||||
CircuitBreaker cbA = manager.getCircuitBreaker("API_FULL_A");
|
||||
recordFailures(cbA, 4);
|
||||
assertEquals(CircuitBreaker.State.OPEN, cbA.getState(), "전체 reload 전 API_FULL_A 는 OPEN 이어야 한다");
|
||||
|
||||
// 전체 reload → CircuitBreakerRegistry 재생성
|
||||
// init() 은 apiIdCBConfigMap 을 초기화하지 않으므로 API_FULL_A 는 맵에 남아있다.
|
||||
// 하지만 새 레지스트리를 생성하므로 CB 상태(OPEN)는 초기화(CLOSED)된다.
|
||||
when(mockDao.getList()).thenReturn(Arrays.asList(
|
||||
buildVo("uuid-FULL-A", "API_FULL_A", "1"),
|
||||
buildVo("uuid-FULL-B", "API_FULL_B", "1")));
|
||||
manager.reload();
|
||||
|
||||
// 레지스트리 재생성으로 API_FULL_A CB 상태가 CLOSED 로 초기화되어야 한다
|
||||
CircuitBreaker cbAAfter = manager.getCircuitBreaker("API_FULL_A");
|
||||
assertEquals(CircuitBreaker.State.CLOSED, cbAAfter.getState(),
|
||||
"전체 reload 후 CircuitBreakerRegistry 재생성으로 이전 CB 상태가 CLOSED 로 초기화되어야 한다");
|
||||
|
||||
// 새 apiId 도 정상 등록
|
||||
assertEquals("API_FULL_B", manager.getCircuitBreaker("API_FULL_B").getName(),
|
||||
"전체 reload 후 새 apiId 는 정상 등록되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. 생명주기
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. start() 후 isStarted() = true")
|
||||
void testIsStarted_afterStart_returnsTrue() {
|
||||
assertTrue(manager.isStarted(), "start() 후 isStarted() 는 true 여야 한다");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.util.Enumeration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* SoftHSM2 마스터키 등록 유틸리티 테스트.
|
||||
*
|
||||
* 용도: CryptoModuleConfig.keyDerivParams 의 hsmKeyAlias 에 대응하는
|
||||
* AES-256 마스터키를 SoftHSM2 토큰에 등록한다.
|
||||
*
|
||||
* 사전 조건:
|
||||
* C:\SoftHSM2 에 SoftHSM2 설치
|
||||
* (토큰이 없으면 자동 초기화, 있으면 기존 토큰 재사용)
|
||||
*
|
||||
* 실행 후 확인:
|
||||
* softhsm2-util.exe --show-slots
|
||||
* 또는 pkcs11-tool --list-objects (OpenSC 설치 시)
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class HsmKeyRegisterUtil {
|
||||
|
||||
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
|
||||
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
|
||||
private static final String TOKEN_LABEL = "eapim-test";
|
||||
private static final String PIN = "1234";
|
||||
|
||||
/** DB CryptoModuleConfig.keyDerivParams.hsmKeyAlias 와 반드시 일치해야 한다. */
|
||||
private static final String MASTER_KEY_ALIAS = "MASTER_KEY";
|
||||
|
||||
private static Provider provider;
|
||||
private static KeyStore keyStore;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws Exception {
|
||||
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
|
||||
assumeTrue(new File(SOFTHSM2_DLL).exists(),
|
||||
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
|
||||
|
||||
ensureTokenInitialized();
|
||||
|
||||
String cfgContent = "name = SoftHSM\n"
|
||||
+ "library = " + SOFTHSM2_DLL + "\n"
|
||||
+ "slotListIndex = 0\n";
|
||||
|
||||
provider = HsmManager.createProvider(cfgContent);
|
||||
Provider existing = Security.getProvider(provider.getName());
|
||||
if (existing != null) Security.removeProvider(existing.getName());
|
||||
Security.addProvider(provider);
|
||||
|
||||
keyStore = KeyStore.getInstance("PKCS11", provider);
|
||||
keyStore.load(null, PIN.toCharArray());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 1. 현재 등록된 키 목록 출력
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void listKeys() throws Exception {
|
||||
System.out.println("=== HSM 키 목록 ===");
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
if (!aliases.hasMoreElements()) {
|
||||
System.out.println(" (등록된 키 없음)");
|
||||
}
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
String type = keyStore.isKeyEntry(alias) ? "KEY" : "CERT";
|
||||
System.out.println(" [" + type + "] " + alias);
|
||||
}
|
||||
System.out.println("==================");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 2. 마스터키 등록 (없는 경우에만)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void registerMasterKey() throws Exception {
|
||||
if (keyStore.containsAlias(MASTER_KEY_ALIAS)) {
|
||||
System.out.println("[SKIP] 이미 등록됨: " + MASTER_KEY_ALIAS);
|
||||
return;
|
||||
}
|
||||
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES", provider);
|
||||
kg.init(256);
|
||||
SecretKey sk = kg.generateKey();
|
||||
keyStore.setKeyEntry(MASTER_KEY_ALIAS, sk, null, null);
|
||||
|
||||
System.out.println("[OK] AES-256 마스터키 등록 완료: alias=" + MASTER_KEY_ALIAS);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 3. 등록 후 조회 확인
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
void verifyMasterKey() throws Exception {
|
||||
keyStore.load(null, PIN.toCharArray()); // 갱신
|
||||
|
||||
assert keyStore.containsAlias(MASTER_KEY_ALIAS)
|
||||
: "마스터키 등록 실패: " + MASTER_KEY_ALIAS;
|
||||
|
||||
SecretKey sk = (SecretKey) keyStore.getKey(MASTER_KEY_ALIAS, null);
|
||||
assert sk != null : "키 조회 실패";
|
||||
assert "AES".equals(sk.getAlgorithm()) : "알고리즘 불일치: " + sk.getAlgorithm();
|
||||
|
||||
System.out.println("[OK] 마스터키 조회 성공: alias=" + MASTER_KEY_ALIAS
|
||||
+ " / algorithm=" + sk.getAlgorithm());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 헬퍼
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private static void ensureTokenInitialized() throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
SOFTHSM2_UTIL, "--init-token", "--free",
|
||||
"--label", TOKEN_LABEL,
|
||||
"--pin", PIN,
|
||||
"--so-pin", PIN);
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
String out = readOutput(p);
|
||||
p.waitFor(10, TimeUnit.SECONDS);
|
||||
System.out.println("[SoftHSM2] init-token: " + out.trim());
|
||||
}
|
||||
|
||||
private static String readOutput(Process p) throws Exception {
|
||||
InputStream is = p.getInputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len;
|
||||
while ((len = is.read(buf)) != -1) {
|
||||
sb.append(new String(buf, 0, len, "UTF-8"));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.eactive.eai.common.inflow;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -36,9 +38,13 @@ class InflowControlManagerTest {
|
||||
InflowControlManager inflowControlManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws LifecycleException {
|
||||
void setUp() throws Exception {
|
||||
if (!inflowControlManager.isStarted()) {
|
||||
inflowControlManager.start();
|
||||
} else {
|
||||
inflowControlManager.reloadAdapter();
|
||||
inflowControlManager.reloadInterface();
|
||||
inflowControlManager.reloadGroup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +63,22 @@ class InflowControlManagerTest {
|
||||
assertNull(inflowControlManager.getGroupBucket("non-existent-group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adapter_spikePasses_thenBurstBlocked() {
|
||||
for (int i = 0; i < 5; i++) assertTrue(inflowControlManager.isAdapterPass("TEST_ADAPTER"));
|
||||
assertFalse(inflowControlManager.isAdapterPass("TEST_ADAPTER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void interface_spikePasses_thenBurstBlocked() {
|
||||
for (int i = 0; i < 5; i++) assertTrue(inflowControlManager.isInterfacePass("TEST_INTERFACE"));
|
||||
assertFalse(inflowControlManager.isInterfacePass("TEST_INTERFACE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void group_spikePasses_thenBurstBlocked() {
|
||||
for (int i = 0; i < 5; i++) assertNull(inflowControlManager.isGroupPass("test-group-quota"));
|
||||
assertEquals(CustomGroupBucket.RESULT_BLOCKED_THRESHOLD, inflowControlManager.isGroupPass("test-group-quota"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
/**
|
||||
* ClientDualInflowControlManager 클라이언트 메트릭 단위 테스트.
|
||||
*/
|
||||
class ClientDualInflowControlManagerMetricsTest {
|
||||
|
||||
private ClientDualInflowControlManager manager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
manager = new ClientDualInflowControlManager();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private InflowTargetVO makeTargetVO(String name) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(name);
|
||||
vo.setThresholdPerSecond(100);
|
||||
vo.setThreshold(10000);
|
||||
vo.setThresholdTimeUnit("DAY");
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private LocalBucket stableBucket(long capacity) {
|
||||
return Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(capacity, Duration.ofHours(1)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private LocalBucket exhaustedBucket(long capacity) {
|
||||
LocalBucket b = stableBucket(capacity);
|
||||
b.tryConsume(capacity);
|
||||
return b;
|
||||
}
|
||||
|
||||
private DualCustomBucket passDualBucket(String name) {
|
||||
return new DualCustomBucket(stableBucket(100), null, makeTargetVO(name));
|
||||
}
|
||||
|
||||
private DualCustomBucket blockedPerSecondDualBucket(String name) {
|
||||
return new DualCustomBucket(exhaustedBucket(1), null, makeTargetVO(name));
|
||||
}
|
||||
|
||||
private DualCustomBucket blockedThresholdDualBucket(String name) {
|
||||
return new DualCustomBucket(stableBucket(100), exhaustedBucket(1), makeTargetVO(name));
|
||||
}
|
||||
|
||||
private void injectClientMap(Map<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// isClientPassDetail() — 카운터 증가
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_bucketNotRegistered_noMetricsCreated() {
|
||||
manager.isClientPassDetail("UNKNOWN");
|
||||
|
||||
assertNull(manager.getClientMetrics("UNKNOWN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_pass_incrementsAllowed() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", blockedThresholdDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", blockedPerSecondDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").rejectedPerSecond.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getAllClientMetrics — 조회 및 불변성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getAllClientMetrics_empty_returnsEmptyMap() {
|
||||
assertTrue(manager.getAllClientMetrics().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllClientMetrics_afterCalls_containsEntries() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
map.put("C2", passDualBucket("C2"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.isClientPassDetail("C2");
|
||||
|
||||
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllClientMetrics();
|
||||
assertEquals(2, all.size());
|
||||
assertTrue(all.containsKey("C1"));
|
||||
assertTrue(all.containsKey("C2"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// resetClientMetrics / resetAllClientMetrics
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void resetClientMetrics_resetsTargetClient() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.resetClientMetrics("C1");
|
||||
|
||||
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAllClientMetrics_resetsAll() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
map.put("C2", passDualBucket("C2"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.isClientPassDetail("C2");
|
||||
manager.resetAllClientMetrics();
|
||||
|
||||
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
|
||||
assertEquals(0, manager.getClientMetrics("C2").allowed.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// removeClient — 메트릭 자동 삭제
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void removeClient_cleansUpMetrics() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
assertNotNull(manager.getClientMetrics("C1"));
|
||||
|
||||
manager.removeClient("C1");
|
||||
|
||||
assertNull(manager.getClientMetrics("C1"));
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
/**
|
||||
* DualInflowControlManager의 Dual 맵 기반 오버라이드 메서드 단위 테스트.
|
||||
*
|
||||
* <p>검증 포인트:
|
||||
* - getAdapterAllKeys / getInterfaceAllKeys 가 Dual 맵(adapterBucketMap/interfaceBucketMap) 을 사용
|
||||
* - getAdapterInflowThreashold / getInterfaceInflowThreashold 가 Dual 맵의 VO 를 반환
|
||||
* - removeAdapter / removeInterface 가 Dual 맵에서 항목을 제거
|
||||
* - base 의 adapterBucketList / interfaceBucketList 상태와 무관하게 동작 (DAO 이중 호출 제거 효과)
|
||||
*/
|
||||
class DualInflowControlManagerBucketMethodsTest {
|
||||
|
||||
private DualInflowControlManager manager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
manager = new DualInflowControlManager();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private InflowTargetVO makeVO(String name) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(name);
|
||||
vo.setThresholdPerSecond(100);
|
||||
vo.setThreshold(10000);
|
||||
vo.setThresholdTimeUnit("DAY");
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private LocalBucket bucket(long capacity) {
|
||||
return Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(capacity, Duration.ofSeconds(1)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DualCustomBucket dualBucket(String name) {
|
||||
return new DualCustomBucket(bucket(100), bucket(1000), makeVO(name));
|
||||
}
|
||||
|
||||
private void setAdapterMap(ConcurrentHashMap<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
|
||||
}
|
||||
|
||||
private void setInterfaceMap(ConcurrentHashMap<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getAdapterAllKeys
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getAdapterAllKeys_emptyMap_returnsEmptyArray() {
|
||||
setAdapterMap(new ConcurrentHashMap<>());
|
||||
|
||||
assertEquals(0, manager.getAdapterAllKeys().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAdapterAllKeys_twoEntries_returnsSortedKeys() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("ADAPTER_B", dualBucket("ADAPTER_B"));
|
||||
map.put("ADAPTER_A", dualBucket("ADAPTER_A"));
|
||||
setAdapterMap(map);
|
||||
|
||||
String[] keys = manager.getAdapterAllKeys();
|
||||
|
||||
assertArrayEquals(new String[]{"ADAPTER_A", "ADAPTER_B"}, keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAdapterAllKeys_usesDualMap_notBaseList() {
|
||||
// Dual 맵에만 항목 존재 — base 의 adapterBucketList 는 비어 있음
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("DUAL_ONLY", dualBucket("DUAL_ONLY"));
|
||||
setAdapterMap(map);
|
||||
|
||||
String[] keys = manager.getAdapterAllKeys();
|
||||
|
||||
assertEquals(1, keys.length);
|
||||
assertEquals("DUAL_ONLY", keys[0]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getInterfaceAllKeys
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getInterfaceAllKeys_emptyMap_returnsEmptyArray() {
|
||||
setInterfaceMap(new ConcurrentHashMap<>());
|
||||
|
||||
assertEquals(0, manager.getInterfaceAllKeys().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInterfaceAllKeys_threeEntries_returnsSortedKeys() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF_C", dualBucket("IF_C"));
|
||||
map.put("IF_A", dualBucket("IF_A"));
|
||||
map.put("IF_B", dualBucket("IF_B"));
|
||||
setInterfaceMap(map);
|
||||
|
||||
String[] keys = manager.getInterfaceAllKeys();
|
||||
|
||||
assertArrayEquals(new String[]{"IF_A", "IF_B", "IF_C"}, keys);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInterfaceAllKeys_usesDualMap_notBaseList() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("DUAL_IF", dualBucket("DUAL_IF"));
|
||||
setInterfaceMap(map);
|
||||
|
||||
String[] keys = manager.getInterfaceAllKeys();
|
||||
|
||||
assertEquals(1, keys.length);
|
||||
assertEquals("DUAL_IF", keys[0]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getAdapterInflowThreashold
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getAdapterInflowThreashold_found_returnsVO() {
|
||||
InflowTargetVO vo = makeVO("ADAPTER_A");
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("ADAPTER_A", new DualCustomBucket(bucket(10), bucket(100), vo));
|
||||
setAdapterMap(map);
|
||||
|
||||
InflowTargetVO result = manager.getAdapterInflowThreashold("ADAPTER_A");
|
||||
|
||||
assertSame(vo, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAdapterInflowThreashold_notFound_returnsNull() {
|
||||
setAdapterMap(new ConcurrentHashMap<>());
|
||||
|
||||
assertNull(manager.getAdapterInflowThreashold("MISSING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAdapterInflowThreashold_usesDualMap_notBaseList() {
|
||||
// base adapterBucketList 에는 항목 없고 Dual 맵에만 존재
|
||||
InflowTargetVO vo = makeVO("ADAPTER_A");
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("ADAPTER_A", new DualCustomBucket(null, null, vo));
|
||||
setAdapterMap(map);
|
||||
|
||||
assertSame(vo, manager.getAdapterInflowThreashold("ADAPTER_A"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getInterfaceInflowThreashold
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getInterfaceInflowThreashold_found_returnsVO() {
|
||||
InflowTargetVO vo = makeVO("IF_001");
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF_001", new DualCustomBucket(bucket(10), bucket(100), vo));
|
||||
setInterfaceMap(map);
|
||||
|
||||
assertSame(vo, manager.getInterfaceInflowThreashold("IF_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInterfaceInflowThreashold_notFound_returnsNull() {
|
||||
setInterfaceMap(new ConcurrentHashMap<>());
|
||||
|
||||
assertNull(manager.getInterfaceInflowThreashold("MISSING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInterfaceInflowThreashold_usesDualMap_notBaseList() {
|
||||
InflowTargetVO vo = makeVO("IF_001");
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF_001", new DualCustomBucket(null, null, vo));
|
||||
setInterfaceMap(map);
|
||||
|
||||
assertSame(vo, manager.getInterfaceInflowThreashold("IF_001"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// removeAdapter
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void removeAdapter_existingKey_removedFromDualMap() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("ADAPTER_A", dualBucket("ADAPTER_A"));
|
||||
map.put("ADAPTER_B", dualBucket("ADAPTER_B"));
|
||||
setAdapterMap(map);
|
||||
|
||||
manager.removeAdapter("ADAPTER_A");
|
||||
|
||||
assertNull(manager.getAdapterBucket("ADAPTER_A"));
|
||||
assertNotNull(manager.getAdapterBucket("ADAPTER_B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAdapter_keysReflectRemoval() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("ADAPTER_A", dualBucket("ADAPTER_A"));
|
||||
map.put("ADAPTER_B", dualBucket("ADAPTER_B"));
|
||||
setAdapterMap(map);
|
||||
|
||||
manager.removeAdapter("ADAPTER_A");
|
||||
|
||||
String[] keys = manager.getAdapterAllKeys();
|
||||
assertEquals(1, keys.length);
|
||||
assertEquals("ADAPTER_B", keys[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAdapter_missingKey_noException() {
|
||||
setAdapterMap(new ConcurrentHashMap<>());
|
||||
|
||||
assertDoesNotThrow(() -> manager.removeAdapter("NOT_EXISTS"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// removeInterface
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void removeInterface_existingKey_removedFromDualMap() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF_001", dualBucket("IF_001"));
|
||||
map.put("IF_002", dualBucket("IF_002"));
|
||||
setInterfaceMap(map);
|
||||
|
||||
manager.removeInterface("IF_001");
|
||||
|
||||
assertNull(manager.getInterfaceBucket("IF_001"));
|
||||
assertNotNull(manager.getInterfaceBucket("IF_002"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeInterface_keysReflectRemoval() {
|
||||
ConcurrentHashMap<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF_001", dualBucket("IF_001"));
|
||||
map.put("IF_002", dualBucket("IF_002"));
|
||||
setInterfaceMap(map);
|
||||
|
||||
manager.removeInterface("IF_001");
|
||||
|
||||
String[] keys = manager.getInterfaceAllKeys();
|
||||
assertEquals(1, keys.length);
|
||||
assertEquals("IF_002", keys[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeInterface_missingKey_noException() {
|
||||
setInterfaceMap(new ConcurrentHashMap<>());
|
||||
|
||||
assertDoesNotThrow(() -> manager.removeInterface("NOT_EXISTS"));
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
/**
|
||||
* DualInflowControlManager.makeBucket() 버킷 생성 동작 검증.
|
||||
*
|
||||
* <p>검증 포인트:
|
||||
* - threshold/perSecond 버킷이 실제로 생성되는지
|
||||
* - 할당량 이내 spike는 통과하고 초과 burst는 차단되는지
|
||||
* - perSecond/threshold 중 어느 쪽이 binding 제약인지 구분되는지
|
||||
*/
|
||||
class DualInflowControlManagerMakeBucketTest {
|
||||
|
||||
private final DualInflowControlManager manager = new DualInflowControlManager();
|
||||
|
||||
private InflowTargetVO vo(long perSecond, long threshold, String timeUnit) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName("TEST");
|
||||
vo.setThresholdPerSecond(perSecond);
|
||||
vo.setThreshold(threshold);
|
||||
vo.setThresholdTimeUnit(timeUnit);
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// threshold 단독 (5건/분)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void thresholdOnly_spikePasses_thenBurstBlocked() {
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(0, 5, "MIN"));
|
||||
|
||||
for (int i = 0; i < 5; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, bucket.tryConsume());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// perSecond 단독
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void perSecondOnly_spikePasses_thenBurstBlocked() {
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(5, 0, null));
|
||||
|
||||
for (int i = 0; i < 5; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, bucket.tryConsume());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// perSecond + threshold 동시 설정 — binding 제약 구분
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void both_perSecondIsBinding_blockedByPerSecond() {
|
||||
// perSecond=3 이 binding: threshold=100/MIN은 여유 있음
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(3, 100, "MIN"));
|
||||
|
||||
for (int i = 0; i < 3; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, bucket.tryConsume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void both_thresholdIsBinding_blockedByThreshold() {
|
||||
// threshold=3/MIN 이 binding: perSecond=100은 여유 있음
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(100, 3, "MIN"));
|
||||
|
||||
for (int i = 0; i < 3; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, bucket.tryConsume());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 비활성 / 조건 미충족 → null 반환
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void inactiveVO_returnsNull() {
|
||||
InflowTargetVO vo = vo(5, 0, null);
|
||||
vo.setActivate(false);
|
||||
assertNull(manager.makeBucket(vo));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noLimitsSet_returnsNull() {
|
||||
assertNull(manager.makeBucket(vo(0, 0, null)));
|
||||
}
|
||||
}
|
||||
+276
-3
@@ -12,16 +12,17 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.CustomGroupBucket;
|
||||
import com.eactive.eai.common.inflow.InflowGroupVO;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
/**
|
||||
* DualInflowControlManager 그룹 메트릭 단위 테스트.
|
||||
* DualInflowControlManager 메트릭 단위 테스트.
|
||||
*
|
||||
* <p>start() 없이 groupBucketList를 ReflectionTestUtils로 직접 주입하여
|
||||
* isGroupPass() 카운팅과 getGroupMetrics / reset 메서드를 검증한다.
|
||||
* <p>어댑터/인터페이스/그룹 메트릭을 검증한다.
|
||||
* 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다.
|
||||
*/
|
||||
class DualInflowControlManagerMetricsTest {
|
||||
|
||||
@@ -68,6 +69,36 @@ class DualInflowControlManagerMetricsTest {
|
||||
return new CustomGroupBucket(stableBucket(100), exhaustedBucket(1), makeGroupVO(groupId, groupId + "-name"));
|
||||
}
|
||||
|
||||
private InflowTargetVO makeTargetVO(String name) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(name);
|
||||
vo.setThresholdPerSecond(100);
|
||||
vo.setThreshold(10000);
|
||||
vo.setThresholdTimeUnit("DAY");
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private DualCustomBucket passDualBucket(String name) {
|
||||
return new DualCustomBucket(stableBucket(100), null, makeTargetVO(name));
|
||||
}
|
||||
|
||||
private DualCustomBucket blockedPerSecondDualBucket(String name) {
|
||||
return new DualCustomBucket(exhaustedBucket(1), null, makeTargetVO(name));
|
||||
}
|
||||
|
||||
private DualCustomBucket blockedThresholdDualBucket(String name) {
|
||||
return new DualCustomBucket(stableBucket(100), exhaustedBucket(1), makeTargetVO(name));
|
||||
}
|
||||
|
||||
private void injectAdapterMap(Map<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
|
||||
}
|
||||
|
||||
private void injectInterfaceMap(Map<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
|
||||
}
|
||||
|
||||
private void injectGroupBucketList(Map<String, CustomGroupBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "groupBucketList", map);
|
||||
}
|
||||
@@ -315,4 +346,246 @@ class DualInflowControlManagerMetricsTest {
|
||||
assertNotNull(manager.getGroupMetrics("G1"));
|
||||
assertNotNull(manager.getGroupMetrics("G2"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// isAdapterPassDetail() — 카운터 증가
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void isAdapterPassDetail_bucketNotRegistered_noMetricsCreated() {
|
||||
manager.isAdapterPassDetail("UNKNOWN");
|
||||
|
||||
assertNull(manager.getAdapterMetrics("UNKNOWN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAdapterPassDetail_pass_incrementsAllowed() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", passDualBucket("A1"));
|
||||
injectAdapterMap(map);
|
||||
|
||||
String result = manager.isAdapterPassDetail("A1");
|
||||
|
||||
assertNull(result);
|
||||
DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
|
||||
assertNotNull(m);
|
||||
assertEquals(1, m.allowed.get());
|
||||
assertEquals(0, m.rejectedPerSecond.get());
|
||||
assertEquals(0, m.rejectedThreshold.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAdapterPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", blockedPerSecondDualBucket("A1"));
|
||||
injectAdapterMap(map);
|
||||
|
||||
String result = manager.isAdapterPassDetail("A1");
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, result);
|
||||
DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
|
||||
assertEquals(0, m.allowed.get());
|
||||
assertEquals(1, m.rejectedPerSecond.get());
|
||||
assertEquals(0, m.rejectedThreshold.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAdapterPassDetail_blockedThreshold_incrementsRejectedThreshold() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", blockedThresholdDualBucket("A1"));
|
||||
injectAdapterMap(map);
|
||||
|
||||
String result = manager.isAdapterPassDetail("A1");
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, result);
|
||||
DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
|
||||
assertEquals(0, m.allowed.get());
|
||||
assertEquals(0, m.rejectedPerSecond.get());
|
||||
assertEquals(1, m.rejectedThreshold.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAdapterPassDetail_multipleCalls_countersAccumulate() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
LocalBucket b = stableBucket(3);
|
||||
map.put("A1", new DualCustomBucket(b, null, makeTargetVO("A1")));
|
||||
injectAdapterMap(map);
|
||||
|
||||
for (int i = 0; i < 5; i++) manager.isAdapterPassDetail("A1");
|
||||
|
||||
DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
|
||||
assertEquals(3, m.allowed.get());
|
||||
assertEquals(2, m.rejectedPerSecond.get());
|
||||
assertEquals(5, m.allowed.get() + m.rejectedPerSecond.get() + m.rejectedThreshold.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// isInterfacePassDetail() — 카운터 증가
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void isInterfacePassDetail_bucketNotRegistered_noMetricsCreated() {
|
||||
manager.isInterfacePassDetail("UNKNOWN");
|
||||
|
||||
assertNull(manager.getInterfaceMetrics("UNKNOWN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInterfacePassDetail_pass_incrementsAllowed() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF1", passDualBucket("IF1"));
|
||||
injectInterfaceMap(map);
|
||||
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
|
||||
assertEquals(1, manager.getInterfaceMetrics("IF1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInterfacePassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF1", blockedPerSecondDualBucket("IF1"));
|
||||
injectInterfaceMap(map);
|
||||
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
|
||||
assertEquals(1, manager.getInterfaceMetrics("IF1").rejectedPerSecond.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isInterfacePassDetail_differentInterfaces_independentMetrics() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF1", passDualBucket("IF1"));
|
||||
map.put("IF2", blockedPerSecondDualBucket("IF2"));
|
||||
injectInterfaceMap(map);
|
||||
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
manager.isInterfacePassDetail("IF2");
|
||||
|
||||
assertEquals(2, manager.getInterfaceMetrics("IF1").allowed.get());
|
||||
assertEquals(1, manager.getInterfaceMetrics("IF2").rejectedPerSecond.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getAllXxxMetrics — 조회 및 불변성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getAllAdapterMetrics_afterCalls_containsEntries() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", passDualBucket("A1"));
|
||||
map.put("A2", passDualBucket("A2"));
|
||||
injectAdapterMap(map);
|
||||
|
||||
manager.isAdapterPassDetail("A1");
|
||||
manager.isAdapterPassDetail("A2");
|
||||
|
||||
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllAdapterMetrics();
|
||||
assertEquals(2, all.size());
|
||||
assertTrue(all.containsKey("A1"));
|
||||
assertTrue(all.containsKey("A2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllAdapterMetrics_returnsUnmodifiableView() {
|
||||
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllAdapterMetrics();
|
||||
assertThrows(UnsupportedOperationException.class, () -> all.put("NEW", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllInterfaceMetrics_empty_returnsEmptyMap() {
|
||||
assertTrue(manager.getAllInterfaceMetrics().isEmpty());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// resetXxxMetrics / resetAllXxxMetrics
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void resetAdapterMetrics_resetsTargetAdapter() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", passDualBucket("A1"));
|
||||
map.put("A2", passDualBucket("A2"));
|
||||
injectAdapterMap(map);
|
||||
|
||||
manager.isAdapterPassDetail("A1");
|
||||
manager.isAdapterPassDetail("A1");
|
||||
manager.isAdapterPassDetail("A2");
|
||||
|
||||
manager.resetAdapterMetrics("A1");
|
||||
|
||||
assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
|
||||
assertEquals(1, manager.getAdapterMetrics("A2").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAdapterMetrics_nonExistent_noException() {
|
||||
assertDoesNotThrow(() -> manager.resetAdapterMetrics("NONE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAllAdapterMetrics_resetsAll() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", passDualBucket("A1"));
|
||||
map.put("A2", passDualBucket("A2"));
|
||||
injectAdapterMap(map);
|
||||
|
||||
manager.isAdapterPassDetail("A1");
|
||||
manager.isAdapterPassDetail("A2");
|
||||
manager.resetAllAdapterMetrics();
|
||||
|
||||
assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
|
||||
assertEquals(0, manager.getAdapterMetrics("A2").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetInterfaceMetrics_resetsTargetInterface() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF1", passDualBucket("IF1"));
|
||||
injectInterfaceMap(map);
|
||||
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
manager.resetInterfaceMetrics("IF1");
|
||||
|
||||
assertEquals(0, manager.getInterfaceMetrics("IF1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAllInterfaceMetrics_emptyMap_noException() {
|
||||
assertDoesNotThrow(() -> manager.resetAllInterfaceMetrics());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// remove 시 메트릭 자동 삭제
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void removeAdapter_cleansUpMetrics() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("A1", passDualBucket("A1"));
|
||||
ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
|
||||
|
||||
manager.isAdapterPassDetail("A1");
|
||||
assertNotNull(manager.getAdapterMetrics("A1"));
|
||||
|
||||
manager.removeAdapter("A1");
|
||||
|
||||
assertNull(manager.getAdapterMetrics("A1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeInterface_cleansUpMetrics() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("IF1", passDualBucket("IF1"));
|
||||
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
|
||||
|
||||
manager.isInterfacePassDetail("IF1");
|
||||
assertNotNull(manager.getInterfaceMetrics("IF1"));
|
||||
|
||||
manager.removeInterface("IF1");
|
||||
|
||||
assertNull(manager.getInterfaceMetrics("IF1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* AESCryptoModuleExtension 단위 테스트
|
||||
* Spring 컨텍스트 없이 순수 암복호화 로직만 검증한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class AESCryptoModuleExtensionTest {
|
||||
|
||||
private static final byte[] KEY_128 = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] KEY_256 = "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] IV_16 = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] PLAIN = "암호화 테스트 평문 데이터입니다.".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// =========================================================================
|
||||
// 1. CBC 모드
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. AES/CBC/PKCS5Padding 128bit — 암복호화 라운드트립")
|
||||
void testCbc128_roundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertNotNull(encrypted);
|
||||
assertFalse(Arrays.equals(PLAIN, encrypted), "암호문은 평문과 달라야 한다");
|
||||
assertArrayEquals(PLAIN, decrypted, "복호화 결과가 원문과 일치해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. AES/CBC/PKCS5Padding 256bit — 암복호화 라운드트립")
|
||||
void testCbc256_roundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_256, KEY_256);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. AES/CBC — 암호화 결과는 블록 크기(16바이트)의 배수")
|
||||
void testCbc_ciphertextIsMultipleOfBlockSize() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertEquals(0, encrypted.length % 16, "CBC 암호문 길이는 16 바이트 배수여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. AES/CBC — 잘못된 키로 복호화 시 예외 발생")
|
||||
void testCbc_wrongKeyThrowsException() throws Exception {
|
||||
AESCryptoModuleExtension encExt = new AESCryptoModuleExtension();
|
||||
encExt.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
byte[] encrypted = encExt.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
byte[] wrongKey = "wrongkey12345678".getBytes(StandardCharsets.UTF_8);
|
||||
AESCryptoModuleExtension decExt = new AESCryptoModuleExtension();
|
||||
decExt.init("AES", "CBC", "PKCS5Padding", IV_16, wrongKey, wrongKey);
|
||||
|
||||
assertThrows(Exception.class, () -> decExt.decrypt(encrypted, 0, encrypted.length),
|
||||
"잘못된 키로 복호화 시 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. GCM 모드
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. AES/GCM/NoPadding 128bit — 암복호화 라운드트립")
|
||||
void testGcm128_roundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertNotNull(encrypted);
|
||||
assertArrayEquals(PLAIN, decrypted, "GCM 복호화 결과가 원문과 일치해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. AES/GCM — 암호화마다 랜덤 Nonce → 동일 평문도 다른 암호문 생성")
|
||||
void testGcm_randomNonce_differentCiphertextEachCall() throws Exception {
|
||||
AESCryptoModuleExtension ext1 = new AESCryptoModuleExtension();
|
||||
ext1.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
AESCryptoModuleExtension ext2 = new AESCryptoModuleExtension();
|
||||
ext2.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] enc1 = ext1.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] enc2 = ext2.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertFalse(Arrays.equals(enc1, enc2), "GCM은 랜덤 Nonce로 매번 다른 암호문을 생성해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. AES/GCM — 암호문 앞 12바이트가 Nonce")
|
||||
void testGcm_ciphertextStartsWithNonce() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertTrue(encrypted.length > 12, "GCM 암호문은 Nonce(12바이트) + 암호문 + Tag(16바이트) 구조여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4. AES/GCM — 256bit 키 라운드트립")
|
||||
void testGcm256_roundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "GCM", "NoPadding", IV_16, KEY_256, KEY_256);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. init 편의 메서드 (IV 없는 버전)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. init(alg, mode, pad, encKey, decKey) — IV null로 CBC 초기화 시 예외")
|
||||
void testInitWithoutIv_cbcThrowsException() {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
assertThrows(Exception.class,
|
||||
() -> ext.init("AES", "CBC", "PKCS5Padding", KEY_128, KEY_128),
|
||||
"IV 없이 CBC 초기화 시 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. 바이트 배열 오프셋/길이 encrypt/decrypt
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. encrypt(src, sindex, slength, dst, dindex) — 버퍼 직접 지정 라운드트립")
|
||||
void testEncryptDecryptWithBuffer() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] dst = new byte[256];
|
||||
int encLen = ext.encrypt(PLAIN, 0, PLAIN.length, dst, 0);
|
||||
|
||||
byte[] decDst = new byte[256];
|
||||
int decLen = ext.decrypt(dst, 0, encLen, decDst, 0);
|
||||
|
||||
assertArrayEquals(PLAIN, Arrays.copyOf(decDst, decLen));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. 프로바이더 파라미터
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. provider=BC 명시 — CBC 암복호화 라운드트립")
|
||||
void testProvider_bc_cbcRoundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128, "BC");
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted, "BC 프로바이더 명시 시 CBC 암복호화가 성공해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. provider=BC 명시 — GCM 암복호화 라운드트립")
|
||||
void testProvider_bc_gcmRoundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128, "BC");
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted, "BC 프로바이더 명시 시 GCM 암복호화가 성공해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3. provider null — GCM 기본 동작(BC) 라운드트립")
|
||||
void testProvider_null_gcmDefaultBcRoundTrip() throws Exception {
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128, (String) null);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128, (String) null);
|
||||
|
||||
byte[] encrypted = enc.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = dec.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted, "provider=null이면 GCM은 BC 기본값으로 동작해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-4. provider 파라미터 없는 CBC init — provider 없는 버전과 동일 결과")
|
||||
void testProvider_cbcWithAndWithoutProvider_sameResult() throws Exception {
|
||||
AESCryptoModuleExtension ext1 = new AESCryptoModuleExtension();
|
||||
ext1.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
AESCryptoModuleExtension ext2 = new AESCryptoModuleExtension();
|
||||
ext2.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128, "BC");
|
||||
|
||||
byte[] enc1 = ext1.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] enc2 = ext2.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertArrayEquals(enc1, enc2, "동일 키/IV에서 provider 유무와 무관하게 CBC 암호문이 같아야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 6. AAD (Additional Authenticated Data)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("6-1. GCM — 명시적 AAD로 암복호화 라운드트립")
|
||||
void testGcm_explicitAad_roundTrip() throws Exception {
|
||||
byte[] aad = "header-context-data".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = enc.encrypt(PLAIN, 0, PLAIN.length, aad);
|
||||
byte[] decrypted = dec.decrypt(encrypted, 0, encrypted.length, aad);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted, "명시적 AAD로 암복호화 라운드트립이 성공해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-2. GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
|
||||
void testGcm_nullAad_defaultIvAsAad() throws Exception {
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
// aad=null → IV_16을 AAD로 사용 (기본 동작)
|
||||
byte[] encrypted = enc.encrypt(PLAIN, 0, PLAIN.length, null);
|
||||
byte[] decrypted = dec.decrypt(encrypted, 0, encrypted.length, null);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted, "AAD=null이면 IV를 AAD로 사용하는 기본 동작이어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-3. GCM — encrypt(aad=null)과 encrypt() 결과가 복호화 상호 호환")
|
||||
void testGcm_nullAadCompatibleWithNoAadMethod() throws Exception {
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encryptedByNoArg = enc.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decryptedByNullAad = dec.decrypt(encryptedByNoArg, 0, encryptedByNoArg.length, null);
|
||||
|
||||
assertArrayEquals(PLAIN, decryptedByNullAad, "encrypt()와 decrypt(null)은 상호 호환되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-4. GCM — 다른 AAD로 복호화 시 인증 실패 예외")
|
||||
void testGcm_wrongAad_throwsAuthException() throws Exception {
|
||||
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
byte[] encrypted = enc.encrypt(PLAIN, 0, PLAIN.length, aad);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> dec.decrypt(encrypted, 0, encrypted.length, wrongAad),
|
||||
"잘못된 AAD로 GCM 복호화 시 인증 실패 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-5. GCM — AAD 있는 암호문을 AAD 없이(null IV) 복호화 시 인증 실패")
|
||||
void testGcm_encryptedWithAad_decryptWithoutAad_fails() throws Exception {
|
||||
byte[] aad = "must-have-aad".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", null, KEY_128, KEY_128);
|
||||
byte[] encrypted = enc.encrypt(PLAIN, 0, PLAIN.length, aad);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", null, KEY_128, KEY_128);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> dec.decrypt(encrypted, 0, encrypted.length, null),
|
||||
"AAD로 암호화된 데이터를 AAD 없이 복호화하면 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-6. CBC — AAD 파라미터는 무시되고 정상 동작")
|
||||
void testCbc_aadIgnored_normalOperation() throws Exception {
|
||||
byte[] aad = "ignored-in-cbc".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length, aad);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length, aad);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted, "CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 7. 오프셋/길이 없는 편의 메서드
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("7-1. CBC — encrypt(byte[]) / decrypt(byte[]) 라운드트립")
|
||||
void testCbc_noOffsetEncryptDecrypt_roundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN);
|
||||
byte[] decrypted = ext.decrypt(encrypted);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-2. GCM — encrypt(byte[]) / decrypt(byte[]) 라운드트립")
|
||||
void testGcm_noOffsetEncryptDecrypt_roundTrip() throws Exception {
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN);
|
||||
byte[] decrypted = ext.decrypt(encrypted);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-3. GCM — encrypt(byte[], aad) / decrypt(byte[], aad) 라운드트립")
|
||||
void testGcm_noOffsetWithAad_roundTrip() throws Exception {
|
||||
byte[] aad = "request-header-ctx".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
AESCryptoModuleExtension enc = new AESCryptoModuleExtension();
|
||||
enc.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
AESCryptoModuleExtension dec = new AESCryptoModuleExtension();
|
||||
dec.init("AES", "GCM", "NoPadding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = enc.encrypt(PLAIN, aad);
|
||||
byte[] decrypted = dec.decrypt(encrypted, aad);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-4. CBC — encrypt(byte[], aad) 는 AAD 무시 후 정상 라운드트립")
|
||||
void testCbc_noOffsetWithAad_aadIgnored() throws Exception {
|
||||
byte[] aad = "ignored".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
AESCryptoModuleExtension ext = new AESCryptoModuleExtension();
|
||||
ext.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, aad);
|
||||
byte[] decrypted = ext.decrypt(encrypted, aad);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-5. encrypt(byte[])와 encrypt(src, 0, src.length) 결과 동일 — CBC")
|
||||
void testCbc_noOffsetEquivalentToFullOffset() throws Exception {
|
||||
AESCryptoModuleExtension ext1 = new AESCryptoModuleExtension();
|
||||
ext1.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
AESCryptoModuleExtension ext2 = new AESCryptoModuleExtension();
|
||||
ext2.init("AES", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] enc1 = ext1.encrypt(PLAIN);
|
||||
byte[] enc2 = ext2.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertArrayEquals(enc1, enc2, "편의 메서드와 오프셋 메서드의 결과가 동일해야 한다");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* ARIACryptoModuleExtension 단위 테스트
|
||||
* BouncyCastle ARIA 암복호화 로직을 검증한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class ARIACryptoModuleExtensionTest {
|
||||
|
||||
private static final byte[] KEY_128 = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] KEY_256 = "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] IV_16 = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] PLAIN = "ARIA 암호화 테스트 평문입니다.".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// =========================================================================
|
||||
// 1. CBC 모드
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. ARIA/CBC/PKCS5Padding 128bit — 암복호화 라운드트립")
|
||||
void testCbc128_roundTrip() throws Exception {
|
||||
ARIACryptoModuleExtension ext = new ARIACryptoModuleExtension();
|
||||
ext.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertNotNull(encrypted);
|
||||
assertFalse(Arrays.equals(PLAIN, encrypted), "암호문은 평문과 달라야 한다");
|
||||
assertArrayEquals(PLAIN, decrypted, "복호화 결과가 원문과 일치해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. ARIA/CBC/PKCS5Padding 256bit — 암복호화 라운드트립")
|
||||
void testCbc256_roundTrip() throws Exception {
|
||||
ARIACryptoModuleExtension ext = new ARIACryptoModuleExtension();
|
||||
ext.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_256, KEY_256);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] decrypted = ext.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(PLAIN, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. ARIA/CBC — 암호화 결과는 블록 크기(16바이트)의 배수")
|
||||
void testCbc_ciphertextIsMultipleOfBlockSize() throws Exception {
|
||||
ARIACryptoModuleExtension ext = new ARIACryptoModuleExtension();
|
||||
ext.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] encrypted = ext.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertEquals(0, encrypted.length % 16, "ARIA CBC 암호문 길이는 16 바이트 배수여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. ARIA/CBC — 다른 IV로 암호화 시 다른 암호문 생성")
|
||||
void testCbc_differentIv_differentCiphertext() throws Exception {
|
||||
byte[] iv2 = "9876543210fedcba".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
ARIACryptoModuleExtension ext1 = new ARIACryptoModuleExtension();
|
||||
ext1.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
ARIACryptoModuleExtension ext2 = new ARIACryptoModuleExtension();
|
||||
ext2.init("ARIA", "CBC", "PKCS5Padding", iv2, KEY_128, KEY_128);
|
||||
|
||||
byte[] enc1 = ext1.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] enc2 = ext2.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
assertFalse(Arrays.equals(enc1, enc2), "IV가 다르면 암호문도 달라야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. ARIA/CBC — 잘못된 키로 복호화 시 예외 발생")
|
||||
void testCbc_wrongKeyThrowsException() throws Exception {
|
||||
ARIACryptoModuleExtension encExt = new ARIACryptoModuleExtension();
|
||||
encExt.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
byte[] encrypted = encExt.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
byte[] wrongKey = "wrongkey12345678".getBytes(StandardCharsets.UTF_8);
|
||||
ARIACryptoModuleExtension decExt = new ARIACryptoModuleExtension();
|
||||
decExt.init("ARIA", "CBC", "PKCS5Padding", IV_16, wrongKey, wrongKey);
|
||||
|
||||
assertThrows(Exception.class, () -> decExt.decrypt(encrypted, 0, encrypted.length),
|
||||
"잘못된 키로 복호화 시 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. 암/복호화 키 분리
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. init(alg, mode, pad, encKey, decKey) — IV null 편의 메서드")
|
||||
void testInitWithoutIv() throws Exception {
|
||||
ARIACryptoModuleExtension ext = new ARIACryptoModuleExtension();
|
||||
// IV 없이 초기화 (null → IvParameterSpec도 null → CBC에서 예외)
|
||||
assertThrows(Exception.class,
|
||||
() -> ext.init("ARIA", "CBC", "PKCS5Padding", KEY_128, KEY_128),
|
||||
"IV 없이 CBC 초기화 시 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. 바이트 배열 오프셋/길이 encrypt/decrypt
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. encrypt(src, sindex, slength, dst, dindex) — 버퍼 직접 지정 라운드트립")
|
||||
void testEncryptDecryptWithBuffer() throws Exception {
|
||||
ARIACryptoModuleExtension ext = new ARIACryptoModuleExtension();
|
||||
ext.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] dst = new byte[256];
|
||||
int encLen = ext.encrypt(PLAIN, 0, PLAIN.length, dst, 0);
|
||||
|
||||
byte[] decDst = new byte[256];
|
||||
int decLen = ext.decrypt(dst, 0, encLen, decDst, 0);
|
||||
|
||||
assertArrayEquals(PLAIN, Arrays.copyOf(decDst, decLen));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. BouncyCastle Provider 자동 등록
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. 동일 평문을 여러 번 암호화 — 각각 새 인스턴스로 독립 처리")
|
||||
void testMultipleInstances_independentEncryption() throws Exception {
|
||||
ARIACryptoModuleExtension ext1 = new ARIACryptoModuleExtension();
|
||||
ext1.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
ARIACryptoModuleExtension ext2 = new ARIACryptoModuleExtension();
|
||||
ext2.init("ARIA", "CBC", "PKCS5Padding", IV_16, KEY_128, KEY_128);
|
||||
|
||||
byte[] enc1 = ext1.encrypt(PLAIN, 0, PLAIN.length);
|
||||
byte[] enc2 = ext2.encrypt(PLAIN, 0, PLAIN.length);
|
||||
|
||||
// 같은 키/IV → 같은 암호문
|
||||
assertArrayEquals(enc1, enc2, "동일 키/IV라면 암호문이 동일해야 한다");
|
||||
|
||||
// 각자 복호화
|
||||
assertArrayEquals(PLAIN, ext1.decrypt(enc1, 0, enc1.length));
|
||||
assertArrayEquals(PLAIN, ext2.decrypt(enc2, 0, enc2.length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
|
||||
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
|
||||
/**
|
||||
* CryptoModuleManager 단위 테스트
|
||||
* DB / 실 Spring 컨텍스트 없이 Mock으로 동작한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class CryptoModuleManagerTest {
|
||||
|
||||
private static final byte[] KEY_128 = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] IV_16 = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String ENC_KEY_HEX = toHex(KEY_128);
|
||||
private static final String IV_HEX = toHex(IV_16);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static CryptoModuleConfigLoader mockLoader;
|
||||
private static HsmCryptoService mockHsmCryptoService;
|
||||
private static CryptoModuleManager manager;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockLoader = mock(CryptoModuleConfigLoader.class);
|
||||
mockHsmCryptoService = mock(HsmCryptoService.class);
|
||||
|
||||
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
|
||||
when(mockLoader.findAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
manager = ctor.newInstance();
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
|
||||
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
manager.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void resetMocks() {
|
||||
reset(mockLoader);
|
||||
when(mockLoader.findAll()).thenReturn(Collections.emptyList());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. start / stop Lifecycle
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. start() 후 isStarted() = true")
|
||||
void testIsStarted() {
|
||||
assertTrue(manager.isStarted());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. STATIC 키 — createExtension
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. STATIC AES/CBC — createExtension 반환 및 암복호화 동작")
|
||||
void testCreateExtension_staticAesCbc_roundTrip() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("AES_CBC", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
byte[] plain = "테스트 평문".getBytes(StandardCharsets.UTF_8);
|
||||
CryptoModuleExtension ext = manager.createExtension("AES_CBC");
|
||||
byte[] encrypted = ext.encrypt(plain, 0, plain.length);
|
||||
|
||||
CryptoModuleExtension extDec = manager.createExtension("AES_CBC");
|
||||
byte[] decrypted = extDec.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(plain, decrypted, "STATIC AES/CBC 암복호화 라운드트립 성공해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. STATIC ARIA/CBC — createExtension 반환 및 암복호화 동작")
|
||||
void testCreateExtension_staticAriaCbc_roundTrip() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("ARIA_CBC", "ARIA", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
byte[] plain = "ARIA 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
CryptoModuleExtension ext = manager.createExtension("ARIA_CBC");
|
||||
byte[] encrypted = ext.encrypt(plain, 0, plain.length);
|
||||
|
||||
CryptoModuleExtension extDec = manager.createExtension("ARIA_CBC");
|
||||
byte[] decrypted = extDec.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. 미등록 cryptoName — IllegalArgumentException")
|
||||
void testCreateExtension_unknownName_throwsException() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> manager.createExtension("UNKNOWN_MODULE"),
|
||||
"미등록 cryptoName은 IllegalArgumentException이어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4. use_yn=N 설정 — configMap 미등록")
|
||||
void testLoadAll_disabledConfig_notRegistered() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("DISABLED_MOD", "AES", "CBC", "PKCS5Padding");
|
||||
config.setUseYn("N");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> manager.createExtension("DISABLED_MOD"),
|
||||
"use_yn=N 설정은 로드되지 않아야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. DYNAMIC 키 — createExtension with runtimeContext
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. DYNAMIC AES/CBC — runtimeContext 전달 시 암복호화 동작")
|
||||
void testCreateExtension_dynamicAesCbc_roundTrip() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DYN_AES_CBC", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> ctx = new HashMap<>();
|
||||
ctx.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
|
||||
byte[] plain = "DYNAMIC 키 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
CryptoModuleExtension ext = manager.createExtension("DYN_AES_CBC", ctx);
|
||||
byte[] encrypted = ext.encrypt(plain, 0, plain.length);
|
||||
|
||||
CryptoModuleExtension extDec = manager.createExtension("DYN_AES_CBC", ctx);
|
||||
byte[] decrypted = extDec.decrypt(encrypted, 0, encrypted.length);
|
||||
|
||||
assertArrayEquals(plain, decrypted, "DYNAMIC AES/CBC 암복호화 라운드트립 성공해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. DYNAMIC — 동일 runtimeContext로 2회 호출 시 HSM은 1회만 호출 (캐시 적용)")
|
||||
void testCreateExtension_dynamicCached_hsmCalledOnce() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DYN_CACHE", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "cachedKeyValue01");
|
||||
|
||||
manager.createExtension("DYN_CACHE", runtimeCtx);
|
||||
manager.createExtension("DYN_CACHE", runtimeCtx);
|
||||
|
||||
verify(mockHsmCryptoService, times(1)).getSecretKey(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. DYNAMIC — 잘못된 전략 FQCN — IllegalArgumentException")
|
||||
void testCreateExtension_invalidStrategyFqcn_throwsException() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DYN_BAD_FQCN", "AES", "CBC", "PKCS5Padding");
|
||||
config.setKeyDerivStrategy("com.example.NonExistentStrategy");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> manager.createExtension("DYN_BAD_FQCN", runtimeCtx),
|
||||
"존재하지 않는 FQCN이면 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-4. DYNAMIC — 동일 FQCN 반복 호출 시 strategyCache에서 재사용")
|
||||
void testCreateExtension_sameFqcn_strategyCacheReused() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DYN_CACHE_STRAT", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
|
||||
manager.createExtension("DYN_CACHE_STRAT", runtimeCtx);
|
||||
manager.createExtension("DYN_CACHE_STRAT", runtimeCtx);
|
||||
|
||||
Field stratCacheField = CryptoModuleManager.class.getDeclaredField("strategyCache");
|
||||
stratCacheField.setAccessible(true);
|
||||
Map<?, ?> stratCache = (Map<?, ?>) stratCacheField.get(manager);
|
||||
|
||||
assertEquals(1, stratCache.size(), "동일 FQCN은 strategyCache에 하나만 존재해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-5. DYNAMIC — 다른 runtimeContext 값은 다른 키 도출 (동적 키 캐시 미사용)")
|
||||
void testCreateExtension_dynamicDifferentContext_differentKey() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DYN_DIFF", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> ctx1 = new HashMap<>();
|
||||
ctx1.put("X-Api-Enc-Key", "keyValueAAA00001");
|
||||
|
||||
Map<String, String> ctx2 = new HashMap<>();
|
||||
ctx2.put("X-Api-Enc-Key", "keyValueBBB00001");
|
||||
|
||||
manager.createExtension("DYN_DIFF", ctx1);
|
||||
manager.createExtension("DYN_DIFF", ctx2);
|
||||
|
||||
// 컨텍스트 값이 달라 동적 키 캐시 미사용 → HSM 2회 호출
|
||||
verify(mockHsmCryptoService, times(2)).getSecretKey(anyString());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. reload
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. reload(cryptoId) — 변경된 설정 반영")
|
||||
void testReload_specificId_configUpdated() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("RELOAD_MOD", "AES", "CBC", "PKCS5Padding");
|
||||
config.setCryptoId("reload-id-001");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
CryptoModuleConfig updated = buildStaticConfig("RELOAD_MOD", "ARIA", "CBC", "PKCS5Padding");
|
||||
updated.setCryptoId("reload-id-001");
|
||||
when(mockLoader.findById("reload-id-001")).thenReturn(Optional.of(updated));
|
||||
|
||||
manager.reload("reload-id-001");
|
||||
|
||||
// 변경된 설정(ARIA)으로 암복호화 가능한지 확인
|
||||
byte[] plain = "리로드 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
CryptoModuleExtension ext = manager.createExtension("RELOAD_MOD");
|
||||
assertNotNull(ext);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. reload(cryptoId) use_yn=N — configMap에서 제거")
|
||||
void testReload_specificId_disabled_removedFromMap() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("RM_MOD", "AES", "CBC", "PKCS5Padding");
|
||||
config.setCryptoId("rm-id-001");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
CryptoModuleConfig disabled = buildStaticConfig("RM_MOD", "AES", "CBC", "PKCS5Padding");
|
||||
disabled.setCryptoId("rm-id-001");
|
||||
disabled.setUseYn("N");
|
||||
when(mockLoader.findById("rm-id-001")).thenReturn(Optional.of(disabled));
|
||||
|
||||
manager.reload("rm-id-001");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> manager.createExtension("RM_MOD"),
|
||||
"비활성화된 모듈은 configMap에서 제거되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private void reload() throws Exception {
|
||||
clearField("configMap");
|
||||
clearField("dynamicKeyCache");
|
||||
clearField("strategyCache");
|
||||
|
||||
Method loadAll = CryptoModuleManager.class.getDeclaredMethod("loadAll");
|
||||
loadAll.setAccessible(true);
|
||||
loadAll.invoke(manager);
|
||||
|
||||
reset(mockHsmCryptoService);
|
||||
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
|
||||
}
|
||||
|
||||
private void clearField(String fieldName) throws Exception {
|
||||
Field field = CryptoModuleManager.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
((Map<?, ?>) field.get(manager)).clear();
|
||||
}
|
||||
|
||||
private CryptoModuleConfig buildStaticConfig(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(java.util.UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setIvHex(IV_HEX);
|
||||
c.setKeySourceType("STATIC");
|
||||
c.setEncKeyHex(ENC_KEY_HEX);
|
||||
c.setDecKeyHex(ENC_KEY_HEX);
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private CryptoModuleConfig buildDynamicConfig(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(java.util.UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setIvHex(IV_HEX);
|
||||
c.setKeySourceType("DYNAMIC");
|
||||
c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy");
|
||||
c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\",\"contextKey\":\"X-Api-Enc-Key\",\"offset\":\"0\",\"length\":\"16\"}");
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private static String toHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) sb.append(String.format("%02x", b));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** MapStruct APT 없이도 동작하는 인라인 매퍼 구현체 */
|
||||
private static CryptoModuleConfigMapper testMapper() {
|
||||
return new CryptoModuleConfigMapper() {
|
||||
@Override
|
||||
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
|
||||
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
|
||||
vo.setCryptoId(e.getCryptoId());
|
||||
vo.setCryptoName(e.getCryptoName());
|
||||
vo.setCryptoDesc(e.getCryptoDesc());
|
||||
vo.setAlgType(e.getAlgType());
|
||||
vo.setCipherMode(e.getCipherMode());
|
||||
vo.setPadding(e.getPadding());
|
||||
vo.setIvHex(e.getIvHex());
|
||||
vo.setKeySourceType(e.getKeySourceType());
|
||||
vo.setEncKeyHex(e.getEncKeyHex());
|
||||
vo.setDecKeyHex(e.getDecKeyHex());
|
||||
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
|
||||
vo.setKeyDerivParams(e.getKeyDerivParams());
|
||||
vo.setCacheYn(e.getCacheYn());
|
||||
vo.setCacheTtlSec(e.getCacheTtlSec());
|
||||
vo.setUseYn(e.getUseYn());
|
||||
return vo;
|
||||
}
|
||||
@Override
|
||||
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
|
||||
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
|
||||
/**
|
||||
* CryptoModuleService 단위 테스트
|
||||
* CryptoModuleManager를 통한 암복호화 진입점 전체 흐름을 검증한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class CryptoModuleServiceTest {
|
||||
|
||||
private static final byte[] KEY_128 = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] IV_16 = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String ENC_KEY_HEX = toHex(KEY_128);
|
||||
private static final String IV_HEX = toHex(IV_16);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static CryptoModuleManager manager;
|
||||
private static CryptoModuleService service;
|
||||
private static CryptoModuleConfigLoader mockLoader;
|
||||
private static HsmCryptoService mockHsm;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockLoader = mock(CryptoModuleConfigLoader.class);
|
||||
mockHsm = mock(HsmCryptoService.class);
|
||||
|
||||
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
|
||||
System.out.println("masterKey:"+new String(KEY_128));
|
||||
when(mockHsm.getSecretKey(anyString())).thenReturn(masterKey);
|
||||
|
||||
CryptoModuleConfig aes = buildStatic("AES_SVC", "AES", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig aria = buildStatic("ARIA_SVC", "ARIA", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig ecb = buildStaticNoIv("AES_ECB_SVC", "AES", "ECB", "NoPadding");
|
||||
CryptoModuleConfig dyn = buildDynamic("DYN_SVC", "AES", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig gcm = buildStatic("AES_GCM_SVC", "AES", "GCM", "NoPadding");
|
||||
CryptoModuleConfig gcmDyn = buildDynamic("DYN_GCM_SVC", "AES", "GCM", "NoPadding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(aes, aria, ecb, dyn, gcm, gcmDyn));
|
||||
|
||||
Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
manager = ctor.newInstance();
|
||||
service = new CryptoModuleService();
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
|
||||
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsm);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleService", service);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
manager.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. STATIC 키 — encrypt / decrypt
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. STATIC AES/CBC — encrypt → decrypt 라운드트립")
|
||||
void testStaticAes_encryptDecrypt() throws Exception {
|
||||
byte[] plain = "CryptoModuleService AES 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_SVC", plain);
|
||||
byte[] decrypted = service.decrypt("AES_SVC", encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. STATIC ARIA/CBC — encrypt → decrypt 라운드트립")
|
||||
void testStaticAria_encryptDecrypt() throws Exception {
|
||||
byte[] plain = "CryptoModuleService ARIA 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("ARIA_SVC", plain);
|
||||
byte[] decrypted = service.decrypt("ARIA_SVC", encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 암호문은 평문과 다른 바이트 배열")
|
||||
void testEncrypt_ciphertextDiffersFromPlaintext() throws Exception {
|
||||
byte[] plain = "평문입니다.".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_SVC", plain);
|
||||
|
||||
assertFalse(java.util.Arrays.equals(plain, encrypted));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. 미등록 모듈명 — IllegalArgumentException")
|
||||
void testEncrypt_unknownModule_throwsException() {
|
||||
byte[] plain = "test".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.encrypt("UNKNOWN_SVC", plain));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. DYNAMIC 키 — encrypt / decrypt
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. DYNAMIC AES/CBC — runtimeContext 전달 encrypt → decrypt 라운드트립")
|
||||
void testDynamicAes_encryptDecrypt() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] plain = "{\"test\": \"DYNAMIC AES/CBC 테스트\"}".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, encrypted);
|
||||
|
||||
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
|
||||
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
|
||||
System.out.println("DYNAMIC AES/CBC");
|
||||
|
||||
System.out.println("plain:"+new String(plain));
|
||||
System.out.println("encBase64:"+encBase64);
|
||||
System.out.println("decBase64:"+decBase64);
|
||||
System.out.println("plain:"+new String(decrypted));
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. DYNAMIC — 다른 runtimeContext 값으로 복호화 시 실패")
|
||||
void testDynamic_differentContext_decryptFails() throws Exception {
|
||||
Map<String, String> encCtx = new HashMap<>();
|
||||
encCtx.put("X-Api-Enc-Key", "encryptKeyValue0");
|
||||
|
||||
Map<String, String> decCtx = new HashMap<>();
|
||||
decCtx.put("X-Api-Enc-Key", "differentKeyVal0");
|
||||
|
||||
byte[] plain = "키불일치 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] encrypted = service.encrypt("DYN_SVC", encCtx, plain);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> service.decrypt("DYN_SVC", decCtx, encrypted),
|
||||
"다른 컨텍스트 키로 복호화 시 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. AES/ECB/NoPadding
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. AES/ECB/NoPadding — 16바이트 평문 encrypt → decrypt 라운드트립")
|
||||
void testEcb_encryptDecrypt_exactBlock() throws Exception {
|
||||
byte[] plain = "ExactSixteenByte".getBytes(StandardCharsets.UTF_8); // 정확히 16바이트
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_ECB_SVC", plain);
|
||||
byte[] decrypted = service.decrypt("AES_ECB_SVC", encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. AES/ECB/NoPadding — 동일 평문은 항상 동일 암호문 (ECB 특성)")
|
||||
void testEcb_deterministicOutput() throws Exception {
|
||||
byte[] plain = "ExactSixteenByte".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] enc1 = service.encrypt("AES_ECB_SVC", plain);
|
||||
byte[] enc2 = service.encrypt("AES_ECB_SVC", plain);
|
||||
|
||||
assertArrayEquals(enc1, enc2, "ECB는 IV 없으므로 동일 평문 → 동일 암호문");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. AES/ECB/NoPadding — 16 배수 아닌 평문 → 예외")
|
||||
void testEcb_nonBlockAligned_throwsException() {
|
||||
byte[] plain = "NotSixteenBytes!X".getBytes(StandardCharsets.UTF_8); // 17바이트
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> service.encrypt("AES_ECB_SVC", plain),
|
||||
"NoPadding 모드에서 블록 크기 미정렬 평문은 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. STATIC AAD — encrypt(name, aad, plain) / decrypt(name, aad, cipher)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. STATIC AES/GCM — AAD 명시 encrypt → decrypt 라운드트립")
|
||||
void testStaticGcm_explicitAad_roundTrip() throws Exception {
|
||||
byte[] aad = "request-header-ctx".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "GCM AAD 서비스 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
|
||||
byte[] decrypted = service.decrypt("AES_GCM_SVC", aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. STATIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
|
||||
void testStaticGcm_nullAad_usesIvAsAad() throws Exception {
|
||||
byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_GCM_SVC", (byte[]) null, plain);
|
||||
byte[] decrypted = service.decrypt("AES_GCM_SVC", (byte[]) null, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
|
||||
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
|
||||
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
|
||||
|
||||
System.out.println("STATIC AES/GCM");
|
||||
|
||||
System.out.println("plain:"+new String(plain));
|
||||
System.out.println("encBase64:"+encBase64);
|
||||
System.out.println("decBase64:"+decBase64);
|
||||
System.out.println("plain:"+new String(decrypted));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3. STATIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
|
||||
void testStaticGcm_wrongAad_throwsAuthException() throws Exception {
|
||||
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "GCM 인증 실패 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> service.decrypt("AES_GCM_SVC", wrongAad, encrypted),
|
||||
"잘못된 AAD로 GCM 복호화 시 인증 실패 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4. STATIC CBC — AAD 파라미터는 무시되고 정상 복호화")
|
||||
void testStaticCbc_aadIgnored_normalDecrypt() throws Exception {
|
||||
byte[] aad = "ignored-aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_SVC", aad, plain);
|
||||
byte[] decrypted = service.decrypt("AES_SVC", aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted, "CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. DYNAMIC AAD — encrypt(name, ctx, aad, plain) / decrypt(name, ctx, aad, cipher)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. DYNAMIC AES/GCM — runtimeContext + AAD 명시 encrypt → decrypt 라운드트립")
|
||||
void testDynamicGcm_explicitAad_roundTrip() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] aad = "dynamic-gcm-aad-ctx".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, aad, encrypted);
|
||||
|
||||
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
|
||||
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
|
||||
|
||||
System.out.println("DYNAMIC AES/GCM");
|
||||
|
||||
System.out.println("aad:"+new String(aad));
|
||||
System.out.println("plain:"+new String(plain));
|
||||
System.out.println("encBase64:"+encBase64);
|
||||
System.out.println("decBase64:"+decBase64);
|
||||
System.out.println("plain:"+new String(decrypted));
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. DYNAMIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
|
||||
void testDynamicGcm_nullAad_usesIvAsAad() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, null, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, null, encrypted);
|
||||
|
||||
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
|
||||
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
|
||||
System.out.println("DYNAMIC AES/GCM");
|
||||
System.out.println("aad:null");
|
||||
System.out.println("plain:"+new String(plain));
|
||||
System.out.println("encBase64:"+encBase64);
|
||||
System.out.println("decBase64:"+decBase64);
|
||||
System.out.println("plain:"+new String(decrypted));
|
||||
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3. DYNAMIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
|
||||
void testDynamicGcm_wrongAad_throwsAuthException() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "DYNAMIC GCM 인증 실패".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> service.decrypt("DYN_GCM_SVC", runtimeCtx, wrongAad, encrypted),
|
||||
"잘못된 AAD로 DYNAMIC GCM 복호화 시 인증 실패 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-4. DYNAMIC CBC — AAD 파라미터는 무시되고 정상 복호화")
|
||||
void testDynamicCbc_aadIgnored_normalDecrypt() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] aad = "ignored-aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "DYNAMIC CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, aad, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted, "DYNAMIC CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private static CryptoModuleConfig buildStatic(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setIvHex(IV_HEX);
|
||||
c.setKeySourceType("STATIC");
|
||||
c.setEncKeyHex(ENC_KEY_HEX);
|
||||
c.setDecKeyHex(ENC_KEY_HEX);
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private static CryptoModuleConfig buildStaticNoIv(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setKeySourceType("STATIC");
|
||||
c.setEncKeyHex(ENC_KEY_HEX);
|
||||
c.setDecKeyHex(ENC_KEY_HEX);
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setIvHex(IV_HEX);
|
||||
c.setKeySourceType("DYNAMIC");
|
||||
c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy");
|
||||
c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\"}");
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private static String toHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) sb.append(String.format("%02x", b));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** MapStruct APT 없이도 동작하는 인라인 매퍼 구현체 */
|
||||
private static CryptoModuleConfigMapper testMapper() {
|
||||
return new CryptoModuleConfigMapper() {
|
||||
@Override
|
||||
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
|
||||
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
|
||||
vo.setCryptoId(e.getCryptoId());
|
||||
vo.setCryptoName(e.getCryptoName());
|
||||
vo.setCryptoDesc(e.getCryptoDesc());
|
||||
vo.setAlgType(e.getAlgType());
|
||||
vo.setCipherMode(e.getCipherMode());
|
||||
vo.setPadding(e.getPadding());
|
||||
vo.setIvHex(e.getIvHex());
|
||||
vo.setKeySourceType(e.getKeySourceType());
|
||||
vo.setEncKeyHex(e.getEncKeyHex());
|
||||
vo.setDecKeyHex(e.getDecKeyHex());
|
||||
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
|
||||
vo.setKeyDerivParams(e.getKeyDerivParams());
|
||||
vo.setCacheYn(e.getCacheYn());
|
||||
vo.setCacheTtlSec(e.getCacheTtlSec());
|
||||
vo.setUseYn(e.getUseYn());
|
||||
return vo;
|
||||
}
|
||||
@Override
|
||||
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DerivedKeyTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. encKey/decKey 모두 지정 — 각각 독립적으로 반환")
|
||||
void testBothKeysProvided() {
|
||||
byte[] encKey = {1, 2, 3};
|
||||
byte[] decKey = {4, 5, 6};
|
||||
DerivedKey dk = new DerivedKey(encKey, decKey);
|
||||
|
||||
assertArrayEquals(encKey, dk.getEncKey());
|
||||
assertArrayEquals(decKey, dk.getDecKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. decKey null — encKey로 대체")
|
||||
void testNullDecKey_fallsBackToEncKey() {
|
||||
byte[] encKey = {1, 2, 3};
|
||||
DerivedKey dk = new DerivedKey(encKey, null);
|
||||
|
||||
assertArrayEquals(encKey, dk.getEncKey());
|
||||
assertArrayEquals(encKey, dk.getDecKey(), "decKey가 null이면 encKey와 동일해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 대칭 알고리즘 — encKey == decKey 동일 참조 허용")
|
||||
void testSymmetricKey_sameReference() {
|
||||
byte[] key = {0x10, 0x20, 0x30};
|
||||
DerivedKey dk = new DerivedKey(key, key);
|
||||
|
||||
assertSame(dk.getEncKey(), dk.getDecKey());
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HsmContextSha256KeyDerivationStrategy 단위 테스트
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class HsmContextSha256KeyDerivationStrategyTest {
|
||||
|
||||
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
|
||||
private static final byte[] MASTER_KEY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static HsmCryptoService mockHsmCryptoService;
|
||||
private static HsmContextSha256KeyDerivationStrategy strategy;
|
||||
|
||||
private Map<String, String> params;
|
||||
private Map<String, String> runtimeContext;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockHsmCryptoService = mock(HsmCryptoService.class);
|
||||
SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
strategy = new HsmContextSha256KeyDerivationStrategy();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpParams() {
|
||||
params = new HashMap<>();
|
||||
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
|
||||
params.put("contextKey", "X-Api-Group-Seq");
|
||||
|
||||
runtimeContext = new HashMap<>();
|
||||
runtimeContext.put("X-Api-Group-Seq", "GROUP_001");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. deriveKey
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, 길이 32바이트(SHA-256 고정 출력)")
|
||||
void testDeriveKey_validParams_returns32ByteKey() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertNotNull(dk);
|
||||
assertNotNull(dk.getEncKey());
|
||||
assertNotNull(dk.getDecKey());
|
||||
assertEquals(32, dk.getEncKey().length, "SHA-256 출력은 항상 32바이트여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. SHA-256(masterKey || contextValue) 계산 결과 검증")
|
||||
void testDeriveKey_sha256ResultIsCorrect() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
byte[] contextBytes = "GROUP_001".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] combined = new byte[MASTER_KEY.length + contextBytes.length];
|
||||
System.arraycopy(MASTER_KEY, 0, combined, 0, MASTER_KEY.length);
|
||||
System.arraycopy(contextBytes, 0, combined, MASTER_KEY.length, contextBytes.length);
|
||||
byte[] expected = MessageDigest.getInstance("SHA-256").digest(combined);
|
||||
|
||||
assertArrayEquals(expected, dk.getEncKey(), "SHA-256 해싱 결과가 일치해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
|
||||
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리")
|
||||
void testDeriveKey_missingContextValue_emptyStringUsed() throws Exception {
|
||||
runtimeContext.remove("X-Api-Group-Seq");
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
byte[] expected = MessageDigest.getInstance("SHA-256").digest(MASTER_KEY);
|
||||
|
||||
assertArrayEquals(expected, dk.getEncKey(), "컨텍스트 값 없으면 masterKey만 해싱해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
|
||||
void testDeriveKey_differentContextValue_differentKey() throws Exception {
|
||||
DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
runtimeContext.put("X-Api-Group-Seq", "GROUP_002");
|
||||
DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
|
||||
"컨텍스트 값이 다르면 도출된 키도 달라야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
|
||||
void testDeriveKey_missingHsmKeyAlias_throwsException() {
|
||||
params.remove("hsmKeyAlias");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> strategy.deriveKey(params, runtimeContext),
|
||||
"hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
|
||||
void testDeriveKey_missingContextKey_throwsException() {
|
||||
params.remove("contextKey");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> strategy.deriveKey(params, runtimeContext),
|
||||
"contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. buildCacheKey
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
|
||||
void testBuildCacheKey_format() {
|
||||
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
|
||||
assertEquals("CRYPTO_A:GROUP_001", cacheKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
|
||||
void testBuildCacheKey_missingContextValue_emptyString() {
|
||||
runtimeContext.remove("X-Api-Group-Seq");
|
||||
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
|
||||
assertEquals("CRYPTO_A:", cacheKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
|
||||
void testBuildCacheKey_differentCryptoName_differentKey() {
|
||||
String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
|
||||
|
||||
assertNotEquals(key1, key2);
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HsmContextXorKeyDerivationStrategy 단위 테스트
|
||||
* HsmCryptoService는 Mockito Mock으로 대체한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class HsmContextXorKeyDerivationStrategyTest {
|
||||
|
||||
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
|
||||
private static final byte[] MASTER_KEY = "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static HsmCryptoService mockHsmCryptoService;
|
||||
private static HsmContextXorKeyDerivationStrategy strategy;
|
||||
|
||||
private Map<String, String> params;
|
||||
private Map<String, String> runtimeContext;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockHsmCryptoService = mock(HsmCryptoService.class);
|
||||
SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
strategy = new HsmContextXorKeyDerivationStrategy();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpParams() {
|
||||
params = new HashMap<>();
|
||||
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
|
||||
params.put("contextKey", "X-Api-Enc-Key");
|
||||
params.put("offset", "0");
|
||||
params.put("length", "16");
|
||||
|
||||
runtimeContext = new HashMap<>();
|
||||
runtimeContext.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. deriveKey
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환")
|
||||
void testDeriveKey_validParams_returnsDerivedKey() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertNotNull(dk);
|
||||
assertNotNull(dk.getEncKey());
|
||||
assertNotNull(dk.getDecKey());
|
||||
assertEquals(16, dk.getEncKey().length, "도출된 키 길이는 마스터키 길이(16)여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. XOR 계산 결과 검증")
|
||||
void testDeriveKey_xorResultIsCorrect() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
byte[] contextBytes = "clientKeyValue01".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
byte[] expected = new byte[MASTER_KEY.length];
|
||||
for (int i = 0; i < MASTER_KEY.length; i++) {
|
||||
expected[i] = (i < contextBytes.length)
|
||||
? (byte) (MASTER_KEY[i] ^ contextBytes[i])
|
||||
: MASTER_KEY[i];
|
||||
}
|
||||
|
||||
assertArrayEquals(expected, dk.getEncKey(), "XOR 계산 결과가 일치해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
|
||||
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리 (XOR 불변)")
|
||||
void testDeriveKey_missingContextValue_noXorChange() throws Exception {
|
||||
runtimeContext.remove("X-Api-Enc-Key");
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertArrayEquals(MASTER_KEY, dk.getEncKey(), "컨텍스트 값이 없으면 마스터키 그대로 반환되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. offset 지정 — 해당 위치부터 XOR 적용")
|
||||
void testDeriveKey_withOffset() throws Exception {
|
||||
params.put("offset", "4");
|
||||
params.put("length", "8");
|
||||
runtimeContext.put("X-Api-Enc-Key", "01234567890abcde");
|
||||
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertNotNull(dk);
|
||||
assertEquals(MASTER_KEY.length, dk.getEncKey().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
|
||||
void testDeriveKey_missingHsmKeyAlias_throwsException() {
|
||||
params.remove("hsmKeyAlias");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> strategy.deriveKey(params, runtimeContext),
|
||||
"hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
|
||||
void testDeriveKey_missingContextKey_throwsException() {
|
||||
params.remove("contextKey");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> strategy.deriveKey(params, runtimeContext),
|
||||
"contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-8. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
|
||||
void testDeriveKey_differentContextValue_differentKey() throws Exception {
|
||||
DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
runtimeContext.put("X-Api-Enc-Key", "differentValue00");
|
||||
DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
|
||||
"컨텍스트 값이 다르면 도출된 키도 달라야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. buildCacheKey
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
|
||||
void testBuildCacheKey_format() {
|
||||
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
|
||||
assertEquals("CRYPTO_A:clientKeyValue01", cacheKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
|
||||
void testBuildCacheKey_missingContextValue_emptyString() {
|
||||
runtimeContext.remove("X-Api-Enc-Key");
|
||||
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
|
||||
assertEquals("CRYPTO_A:", cacheKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
|
||||
void testBuildCacheKey_differentCryptoName_differentKey() {
|
||||
String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
|
||||
|
||||
assertNotEquals(key1, key2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
package com.eactive.eai.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class MaskingUtilsTest {
|
||||
|
||||
// =========================================================================
|
||||
// 1. 고유식별정보
|
||||
// =========================================================================
|
||||
|
||||
// --- 1-1. 주민등록번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1-1. 주민등록번호 — 하이픈 포함 정상 형식")
|
||||
void testResidentNumber_withHyphen() {
|
||||
String input = "123456-1234567";
|
||||
String result = MaskingUtils.maskResidentNumber(input);
|
||||
assertEquals("123456-1******", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1-2. 주민등록번호 — 하이픈 없는 형식 (뒤 6자리 마스킹)")
|
||||
void testResidentNumber_withoutHyphen() {
|
||||
String input = "0701011234567";
|
||||
String result = MaskingUtils.maskResidentNumber(input);
|
||||
assertEquals("0701011******", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1-3. 주민등록번호 — null/빈 문자열 방어")
|
||||
void testResidentNumber_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskResidentNumber(null));
|
||||
assertEquals("", MaskingUtils.maskResidentNumber(""));
|
||||
}
|
||||
|
||||
// --- 1-2. 운전면허번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2-1. 운전면허번호 — 숫자 7자리 역방향 마스킹")
|
||||
void testDriverLicense_normal() {
|
||||
String input = "제주-22-300001-50";
|
||||
String result = MaskingUtils.maskDriverLicense(input);
|
||||
assertEquals("제주-22-3*****-**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2-2. 운전면허번호 — null/빈 문자열 방어")
|
||||
void testDriverLicense_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskDriverLicense(null));
|
||||
assertEquals("", MaskingUtils.maskDriverLicense(""));
|
||||
}
|
||||
|
||||
// --- 1-3. 여권번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3-1. 여권번호 — 구여권 형식")
|
||||
void testPassport_old() {
|
||||
String input = "M12345678";
|
||||
String result = MaskingUtils.maskPassport(input);
|
||||
assertEquals("M1234****", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3-2. 여권번호 — 신여권 형식 (영문+숫자 혼합)")
|
||||
void testPassport_new() {
|
||||
String input = "M123A5678";
|
||||
String result = MaskingUtils.maskPassport(input);
|
||||
assertEquals("M123A****", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3-3. 여권번호 — null/빈 문자열 방어")
|
||||
void testPassport_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskPassport(null));
|
||||
assertEquals("", MaskingUtils.maskPassport(""));
|
||||
}
|
||||
|
||||
// --- 1-4. 외국인등록번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4-1. 외국인등록번호 — 주민등록번호와 동일 방식")
|
||||
void testForeignerNumber_normal() {
|
||||
String input = "123456-5234567";
|
||||
String result = MaskingUtils.maskForeignerNumber(input);
|
||||
assertEquals("123456-5******", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. 금융식별정보
|
||||
// =========================================================================
|
||||
|
||||
// --- 2-1. 계좌번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1-1. 계좌번호 — 명세 예시 형식 (XX-XX-XXXXXX)")
|
||||
void testAccountNumber_specExample() {
|
||||
String input = "99-94-123451";
|
||||
String result = MaskingUtils.maskAccountNumber(input);
|
||||
assertEquals("99-9*-*****1", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1-2. 계좌번호 — 일반 형식 (XX-XXXXXX-X): 하이픈 원위치 유지")
|
||||
void testAccountNumber_commonFormat() {
|
||||
String input = "99-912345-1";
|
||||
String result = MaskingUtils.maskAccountNumber(input);
|
||||
assertEquals("99-9*****-1", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1-3. 계좌번호 — 하이픈 없는 형식")
|
||||
void testAccountNumber_noHyphen() {
|
||||
String input = "123456789";
|
||||
String result = MaskingUtils.maskAccountNumber(input);
|
||||
assertEquals("123*****9", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1-4. 계좌번호 — 4자리 이하는 마스킹 안 함")
|
||||
void testAccountNumber_tooShort() {
|
||||
assertEquals("1234", MaskingUtils.maskAccountNumber("1234"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1-5. 계좌번호 — null/빈 문자열 방어")
|
||||
void testAccountNumber_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskAccountNumber(null));
|
||||
assertEquals("", MaskingUtils.maskAccountNumber(""));
|
||||
}
|
||||
|
||||
// --- 2-2. 카드번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2-1. 카드번호 — PCI-DSS 7~12번째 자리 마스킹")
|
||||
void testCardNumber_normal() {
|
||||
String input = "4518-4212-3456-1234";
|
||||
String result = MaskingUtils.maskCardNumber(input);
|
||||
assertEquals("4518-42**-****-1234", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2-2. 카드번호 — 하이픈 없는 16자리 형식")
|
||||
void testCardNumber_noHyphen() {
|
||||
String input = "4518421234561234";
|
||||
String result = MaskingUtils.maskCardNumber(input);
|
||||
assertEquals("451842******1234", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2-3. 카드번호 — null/빈 문자열 방어")
|
||||
void testCardNumber_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskCardNumber(null));
|
||||
assertEquals("", MaskingUtils.maskCardNumber(""));
|
||||
}
|
||||
|
||||
// --- 2-3. 카드 유효기간 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3-1. 카드 유효기간 — 월/년 전체 마스킹")
|
||||
void testCardExpiry_normal() {
|
||||
String input = "12/25";
|
||||
String result = MaskingUtils.maskCardExpiry(input);
|
||||
assertEquals("**/**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3-2. 카드 유효기간 — 구분자 없는 형식")
|
||||
void testCardExpiry_noDelimiter() {
|
||||
String input = "1225";
|
||||
String result = MaskingUtils.maskCardExpiry(input);
|
||||
assertEquals("****", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
// --- 2-4. CVV ---
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4-1. CVV — 전체 마스킹")
|
||||
void testCvv_normal() {
|
||||
String input = "123";
|
||||
String result = MaskingUtils.maskCvv(input);
|
||||
assertEquals("***", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-4-2. CVV — null/빈 문자열 방어")
|
||||
void testCvv_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskCvv(null));
|
||||
assertEquals("", MaskingUtils.maskCvv(""));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. 인증정보
|
||||
// =========================================================================
|
||||
|
||||
// --- 3-1. 비밀번호 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1-1. 비밀번호 — 전체 자리 마스킹")
|
||||
void testPassword_normal() {
|
||||
String input = "password";
|
||||
String result = MaskingUtils.maskPassword(input);
|
||||
assertEquals("********", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1-2. 비밀번호 — 숫자/특수문자 혼합")
|
||||
void testPassword_mixed() {
|
||||
String input = "Pass1234!@";
|
||||
String result = MaskingUtils.maskPassword(input);
|
||||
assertEquals("**********", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1-3. 비밀번호 — null/빈 문자열 방어")
|
||||
void testPassword_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskPassword(null));
|
||||
assertEquals("", MaskingUtils.maskPassword(""));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. 신상정보
|
||||
// =========================================================================
|
||||
|
||||
// --- 4-1. 전화번호/휴대폰 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-1. 전화번호 — 가운데·뒷자리 각 2자리 마스킹 (지역)")
|
||||
void testPhoneNumber_local() {
|
||||
String input = "064-1234-5678";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("064-12**-56**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-2. 전화번호 — 서울 02 형식")
|
||||
void testPhoneNumber_seoul() {
|
||||
String input = "02-1234-5678";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("02-12**-56**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-3. 전화번호 — 010 형식 (하이픈 있음)")
|
||||
void testMobileNumber_normal() {
|
||||
String input = "010-1234-5678";
|
||||
String result = MaskingUtils.maskMobileNumber(input);
|
||||
assertEquals("010-12**-56**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-4. 전화번호 — 하이픈 없는 11자리 휴대폰")
|
||||
void testPhoneNumber_noHyphen_mobile() {
|
||||
String input = "01012345678";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("01012**56**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-5. 전화번호 — 하이픈 없는 10자리 서울(02)")
|
||||
void testPhoneNumber_noHyphen_seoul10() {
|
||||
String input = "0212345678";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("0212**56**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-6. 전화번호 — 하이픈 없는 9자리 서울(02)")
|
||||
void testPhoneNumber_noHyphen_seoul9() {
|
||||
String input = "021234567";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("021**45**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-7. 전화번호 — 하이픈 없는 10자리 지역(0XX-XXX-XXXX)")
|
||||
void testPhoneNumber_noHyphen_local10() {
|
||||
String input = "0641234567";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("0641**45**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-8. 전화번호 — 하이픈 없는 11자리 지역(0XX-XXXX-XXXX)")
|
||||
void testPhoneNumber_noHyphen_local11() {
|
||||
String input = "06412345678";
|
||||
String result = MaskingUtils.maskPhoneNumber(input);
|
||||
assertEquals("06412**56**", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-9. 전화번호 — 인식 불가 형식은 원본 반환")
|
||||
void testPhoneNumber_invalidFormat() {
|
||||
assertEquals("0641234", MaskingUtils.maskPhoneNumber("0641234"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1-10. 전화번호 — null/빈 문자열 방어")
|
||||
void testPhoneNumber_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskPhoneNumber(null));
|
||||
assertEquals("", MaskingUtils.maskPhoneNumber(""));
|
||||
}
|
||||
|
||||
// --- 4-2. 이메일 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2-1. 이메일 — 앞 2자리·@ 앞 2자리 마스킹")
|
||||
void testEmail_normal() {
|
||||
String input = "aajjbacc@shinhan.com";
|
||||
String result = MaskingUtils.maskEmail(input);
|
||||
assertEquals("**jjba**@shinhan.com", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2-2. 이메일 — 로컬 파트 4자 이하는 전체 마스킹")
|
||||
void testEmail_shortLocal() {
|
||||
String input = "abc@test.com";
|
||||
String result = MaskingUtils.maskEmail(input);
|
||||
assertEquals("***@test.com", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2-3. 이메일 — @ 없는 경우 원본 반환")
|
||||
void testEmail_noAtSign() {
|
||||
assertEquals("notanemail", MaskingUtils.maskEmail("notanemail"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2-4. 이메일 — null/빈 문자열 방어")
|
||||
void testEmail_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskEmail(null));
|
||||
assertEquals("", MaskingUtils.maskEmail(""));
|
||||
}
|
||||
|
||||
// --- 4-3. 한글 성명 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3-1. 한글 성명 — 2자: 뒤 1자리 마스킹")
|
||||
void testKoreanName_2chars() {
|
||||
String input = "홍동";
|
||||
String result = MaskingUtils.maskKoreanName(input);
|
||||
assertEquals("홍*", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3-2. 한글 성명 — 3자: 가운데 1자리 마스킹")
|
||||
void testKoreanName_3chars() {
|
||||
String input = "홍길동";
|
||||
String result = MaskingUtils.maskKoreanName(input);
|
||||
assertEquals("홍*동", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3-3. 한글 성명 — 4자: 앞뒤 1자 제외 가운데 전체 마스킹")
|
||||
void testKoreanName_4chars() {
|
||||
String input = "홍길순동";
|
||||
String result = MaskingUtils.maskKoreanName(input);
|
||||
assertEquals("홍**동", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3-4. 한글 성명 — 한자 2자 성명")
|
||||
void testKoreanName_hanja() {
|
||||
String input = "田中";
|
||||
String result = MaskingUtils.maskKoreanName(input);
|
||||
assertEquals("田*", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3-5. 한글 성명 — 1자는 마스킹 없이 반환")
|
||||
void testKoreanName_1char() {
|
||||
String input = "홍";
|
||||
String result = MaskingUtils.maskKoreanName(input);
|
||||
assertEquals("홍", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3-6. 한글 성명 — null/빈 문자열 방어")
|
||||
void testKoreanName_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskKoreanName(null));
|
||||
assertEquals("", MaskingUtils.maskKoreanName(""));
|
||||
}
|
||||
|
||||
// --- 4-4. 영문 성명 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4-1. 영문 성명 — 이름(first name) 마스킹·성(last name) 노출")
|
||||
void testEnglishName_normal() {
|
||||
String input = "GILDONG HONG";
|
||||
String result = MaskingUtils.maskEnglishName(input);
|
||||
assertEquals("******* HONG", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4-2. 영문 성명 — 이름이 2개인 경우")
|
||||
void testEnglishName_multipleFirstNames() {
|
||||
String input = "MARY GRACE HONG";
|
||||
String result = MaskingUtils.maskEnglishName(input);
|
||||
assertEquals("**** ***** HONG", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4-3. 영문 성명 — 단일 토큰은 마스킹 없이 반환")
|
||||
void testEnglishName_singleToken() {
|
||||
assertEquals("HONG", MaskingUtils.maskEnglishName("HONG"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4-4. 영문 성명 — null/빈 문자열 방어")
|
||||
void testEnglishName_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskEnglishName(null));
|
||||
assertEquals("", MaskingUtils.maskEnglishName(""));
|
||||
}
|
||||
|
||||
// --- 4-5. 지번주소 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("4-5-1. 지번주소 — 번지 마스킹 및 상세주소 전체 마스킹")
|
||||
void testJibunAddress_normal() {
|
||||
String input = "제주특별자치도 제주시 연동 123번지, 101동 202호";
|
||||
String result = MaskingUtils.maskJibunAddress(input);
|
||||
assertEquals("제주특별자치도 제주시 연동 ***번지, **** ****", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-5-2. 지번주소 — 상세주소(쉼표 이후) 없는 경우")
|
||||
void testJibunAddress_noDetail() {
|
||||
String input = "제주특별자치도 제주시 연동 123번지";
|
||||
String result = MaskingUtils.maskJibunAddress(input);
|
||||
assertEquals("제주특별자치도 제주시 연동 ***번지", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
// --- 4-6. 도로명주소 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("4-6-1. 도로명주소 — 도로명 번호 및 번지·상세주소 마스킹 (자릿수 보존)")
|
||||
void testRoadAddress_normal() {
|
||||
String input = "제주특별자치도 제주시 은남2길 5, 101동 202호";
|
||||
String result = MaskingUtils.maskRoadAddress(input);
|
||||
assertEquals("제주특별자치도 제주시 은남*길 *, **** ****", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-6-2. 도로명주소 — 상세주소(쉼표 이후) 없는 경우")
|
||||
void testRoadAddress_noDetail() {
|
||||
String input = "제주특별자치도 제주시 은남2길 5";
|
||||
String result = MaskingUtils.maskRoadAddress(input);
|
||||
assertEquals("제주특별자치도 제주시 은남*길 *", result);
|
||||
assertEquals(input.length(), result.length());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. 온라인 정보 (IP는 고정 길이 마스킹 — String.length() 보존 단언 없음)
|
||||
// =========================================================================
|
||||
|
||||
// --- 5-1. IPv4 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1-1. IPv4 — 3번째 옥텟 고정 마스킹 (***)")
|
||||
void testIpv4_normal() {
|
||||
assertEquals("100.200.***.45", MaskingUtils.maskIpv4("100.200.123.45"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1-2. IPv4 — 3rd octet 2자리: *** 고정으로 길이 증가 (의도된 동작)")
|
||||
void testIpv4_twoDigitOctet() {
|
||||
String input = "10.0.10.5";
|
||||
String result = MaskingUtils.maskIpv4(input);
|
||||
assertEquals("10.0.***.5", result);
|
||||
// 2자리(10) → 3자리(***): length 9 → 10
|
||||
assertEquals(input.length() + 1, result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1-3. IPv4 — 3rd octet 1자리: *** 고정으로 길이 증가 (의도된 동작)")
|
||||
void testIpv4_oneDigitOctet() {
|
||||
String input = "10.0.1.5";
|
||||
String result = MaskingUtils.maskIpv4(input);
|
||||
assertEquals("10.0.***.5", result);
|
||||
// 1자리(1) → 3자리(***): length 8 → 10
|
||||
assertEquals(input.length() + 2, result.length());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1-4. IPv4 — 형식이 아닌 경우 원본 반환")
|
||||
void testIpv4_invalidFormat() {
|
||||
assertEquals("100.200.123", MaskingUtils.maskIpv4("100.200.123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1-5. IPv4 — null/빈 문자열 방어")
|
||||
void testIpv4_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskIpv4(null));
|
||||
assertEquals("", MaskingUtils.maskIpv4(""));
|
||||
}
|
||||
|
||||
// --- 5-2. IPv6 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2-1. IPv6 — 마지막 그룹(113~128 비트) 고정 마스킹 (****)")
|
||||
void testIpv6_normal() {
|
||||
assertEquals(
|
||||
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:****",
|
||||
MaskingUtils.maskIpv6("21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2-2. IPv6 — null/빈 문자열 방어")
|
||||
void testIpv6_nullAndEmpty() {
|
||||
assertNull(MaskingUtils.maskIpv6(null));
|
||||
assertEquals("", MaskingUtils.maskIpv6(""));
|
||||
}
|
||||
|
||||
// --- 5-3. IP 자동 판별 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3-1. IP 자동 판별 — IPv4")
|
||||
void testIpAddress_detectIpv4() {
|
||||
assertEquals("192.168.***.1", MaskingUtils.maskIpAddress("192.168.100.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3-2. IP 자동 판별 — IPv6")
|
||||
void testIpAddress_detectIpv6() {
|
||||
assertEquals(
|
||||
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:****",
|
||||
MaskingUtils.maskIpAddress("21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3-3. IP 자동 판별 — 쉼표 구분 다중 IPv4 (3자리/1자리 혼합)")
|
||||
void testIpAddress_multipleIpv4() {
|
||||
// 192.168.100.1: 3rd octet 3자리 → 길이 유지
|
||||
// 10.0.0.2: 3rd octet 1자리(0) → *** 고정으로 길이 증가
|
||||
assertEquals("192.168.***.1,10.0.***.2", MaskingUtils.maskIpAddress("192.168.100.1,10.0.0.2"));
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package com.eactive.eai.inbound.processor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.dual.DualBucket;
|
||||
import com.eactive.eai.common.inflow.dual.DualCustomBucket;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* DualRequestProcessor 단위 테스트.
|
||||
*
|
||||
* <p>RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance()를 호출한다.
|
||||
* @BeforeAll에서 mock ApplicationContext를 주입한 뒤 DualRequestProcessor를 최초 로딩시킨다.
|
||||
*
|
||||
* <p>주의: DualRequestProcessor를 필드 타입으로 선언하면 테스트 클래스 로딩 시 함께 로딩되어
|
||||
* @BeforeAll 실행 전에 NPE가 발생한다. 반드시 메서드 본문 내에서만 참조해야 한다.
|
||||
*/
|
||||
class DualRequestProcessorTest {
|
||||
|
||||
@BeforeAll
|
||||
static void setupMockApplicationContext() {
|
||||
ApplicationContext mockContext = mock(ApplicationContext.class);
|
||||
EAIServerManager mockServerManager = mock(EAIServerManager.class);
|
||||
when(mockContext.getBean(EAIServerManager.class)).thenReturn(mockServerManager);
|
||||
when(mockServerManager.getLocalServerName()).thenReturn("TEST-SERVER");
|
||||
when(mockServerManager.getGroupInstId()).thenReturn("TEST-INST");
|
||||
|
||||
ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext);
|
||||
}
|
||||
|
||||
private InflowTargetVO makeVO(String name, long perSec, long threshold, String unit) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(name);
|
||||
vo.setThresholdPerSecond(perSec);
|
||||
vo.setThreshold(threshold);
|
||||
vo.setThresholdTimeUnit(unit);
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// checkAdapterInflow
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_dualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(null);
|
||||
|
||||
assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
verify(mockBucket).isAdapterPassDetail("ADAPTER_A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_dualBucket_blockedPerSecond() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
|
||||
processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_dualBucket_blockedThreshold() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD,
|
||||
processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_nonDualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(true);
|
||||
|
||||
assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_nonDualBucket_blocked_returnsBlocked() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(false);
|
||||
|
||||
assertEquals("BLOCKED", processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// checkInterfaceInflow
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_dualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(null);
|
||||
|
||||
assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
verify(mockBucket).isInterfacePassDetail("IF_001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_dualBucket_blockedPerSecond() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
|
||||
processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_nonDualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isInterfacePass("IF_001")).thenReturn(true);
|
||||
|
||||
assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_nonDualBucket_blocked_returnsBlocked() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isInterfacePass("IF_001")).thenReturn(false);
|
||||
|
||||
assertEquals("BLOCKED", processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// buildInflowTargetErrorMsg
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_perSecond_includesReqPerSec() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API: 30req/sec]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_threshold_includesReqPerUnit() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API: 1000req/MIN]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_blockedFallback_simpleFormat() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, "BLOCKED");
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_nullBlockedType_simpleFormat() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, null);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_hourUnit() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("조회API", 10, 500, "HOUR");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [조회API: 500req/HOUR]", msg);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
INSERT INTO inflow_control_group (group_id, group_name, threshold, threshold_per_second, threshold_time_unit, use_yn) VALUES
|
||||
('test-group-001', '테스트 그룹', 100, 10, 'MIN', '1');
|
||||
('test-group-001', '테스트 그룹', 100, 10, 'MIN', '1'),
|
||||
('test-group-quota', '쿼터전용 그룹', 5, 0, 'MIN', '1');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES
|
||||
('01','_AA0_IN_NET_AsS',10,'1',0,NULL),
|
||||
('01','_AB0_IN_NET_AsS',3,'1',0,NULL),
|
||||
('01','_TST_IN_NET_AsS',1,'1',0,NULL),
|
||||
('02','FASSFEP00000004S2',10,'1',0,NULL),
|
||||
('02','FDASFEP00000009S2',2,'1',0,NULL);
|
||||
INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES
|
||||
('01','_AA0_IN_NET_AsS',10,'1',0,NULL),
|
||||
('01','_AB0_IN_NET_AsS',3,'1',0,NULL),
|
||||
('01','_TST_IN_NET_AsS',1,'1',0,NULL),
|
||||
('02','FASSFEP00000004S2',10,'1',0,NULL),
|
||||
('02','FDASFEP00000009S2',2,'1',0,NULL),
|
||||
('01','TEST_ADAPTER',5,'1',0,'MIN'),
|
||||
('02','TEST_INTERFACE',5,'1',0,'MIN');
|
||||
Reference in New Issue
Block a user