6 Commits

Author SHA1 Message Date
curry772 6eedac51f7 마스킹 유틸리티 추가 (MaskingUtils + 단위테스트)
- 보안아키텍처 마스킹 정책에 따라 고유식별정보·금융식별정보·인증정보·신상정보·온라인정보
- 카테고리별 마스킹 처리 유틸 및 JUnit 5 단위테스트(3단계 번호 체계) 신규 작성
2026-05-20 16:33:21 +09:00
curry772 8d63586749 거래파일로그 기능 추가
- HTTP Inbound 거래도 출력되도록 수정
- HTTP Header 파일로그 추가
2026-05-19 13:41:35 +09:00
curry772 7b0699fdb9 로그 오타 수정 2026-05-19 11:20:23 +09:00
curry772 b6a5dce74a 거래파일로그(SIFT_LOGGER) 출력기능 개발
- UUID별로 거래파일로그 생성
2026-05-19 11:18:59 +09:00
curry772 265a30ccf3 Outbound Client oAuth Token 기능 개선 및 패치
- 광주은행 내용 적용
- ignite 사용
2026-05-18 10:58:10 +09:00
curry772 d458d1b008 Merge branch 'feature/cryptofilter' 2026-05-14 15:24:04 +09:00
14 changed files with 1455 additions and 161 deletions
@@ -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;
@@ -118,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);
@@ -147,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(
@@ -75,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;
@@ -427,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();
@@ -472,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())) {
@@ -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;
@@ -16,7 +17,9 @@ import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
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);
}
}
@@ -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;
}
}
}
@@ -336,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) {
@@ -345,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)) {
@@ -367,7 +367,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
} else {
TypedCircuitBreakerManager.logger.error(
"CirbuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
"CircuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
new Object[]{event, this.vo});
}
break;
@@ -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 **&#47;**
* 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 **&#47;**</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));
}
}
}
@@ -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 = "";
@@ -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;
@@ -99,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)) {
@@ -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"));
}
}