Compare commits

..

3 Commits

Author SHA1 Message Date
curry772 abf0de7757 Merge branch 'feature/msg-block-parse-skip'
- JsonReader: 수신 전문에 존재하는 블록을 ref 조건으로 버리지 않도록 수정
- 표준 오류응답 메시지 형식을 PropManager 에서 조회하도록 변경

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-03 08:58:24 +09:00
curry772 a7fde3f81e 표준 오류응답 메시지 형식을 PropManager 에서 조회하도록 변경
DefaultProcess.setResMsg() 두 곳(표준/준표준 오류응답)에 하드코딩돼 있던
String.format("[%s] %s (%s)", ...) 를 프로퍼티 기반으로 바꾼다.

- 그룹/키: DefaultProcess / std.error.msg.format
- 인자 순서: 1=오류코드, 2=오류메시지, 3=오류상세
- 기본형식: "[%s] %s"

프로퍼티는 에이전트의 ReloadPropertyCommand 로 무중단 갱신될 수 있으므로
캐시하지 않고 호출할 때마다 조회한다.

방어 두 가지를 함께 넣는다.
 - 그룹/키가 없거나 값이 공백이면 기본형식을 쓴다. 설정을 넣지 않아도 동작한다.
 - 형식이 잘못되면(%s 개수 불일치 등) String.format 이 IllegalFormatException 을
   던지는데, 하필 오류응답 경로라 여기서 예외가 나면 오류 자체를 못 내려보낸다.
   조립 실패 시 경고만 남기고 기본형식으로 되돌린다.

특정 사이트 전용이 아닌 공통 기능이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-03 08:57:17 +09:00
curry772 bfec1edea2 JsonReader: 수신 전문에 존재하는 블록을 ref 조건으로 버리지 않도록 수정
상대가 오류응답을 procs_rslt_dvcd=F 로 보내면서 요청응답구분 dman_rspn_dvcd 는
요청값 S 를 그대로 에코백했다(표준은 응답 시 R). 레이아웃의 MSG 블록 조건이 !S 라
MSG 이하가 통째로 버려졌고, 그 결과

 - MAIN_MSG 가 레이아웃 기본값으로 남아 오류가 "정상처리되었습니다." 로 보고되고
 - 거래로그는 StandardMessage 재직렬화본(EAILogDAO)이라 MSG 부 이후가 통째로 누락됐다

refPath/refValue 는 "이 블록이 전문에 들어있는가" 를 판단하는 조건인데, 고정길이와
달리 JSON 은 키의 존재 자체가 답이다. OBJECT 분기에 도달했다는 것은 수신 전문에
해당 블록이 실제로 들어있다는 뜻이므로 조건으로 다시 거르지 않고, 불일치는 상대
헤더의 오류로 보고 경고만 남긴 뒤 파싱을 계속한다.

기존에 한쪽 분기의 스킵 경고가 isDebugEnabled 가드에 가려 운영 로그에 남지 않던
문제도 함께 해소된다. FlatReader 는 조건이 블록 존재 판단에 반드시 필요하므로
그대로 둔다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-02 17:24:59 +09:00
8 changed files with 154 additions and 963 deletions
@@ -1,32 +0,0 @@
package com.eactive.eai.common.hsm;
import lombok.Data;
/**
* HsmCryptoService 키 캐시의 진단 정보. 키 원본(바이트/인코딩)은 절대 포함하지 않고
* alias, 종류, 알고리즘, 캐시 시각/만료 여부만 노출한다.
*/
@Data
public class HsmCachedKeyInfo {
/** SECRET(대칭키) / PUBLIC(공개키) */
String keyType;
/** HSM KeyStore alias */
String alias;
/** AES, RSA 등 */
String algorithm;
/** 캐시에 등록된 시각 (epoch millis) */
long cachedAt;
/** 캐시 등록 후 경과 시간 (millis) */
long ageMillis;
/** CACHE_TTL_SEC 기준 만료 여부. 만료되어도 HSM 장애 시 fallback 으로 사용된다. */
boolean expired;
/** 키 길이(바이트). non-extractable 등으로 알 수 없으면 -1. 키 값 자체는 노출하지 않는다. */
int keyLength;
}
@@ -7,10 +7,6 @@ import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PostConstruct;
@@ -203,52 +199,6 @@ public class HsmCryptoService implements PropertyChangeListener {
logger.warn("HsmCryptoService] 키 캐시 초기화 완료");
}
/**
* 현재 캐싱된 키 목록의 진단 정보를 반환합니다. 키 원본은 포함하지 않습니다.
* HSM 상태 조회 API(/manage/hsm/status)에서 사용합니다.
*/
public List<HsmCachedKeyInfo> getCachedKeyInfos() {
List<HsmCachedKeyInfo> infos = new ArrayList<>();
for (Map.Entry<String, CachedKey<SecretKey>> entry : secretKeyCache.entrySet()) {
SecretKey key = entry.getValue().key;
byte[] encoded = (key == null) ? null : key.getEncoded();
infos.add(toInfo("SECRET", entry.getKey(), entry.getValue(),
(key == null) ? null : key.getAlgorithm(),
(encoded == null) ? -1 : encoded.length));
}
for (Map.Entry<String, CachedKey<PublicKey>> entry : publicKeyCache.entrySet()) {
PublicKey key = entry.getValue().key;
byte[] encoded = (key == null) ? null : key.getEncoded();
infos.add(toInfo("PUBLIC", entry.getKey(), entry.getValue(),
(key == null) ? null : key.getAlgorithm(),
(encoded == null) ? -1 : encoded.length));
}
infos.sort(Comparator.comparing(HsmCachedKeyInfo::getKeyType)
.thenComparing(HsmCachedKeyInfo::getAlias));
return infos;
}
private HsmCachedKeyInfo toInfo(String keyType, String alias, CachedKey<?> cached,
String algorithm, int keyLength) {
HsmCachedKeyInfo info = new HsmCachedKeyInfo();
info.setKeyType(keyType);
info.setAlias(alias);
info.setAlgorithm(algorithm);
info.setCachedAt(cached.cachedAt);
info.setAgeMillis(System.currentTimeMillis() - cached.cachedAt);
info.setExpired(cached.isExpired());
info.setKeyLength(keyLength);
return info;
}
/** 현재 적용된 키 캐시 TTL(밀리초). HSM.CACHE_TTL_SEC 프로퍼티로 변경된다. */
public long getCacheTtlMs() {
return CACHE_TTL_MS;
}
// -------------------------------------------------------------------------
// RSA
// -------------------------------------------------------------------------
@@ -1,7 +1,5 @@
package com.eactive.eai.common.hsm;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
@@ -14,10 +12,6 @@ import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -25,7 +19,6 @@ import java.util.concurrent.TimeUnit;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.eactive.eai.agent.encryption.EncryptionManager;
@@ -34,7 +27,6 @@ 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.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;
@@ -43,62 +35,40 @@ import com.eactive.eai.common.util.Logger;
* SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11)
*
* PropManager 그룹 "HSM" 에서 읽는 키:
* PKCS11_CONFIG - primary pkcs11.cfg 파일 내용
* PIN - primary 슬롯 PIN
* PKCS11_CONFIG_SECONDARY - secondary pkcs11.cfg 파일 내용 (미설정 시 이중화 비활성)
* PIN_SECONDARY - secondary 슬롯 PIN (미설정 시 PIN 재사용)
* RELOAD_INTERVAL_MINUTES - KeyStore 주기적 재로드 간격(분)
* PKCS11_CONFIG - pkcs11.cfg 파일 내용
* PIN - HSM 슬롯 PIN
*
* PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력):
* name = ProtectServer
* library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
* slot = 0
*
*
* name = SoftHSM
* library = C:/SoftHSM2/lib/softhsm2-x64.dll
* slotListIndex = 0
*
* ---------------------------------------------------------------------
* [설정 이중화 / primary 자동 복귀]
* 기동(init) 과 재연결 시 항상 primary -> secondary 순서로 연결을 시도하고,
* 최초로 성공한 설정을 사용한다. secondary 로 절체된 뒤에는 주기적 재로드마다
* primary 재연결을 먼저 시도하므로 primary 가 복구되면 자동으로 되돌아온다.
*
* [세션 누적 방지 / 회로차단 로직]
* 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로 호출하면
* PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의 최대 세션 수를
* 초과하면서 reload 가 영구적으로 실패한다. 이를 방지하기 위해:
* 1) primary 로 동작 중이고 설정도 그대로면 "기존 keyStore 인스턴스"에 다시
* load() 하여 세션을 재사용한다 (= primary 우선 시도와 동일한 의미)
* [세션 누적 방지 / 회로차단 로직 추가]
* 기존 구현은 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로
* 호출하여 PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의
* 최대 세션 수를 초과하면서 reload 가 영구적으로 실패하는 문제가 있었다.
* 이를 방지하기 위해:
* 1) 정상 상황에서는 "기존 keyStore 인스턴스"에 다시 load() 하여 세션 재사용
* 2) 실제 키 조회(probe)로 세션이 살아있는지 검증
* 3) 재로드가 실패하거나 프로퍼티가 변경되면 primary -> secondary 순서로
* Provider 를 완전히 재생성
* 3) 연속 실패가 임계치를 넘으면 Provider 자체를 logout 후 완전히 재생성
* 4) 재생성마저 실패하면 마지막으로 성공한 keyStore 를 유지 (서비스 연속성 우선)
*
* [프로퍼티 즉시 반영]
* PropManager 의 PropertyChangeListener 로 등록되어 있어, 관리 포털에서 HSM 그룹을
* reload 하면 다음 스케줄을 기다리지 않고 즉시 설정을 다시 읽어 재연결을 시도한다.
* 이 경우에도 신규 연결이 완전히 성공한 뒤에만 keyStore 멤버변수를 교체한다.
* ---------------------------------------------------------------------
*/
@Component
public class HsmManager implements Lifecycle, PropertyChangeListener {
public class HsmManager implements Lifecycle {
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
private static final String GROUP_NAME = "HSM";
private static final String PROP_CONFIG = "PKCS11_CONFIG";
private static final String PROP_PIN = "PIN";
private static final String PROP_CONFIG_SECONDARY = "PKCS11_CONFIG_SECONDARY";
private static final String PROP_PIN_SECONDARY = "PIN_SECONDARY";
private static final String PROP_RELOAD_INTERVAL_MINUTES = "RELOAD_INTERVAL_MINUTES";
/** 설정 구분자. HSM 상태 조회 API 응답에도 그대로 노출된다. */
public static final String CONFIG_PRIMARY = "PRIMARY";
public static final String CONFIG_SECONDARY = "SECONDARY";
private static final long DEFAULT_RELOAD_INTERVAL_MINUTES = 10;
private Provider pkcs11Provider;
private volatile KeyStore keyStore;
private boolean started;
@@ -107,23 +77,10 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
private volatile char[] pin;
/** 현재 연결에 사용 중인 설정. 미연결이면 null. */
private volatile HsmConfig activeConfig;
// ---- 모니터링용 상태값 (HsmStatusController 에서 조회) ----
private volatile long activeSince;
private volatile long lastReloadAt;
private volatile String lastReloadResult;
private volatile String lastErrorMessage;
private volatile long lastErrorAt;
private volatile int reloadSuccessCount;
private volatile int reloadFailCount;
private volatile int failoverCount;
private volatile long reloadIntervalMinutes = DEFAULT_RELOAD_INTERVAL_MINUTES;
// 재로드 주기 (분 단위). 필요시 PropManager로 외부화 가능.
private static long RELOAD_INTERVAL_MINUTES = 1;
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> reloadFuture;
private volatile boolean propListenerRegistered;
private HsmManager() {
}
@@ -132,112 +89,6 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
return ApplicationContextProvider.getContext().getBean(HsmManager.class);
}
// -------------------------------------------------------------------------
// 설정 후보 (primary / secondary)
// -------------------------------------------------------------------------
/**
* 한 번의 연결 시도에 필요한 설정 한 벌. PropManager 에서 읽어 복호화까지 마친 값이다.
*/
static final class HsmConfig {
final String name;
final String configContent;
final char[] pin;
HsmConfig(String name, String configContent, char[] pin) {
this.name = name;
this.configContent = configContent;
this.pin = pin;
}
/** PropManager 값이 바뀌었는지 판단하기 위한 비교. */
boolean sameAs(HsmConfig other) {
if (other == null) {
return false;
}
return name.equals(other.name)
&& configContent.equals(other.configContent)
&& Arrays.equals(pin, other.pin);
}
}
/**
* 연결 시도 결과. Security 등록 전 상태이며, 완전히 성공한 경우에만
* applyConnection() 을 통해 멤버변수로 승격된다.
*/
private static final class HsmConnection {
final HsmConfig config;
final Provider provider;
final KeyStore keyStore;
HsmConnection(HsmConfig config, Provider provider, KeyStore keyStore) {
this.config = config;
this.provider = provider;
this.keyStore = keyStore;
}
}
/**
* PropManager 에서 매번 새로 읽어 primary -> secondary 순서의 연결 후보를 만든다.
* 스케줄러/프로퍼티 변경 이벤트 모두 이 메서드를 통하므로 변경된 값이 즉시 반영된다.
* PKCS11_CONFIG 가 비어 있는 후보는 제외한다.
*/
private List<HsmConfig> loadConfigCandidates() {
PropManager propManager = PropManager.getInstance();
EncryptionManager encManager = EncryptionManager.getInstance();
List<HsmConfig> candidates = new ArrayList<>();
String primaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG));
String primaryPin = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_PIN));
if (StringUtils.isNotBlank(primaryConfig)) {
candidates.add(new HsmConfig(CONFIG_PRIMARY, normalizeConfig(primaryConfig), toPin(primaryPin)));
}
String secondaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG_SECONDARY));
if (StringUtils.isNotBlank(secondaryConfig)) {
// PIN_SECONDARY 미설정이면 primary PIN 을 재사용한다 (슬롯만 이중화하는 구성 지원)
String rawSecondaryPin = propManager.getProperty(GROUP_NAME, PROP_PIN_SECONDARY);
String secondaryPin = StringUtils.isNotBlank(rawSecondaryPin) ? decrypt(encManager, rawSecondaryPin) : primaryPin;
candidates.add(new HsmConfig(CONFIG_SECONDARY, normalizeConfig(secondaryConfig), toPin(secondaryPin)));
}
return candidates;
}
private static String decrypt(EncryptionManager encManager, String value) {
if (value == null) {
return null;
}
return encManager.decryptDBData(value);
}
private static String normalizeConfig(String configContent) {
return configContent.trim().replace("\\n", "\n");
}
private static char[] toPin(String pinStr) {
return (pinStr != null) ? pinStr.toCharArray() : null;
}
private static HsmConfig findByName(List<HsmConfig> candidates, String name) {
if (name == null) {
return null;
}
for (HsmConfig candidate : candidates) {
if (name.equals(candidate.name)) {
return candidate;
}
}
return null;
}
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
@Override
public void start() throws LifecycleException {
if (started) {
@@ -248,7 +99,6 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
try {
init();
startReloadScheduler();
registerPropertyChangeListener();
} catch (Exception e) {
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
}
@@ -259,133 +109,38 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
private void init() throws Exception {
applyReloadInterval();
String reloadIntervalStr = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES, "10");
RELOAD_INTERVAL_MINUTES = Long.parseLong(reloadIntervalStr);
EncryptionManager encManager = EncryptionManager.getInstance();
List<HsmConfig> candidates = loadConfigCandidates();
if (candidates.isEmpty()) {
String configContent = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
String pinStr = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));
if (configContent == null || configContent.trim().isEmpty()) {
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
return;
}
HsmConnection connection = connectFirstAvailable(candidates);
applyConnection(connection);
pkcs11Provider = createProvider(configContent.trim().replace("\\n", "\n"));
logger.warn("HsmManager] 초기화 완료. config=" + connection.config.name
+ ", Provider=" + connection.provider.getName());
// 이미 등록된 Provider 가 있으면 제거 후 재등록
Provider existing = Security.getProvider(pkcs11Provider.getName());
if (existing != null) {
Security.removeProvider(existing.getName());
}
Security.addProvider(pkcs11Provider);
keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
this.pin = (pinStr != null) ? pinStr.toCharArray() : null;
keyStore.load(null, pin);
logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
logKeyStore(this.keyStore);
}
/**
* RELOAD_INTERVAL_MINUTES 를 다시 읽어 적용한다.
*
* @return 값이 변경되어 스케줄 재등록이 필요하면 true
*/
private boolean applyReloadInterval() {
long newInterval = reloadIntervalMinutes;
try {
String value = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES,
String.valueOf(DEFAULT_RELOAD_INTERVAL_MINUTES));
if (StringUtils.isNotBlank(value)) {
newInterval = Long.parseLong(value.trim());
}
} catch (Exception e) {
logger.warn("HsmManager] " + PROP_RELOAD_INTERVAL_MINUTES + " 값이 올바르지 않아 기존 값("
+ reloadIntervalMinutes + "분)을 유지합니다: " + e.getMessage());
return false;
}
if (newInterval <= 0) {
newInterval = DEFAULT_RELOAD_INTERVAL_MINUTES;
}
if (newInterval == reloadIntervalMinutes) {
return false;
}
reloadIntervalMinutes = newInterval;
return true;
}
// -------------------------------------------------------------------------
// 연결
// -------------------------------------------------------------------------
/**
* 후보 설정을 primary -> secondary 순서로 시도하여 최초로 성공한 연결을 반환한다.
* 각 후보는 Provider 생성 + KeyStore.load + 키 목록 조회(probe)까지 모두 성공해야
* 정상으로 간주한다. 전부 실패하면 마지막 예외를 던진다.
*/
private HsmConnection connectFirstAvailable(List<HsmConfig> candidates) throws Exception {
Exception lastException = null;
for (HsmConfig config : candidates) {
try {
HsmConnection connection = connect(config);
logger.warn("HsmManager] HSM 연결 성공. config=" + config.name
+ ", Provider=" + connection.provider.getName());
return connection;
} catch (Exception e) {
lastException = e;
recordError(config.name + " 설정 연결 실패: " + e.getMessage());
logger.warn("HsmManager] HSM 연결 실패. config=" + config.name + " : " + e.getMessage(), e);
}
}
if (lastException != null) {
throw lastException;
}
throw new HsmException("HSM 연결 후보 설정이 없습니다.");
}
/**
* 단일 설정으로 Provider/KeyStore 를 새로 생성한다. Security 에는 아직 등록하지 않으므로
* 여기서 실패해도 현재 사용 중인 Provider/keyStore 는 영향을 받지 않는다.
*/
private HsmConnection connect(HsmConfig config) throws Exception {
Provider provider = createProvider(config.configContent);
KeyStore ks = KeyStore.getInstance("PKCS11", provider);
ks.load(null, config.pin);
logKeyStore(ks);
return new HsmConnection(config, provider, ks);
}
/**
* 신규 연결을 멤버변수로 승격한다. 기존 Provider 는 이 시점에서만 logout/제거된다.
*/
private void applyConnection(HsmConnection connection) {
Provider oldProvider = this.pkcs11Provider;
HsmConfig oldConfig = this.activeConfig;
if (oldProvider != null && oldProvider != connection.provider) {
if (oldProvider instanceof AuthProvider) {
try {
((AuthProvider) oldProvider).logout();
} catch (Exception logoutEx) {
logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage());
}
}
Security.removeProvider(oldProvider.getName());
}
// 동일 이름으로 이미 등록된 Provider 가 있으면 제거 후 재등록
Provider registered = Security.getProvider(connection.provider.getName());
if (registered != null && registered != connection.provider) {
Security.removeProvider(registered.getName());
}
Security.addProvider(connection.provider);
this.pkcs11Provider = connection.provider;
this.keyStore = connection.keyStore;
this.activeConfig = connection.config;
this.pin = connection.config.pin;
this.activeSince = System.currentTimeMillis();
if (oldConfig != null && !oldConfig.name.equals(connection.config.name)) {
failoverCount++;
logger.warn("HsmManager] HSM 설정 절체: " + oldConfig.name + " -> " + connection.config.name);
}
}
private void logKeyStore(KeyStore keyStore)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
java.util.Enumeration<String> aliases = keyStore.aliases();
private void logKeyStore(KeyStore keyStore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
java.util.Enumeration<String> aliases = keyStore.aliases();
StringBuilder aliasList = new StringBuilder();
while (aliases.hasMoreElements()) {
if (aliasList.length() > 0) aliasList.append(", ");
@@ -402,11 +157,7 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
}
}
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
}
// -------------------------------------------------------------------------
// 주기적 재로드
// -------------------------------------------------------------------------
}
/**
* 별도 스레드에서 주기적으로 KeyStore 를 다시 로드하여
@@ -421,29 +172,12 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
reloadFuture = scheduler.scheduleWithFixedDelay(
this::reloadKeyStoreSafely,
reloadIntervalMinutes,
reloadIntervalMinutes,
RELOAD_INTERVAL_MINUTES,
RELOAD_INTERVAL_MINUTES,
TimeUnit.MINUTES
);
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + reloadIntervalMinutes + "");
}
/** RELOAD_INTERVAL_MINUTES 변경 시 스케줄을 다시 등록한다. */
private synchronized void rescheduleReload() {
if (scheduler == null || scheduler.isShutdown()) {
return;
}
if (reloadFuture != null) {
reloadFuture.cancel(false);
}
reloadFuture = scheduler.scheduleWithFixedDelay(
this::reloadKeyStoreSafely,
reloadIntervalMinutes,
reloadIntervalMinutes,
TimeUnit.MINUTES
);
logger.warn("HsmManager] KeyStore 재로드 주기 변경 적용: " + reloadIntervalMinutes + "");
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + RELOAD_INTERVAL_MINUTES + "");
}
/**
@@ -462,181 +196,79 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
* KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
* 즉시 재시도용으로 직접 호출할 수도 있다.
*
* 처리 순서:
* 1) PropManager 에서 설정을 매번 새로 읽는다 (변경값 즉시 반영)
* 2) 설정이 변경되었거나 아직 연결이 없으면 primary -> secondary 순으로 전체 재연결
* 3) 현재 secondary 로 동작 중이면 primary 복귀를 먼저 시도
* 4) 그 외에는 기존 keyStore 인스턴스에 다시 load() (PKCS11 세션 재사용).
* 실패하면 primary -> secondary 순으로 전체 재연결
*
* 어느 경로든 신규 연결이 완전히 성공한 경우에만 keyStore 멤버변수를 교체한다.
* 세션 누적 방지를 위해 새 KeyStore 인스턴스를 만들지 않고,
* 기존 keyStore 객체에 다시 load() 하여 기존 PKCS11 세션을 재사용한다.
* 연속 실패가 임계치를 넘으면 Provider 자체를 재생성한다.
*/
public synchronized void reloadKeyStoreIfNeeded() throws Exception {
List<HsmConfig> candidates = loadConfigCandidates();
if (candidates.isEmpty()) {
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않아 재로드를 건너뜁니다.");
return;
if (pkcs11Provider == null) {
return; // HSM 비활성화 상태
}
HsmConfig active = this.activeConfig;
HsmConfig currentCandidate = findByName(candidates, active == null ? null : active.name);
// 2) 미연결 상태이거나 현재 사용 중인 설정값 자체가 변경된 경우
if (active == null || pkcs11Provider == null || currentCandidate == null
|| !currentCandidate.sameAs(active)) {
logger.warn("HsmManager] HSM 설정 변경 또는 미연결 상태 감지 → 전체 재연결을 수행합니다.");
reconnect(candidates);
return;
}
// 3) secondary 로 동작 중이면 primary 복귀를 우선 시도
HsmConfig preferred = candidates.get(0);
if (!preferred.name.equals(active.name)) {
try {
HsmConnection connection = connect(preferred);
applyConnection(connection);
recordReloadSuccess(preferred.name + " 설정으로 복귀 성공");
logger.warn("HsmManager] " + preferred.name + " 설정으로 복귀했습니다.");
return;
} catch (Exception e) {
logger.warn("HsmManager] " + preferred.name + " 복귀 시도 실패. 현재 " + active.name
+ " 설정을 유지합니다: " + e.getMessage());
}
}
// 4) 현재 연결 유지 재로드 (세션 재사용)
try {
if (keyStore != null) {
keyStore.load(null, pin);
logKeyStore(keyStore);
logger.warn("HsmManager] KeyStore 재로드 완료 (config=" + active.name + ", 기존 세션 재사용).");
logger.warn("HsmManager] KeyStore 재로드 완료 (기존 세션 재사용).");
} else {
KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
ks.load(null, pin);
logKeyStore(ks);
this.keyStore = ks;
logger.warn("HsmManager] KeyStore 신규 생성 완료 (config=" + active.name + ").");
logger.warn("HsmManager] KeyStore 신규 생성 완료.");
}
recordReloadSuccess(active.name + " 설정 재로드 성공");
} catch (Exception e) {
logKeyStore(keyStore);
} catch (Exception e) {
logger.warn("HsmManager] KeyStore 재로드 실패:" + e.getMessage(), e);
logger.warn("HsmManager] 후보 설정(PRIMARY→SECONDARY) 전체 재연결을 시도합니다.");
reconnect(candidates);
logger.warn("HsmManager] Provider 전체 재초기화를 시도합니다.");
fullReinitialize();
}
}
/**
* 세션 누적, 네트워크 단절, 설정 변경 등으로 일반 재로드가 더 이상 복구되지 않을 때
* primary -> secondary 순서로 Provider 를 완전히 새로 생성한다.
* 세션 누적, 네트워크 단절 등으로 일반 재로드가 더 이상 복구되지 않을 때
* 기존 세션을 정리하고 Provider 를 완전히 새로 생성한다.
*
* 신규 Provider/KeyStore 준비가 완전히 성공한 후에만 기존 Provider 를 제거하고 교체한다.
* 모든 후보가 실패하면 기존 Provider 와 keyStore 를 그대로 유지한다
* 재초기화 중 예외가 발생하면 기존 Provider 와 keyStore 를 그대로 유지한다
* (서비스 중단보다 마지막 정상 상태 보존을 우선).
*/
private void reconnect(List<HsmConfig> candidates) throws Exception {
private void fullReinitialize() throws Exception {
Provider oldProvider = this.pkcs11Provider;
try {
HsmConnection connection = connectFirstAvailable(candidates);
applyConnection(connection);
recordReloadSuccess(connection.config.name + " 설정 재연결 성공");
logger.warn("HsmManager] HSM 재연결 성공. config=" + connection.config.name);
// 1. 신규 Provider 인스턴스 생성 (Security 미등록 상태)
String configContent = EncryptionManager.getInstance()
.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
Provider newProvider = createProvider(configContent.trim().replace("\\n", "\n"));
// 2. 신규 Provider 로 KeyStore 로드 테스트
// KeyStore.getInstance(type, providerInstance) 는 Security 등록 없이도 동작하므로
// 여기서 실패해도 oldProvider/keyStore 는 변경되지 않은 상태를 유지함
KeyStore ks = KeyStore.getInstance("PKCS11", newProvider);
ks.load(null, pin);
// 3. 신규 연결 성공 → 기존 Provider 정리 후 교체
if (oldProvider instanceof AuthProvider) {
try {
((AuthProvider) oldProvider).logout();
} catch (Exception logoutEx) {
logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage());
}
}
Security.removeProvider(oldProvider.getName());
Security.addProvider(newProvider);
this.pkcs11Provider = newProvider;
this.keyStore = ks;
logger.warn("HsmManager] Provider 전체 재초기화 성공.");
} catch (Exception e) {
recordReloadFailure(e.getMessage());
logger.warn("HsmManager] HSM 재연결 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e);
logger.warn("HsmManager] Provider 전체 재초기화 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e);
throw e;
}
}
private void recordReloadSuccess(String detail) {
lastReloadAt = System.currentTimeMillis();
lastReloadResult = "SUCCESS - " + detail;
reloadSuccessCount++;
}
private void recordReloadFailure(String detail) {
lastReloadAt = System.currentTimeMillis();
lastReloadResult = "FAIL - " + detail;
reloadFailCount++;
recordError(detail);
}
private void recordError(String message) {
lastErrorMessage = message;
lastErrorAt = System.currentTimeMillis();
}
// -------------------------------------------------------------------------
// PropManager 변경 즉시 반영
// -------------------------------------------------------------------------
private void registerPropertyChangeListener() {
if (propListenerRegistered) {
return;
}
try {
PropManager.getInstance().addPropertyChangeListener(this);
propListenerRegistered = true;
logger.warn("HsmManager] PropManager PropertyChangeListener 등록 완료");
} catch (Exception e) {
logger.warn("HsmManager] PropManager PropertyChangeListener 등록 실패(주기적 재로드로 대체): "
+ e.getMessage());
}
}
private void unregisterPropertyChangeListener() {
if (!propListenerRegistered) {
return;
}
try {
PropManager.getInstance().removePropertyChangeListener(this);
} catch (Exception e) {
logger.warn("HsmManager] PropManager PropertyChangeListener 해제 실패(무시): " + e.getMessage());
}
propListenerRegistered = false;
}
/**
* 관리 포털에서 PropManager.reload("HSM") 또는 setProperty 를 호출하면 수신된다.
* 재로드 자체는 HSM 통신을 수반하므로 호출 스레드를 막지 않도록 스케줄러 스레드에 위임한다.
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!isHsmGroupEvent(evt)) {
return;
}
if (applyReloadInterval()) {
rescheduleReload();
}
ScheduledExecutorService currentScheduler = this.scheduler;
if (currentScheduler == null || currentScheduler.isShutdown()) {
return;
}
logger.warn("HsmManager] HSM 프로퍼티 변경 감지 → 설정 재적용을 요청합니다.");
currentScheduler.execute(this::reloadKeyStoreSafely);
}
/**
* PropManager 는 reload(group) 시 propertyName 에 그룹명을, setProperty 시 키명을 담고
* source 에는 항상 해당 그룹의 PropGroupVO 를 담는다. 두 경우를 모두 인식한다.
*/
private boolean isHsmGroupEvent(PropertyChangeEvent evt) {
if (evt == null) {
return false;
}
if (GROUP_NAME.equals(evt.getPropertyName())) {
return true;
}
Object source = evt.getSource();
return (source instanceof PropGroupVO) && GROUP_NAME.equals(((PropGroupVO) source).getName());
}
// -------------------------------------------------------------------------
// Provider 생성
// -------------------------------------------------------------------------
/**
* JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
*
@@ -672,8 +304,6 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
}
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
unregisterPropertyChangeListener();
if (reloadFuture != null) {
reloadFuture.cancel(false);
}
@@ -693,8 +323,6 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
}
pkcs11Provider = null;
keyStore = null;
activeConfig = null;
activeSince = 0;
started = false;
lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
@@ -747,98 +375,4 @@ public class HsmManager implements Lifecycle, PropertyChangeListener {
return false;
}
}
// -------------------------------------------------------------------------
// 모니터링용 조회 (HsmStatusController)
// -------------------------------------------------------------------------
/** 현재 연결에 사용 중인 설정 이름(PRIMARY/SECONDARY). 미연결이면 null. */
public String getActiveConfigName() {
HsmConfig config = this.activeConfig;
return (config == null) ? null : config.name;
}
/** 현재 secondary 설정으로 절체된 상태인지 여부. */
public boolean isUsingSecondaryConfig() {
return CONFIG_SECONDARY.equals(getActiveConfigName());
}
/** 현재 연결에 실제로 사용된 pkcs11.cfg 내용. PIN 은 포함되지 않는다. */
public String getActiveConfigContent() {
HsmConfig config = this.activeConfig;
return (config == null) ? null : config.configContent;
}
/**
* 현재 PropManager 값 기준으로 실제 적용될 pkcs11.cfg 내용을 후보별로 반환한다.
* PIN 은 포함하지 않는다. (설정이름 -> cfg 내용, primary 우선순)
*/
public java.util.Map<String, String> getResolvedConfigContents() {
java.util.Map<String, String> contents = new java.util.LinkedHashMap<>();
try {
for (HsmConfig config : loadConfigCandidates()) {
contents.put(config.name, config.configContent);
}
} catch (Exception e) {
logger.warn("HsmManager] 연결 후보 설정 조회 실패: " + e.getMessage());
}
return contents;
}
/** 현재 KeyStore 의 alias 목록. HSM 통신이 발생한다. */
public List<String> getKeyAliases() throws Exception {
KeyStore ks = this.keyStore;
if (ks == null) {
return Collections.emptyList();
}
List<String> aliases = new ArrayList<>();
java.util.Enumeration<String> e = ks.aliases();
while (e.hasMoreElements()) {
aliases.add(e.nextElement());
}
return aliases;
}
public String getProviderName() {
Provider provider = this.pkcs11Provider;
return (provider == null) ? null : provider.getName();
}
public long getReloadIntervalMinutes() {
return reloadIntervalMinutes;
}
/** 현재 설정으로 연결된 시각(epoch millis). 미연결이면 0. */
public long getActiveSince() {
return activeSince;
}
public long getLastReloadAt() {
return lastReloadAt;
}
public String getLastReloadResult() {
return lastReloadResult;
}
public String getLastErrorMessage() {
return lastErrorMessage;
}
public long getLastErrorAt() {
return lastErrorAt;
}
public int getReloadSuccessCount() {
return reloadSuccessCount;
}
public int getReloadFailCount() {
return reloadFailCount;
}
/** primary <-> secondary 절체가 발생한 횟수. */
public int getFailoverCount() {
return failoverCount;
}
}
}
@@ -1,82 +0,0 @@
package com.eactive.eai.manage.hsm;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* HSM 연동 현황 조회 API. (ToolsController 와 동일한 응답 형태)
*
* PIN 등 비밀값과 키 원본은 응답에 포함하지 않는다.
*
* GET /manage/hsm/status → 연결 설정(PRIMARY/SECONDARY), 적용 프로퍼티,
* KeyStore alias, 캐싱된 키 목록 등 전체 현황
* ?healthCheck=true → 실제 HSM 통신으로 세션 생존까지 확인
* GET /manage/hsm/properties → HSM 프로퍼티 그룹의 현재 값 (PIN 류 마스킹)
* GET /manage/hsm/keystore/aliases → 현재 KeyStore 의 alias 목록 (HSM 통신 발생)
* GET /manage/hsm/cache/keys → HsmCryptoService 에 캐싱된 키 목록
* POST /manage/hsm/reload → 즉시 재로드 (PRIMARY → SECONDARY 순 연결 시도)
* POST /manage/hsm/cache/clear → 키 캐시 초기화
*/
@RestController
@RequestMapping("/manage/hsm")
public class HsmStatusController {
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
@Autowired
private HsmStatusService hsmStatusService;
@GetMapping("/status")
public ResponseEntity<?> status(
@RequestParam(name = "healthCheck", required = false, defaultValue = "false") boolean healthCheck) {
return respond(() -> hsmStatusService.getStatus(healthCheck));
}
@GetMapping("/properties")
public ResponseEntity<?> properties() {
return respond(() -> hsmStatusService.getMaskedProperties());
}
@GetMapping("/keystore/aliases")
public ResponseEntity<?> keyStoreAliases() {
return respond(() -> hsmStatusService.getKeyAliases());
}
@GetMapping("/cache/keys")
public ResponseEntity<?> cachedKeys() {
return respond(() -> hsmStatusService.getCachedKeys());
}
@PostMapping("/reload")
public ResponseEntity<?> reload() {
return respond(() -> hsmStatusService.reloadNow());
}
@PostMapping("/cache/clear")
public ResponseEntity<?> clearCache() {
return respond(() -> hsmStatusService.clearKeyCache());
}
private ResponseEntity<?> respond(Callable<Object> action) {
Map<String, Object> result = new HashMap<>();
try {
result.put("success", true);
result.put("data", action.call());
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok().contentType(APPLICATION_JSON_UTF8).body(result);
}
}
@@ -1,78 +0,0 @@
package com.eactive.eai.manage.hsm;
import java.util.List;
import java.util.Map;
import com.eactive.eai.common.hsm.HsmCachedKeyInfo;
import lombok.Data;
/**
* HSM 연동 현황 진단 정보.
*
* PIN 등 비밀값과 키 원본은 포함하지 않는다. PKCS11_CONFIG 는 라이브러리 경로/슬롯 정보만
* 담고 있어 그대로 노출한다.
*/
@Data
public class HsmStatusDTO {
// ---- 기동/연결 상태 ----
/** HsmManager Lifecycle 기동 여부 */
boolean started;
/** Provider 와 KeyStore 가 모두 준비된 상태인지 (HSM 통신 없음) */
boolean ready;
/** 실제 HSM 키 조회까지 성공하는지 (HSM 통신 발생, 조회 요청 시에만 채움) */
Boolean healthy;
/** 현재 연결에 사용 중인 설정: PRIMARY / SECONDARY / null(미연결) */
String activeConfigName;
/** secondary 로 절체된 상태인지 */
boolean usingSecondaryConfig;
/** 현재 등록된 SunPKCS11 Provider 이름 */
String providerName;
/** 현재 설정으로 연결된 시각 */
String activeSince;
// ---- 설정 ----
/** PropManager 기준 연결 후보 설정과 실제 적용될 pkcs11.cfg 내용 (primary 우선순) */
Map<String, String> resolvedConfigs;
/** HSM 프로퍼티 그룹의 현재 값 (PIN 류는 마스킹) */
Map<String, String> properties;
/** KeyStore 주기적 재로드 간격(분) */
long reloadIntervalMinutes;
// ---- 재로드 이력 ----
String lastReloadAt;
String lastReloadResult;
String lastErrorAt;
String lastErrorMessage;
int reloadSuccessCount;
int reloadFailCount;
/** primary <-> secondary 절체 발생 횟수 */
int failoverCount;
// ---- 키 ----
/** 현재 KeyStore 의 alias 목록 (HSM 통신 발생) */
List<String> keyAliases;
/** alias 조회 실패 시 사유. 성공이면 null */
String keyAliasError;
/** HsmCryptoService 에 캐싱된 키 목록 (키 원본 미포함) */
List<HsmCachedKeyInfo> cachedKeys;
/** 키 캐시 TTL(밀리초) */
long cacheTtlMs;
}
@@ -1,147 +0,0 @@
package com.eactive.eai.manage.hsm;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.eactive.eai.common.hsm.HsmCachedKeyInfo;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.hsm.HsmManager;
import com.eactive.eai.common.property.PropManager;
/**
* HSM 연동 현황 조회 서비스.
*
* HsmManager 의 연결 상태(primary/secondary), 적용 중인 HSM 프로퍼티, HsmCryptoService 의
* 키 캐시 목록을 한 곳에 모아 진단용으로 제공한다.
*/
@Service
public class HsmStatusService {
private static final String PROP_GROUP = "HSM";
/** 이 문자열이 포함된 프로퍼티 키의 값은 마스킹한다. */
private static final String[] SECRET_KEY_TOKENS = { "PIN", "PASSWORD", "PASSWD", "SECRET" };
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Autowired
private HsmCryptoService hsmCryptoService;
/**
* @param includeHealthCheck true 이면 실제 HSM 통신으로 세션 생존까지 확인한다.
*/
public HsmStatusDTO getStatus(boolean includeHealthCheck) {
HsmManager hsmManager = HsmManager.getInstance();
HsmStatusDTO status = new HsmStatusDTO();
status.setStarted(hsmManager.isStarted());
status.setReady(hsmManager.isReady());
status.setActiveConfigName(hsmManager.getActiveConfigName());
status.setUsingSecondaryConfig(hsmManager.isUsingSecondaryConfig());
status.setProviderName(hsmManager.getProviderName());
status.setActiveSince(formatTime(hsmManager.getActiveSince()));
status.setResolvedConfigs(hsmManager.getResolvedConfigContents());
status.setProperties(getMaskedProperties());
status.setReloadIntervalMinutes(hsmManager.getReloadIntervalMinutes());
status.setLastReloadAt(formatTime(hsmManager.getLastReloadAt()));
status.setLastReloadResult(hsmManager.getLastReloadResult());
status.setLastErrorAt(formatTime(hsmManager.getLastErrorAt()));
status.setLastErrorMessage(hsmManager.getLastErrorMessage());
status.setReloadSuccessCount(hsmManager.getReloadSuccessCount());
status.setReloadFailCount(hsmManager.getReloadFailCount());
status.setFailoverCount(hsmManager.getFailoverCount());
try {
status.setKeyAliases(hsmManager.getKeyAliases());
} catch (Exception e) {
status.setKeyAliases(Collections.<String>emptyList());
status.setKeyAliasError(e.getMessage());
}
status.setCachedKeys(getCachedKeys());
status.setCacheTtlMs(hsmCryptoService.getCacheTtlMs());
if (includeHealthCheck) {
status.setHealthy(Boolean.valueOf(hsmManager.isHealthy()));
}
return status;
}
/** HsmCryptoService 에 캐싱된 키 목록 (키 원본 미포함). */
public List<HsmCachedKeyInfo> getCachedKeys() {
return hsmCryptoService.getCachedKeyInfos();
}
/** 현재 KeyStore 의 alias 목록. HSM 통신이 발생한다. */
public List<String> getKeyAliases() throws Exception {
return HsmManager.getInstance().getKeyAliases();
}
/**
* HSM 프로퍼티 그룹의 현재 값. PIN 등 비밀값은 마스킹한다.
* PropManager 에 저장된 원본(암호화 저장 시 암호문) 그대로이며, 실제 적용되는
* 복호화 config 내용은 HsmStatusDTO.resolvedConfigs 에서 확인한다.
*/
public Map<String, String> getMaskedProperties() {
Map<String, String> masked = new TreeMap<>();
try {
Properties properties = PropManager.getInstance().getProperties(PROP_GROUP);
for (String key : properties.stringPropertyNames()) {
masked.put(key, mask(key, properties.getProperty(key)));
}
} catch (Exception e) {
masked.put("_error", "HSM 프로퍼티 그룹 조회 실패: " + e.getMessage());
}
return masked;
}
/**
* 재로드를 즉시 수행한다. primary -> secondary 순으로 연결을 시도하며,
* 신규 연결이 완전히 성공한 경우에만 KeyStore 가 교체된다.
*/
public String reloadNow() throws Exception {
HsmManager hsmManager = HsmManager.getInstance();
hsmManager.reloadKeyStoreIfNeeded();
return "재로드 완료. activeConfig=" + hsmManager.getActiveConfigName()
+ ", result=" + hsmManager.getLastReloadResult();
}
/** HsmCryptoService 키 캐시를 비운다. */
public String clearKeyCache() {
hsmCryptoService.clearKeyCache();
return "키 캐시 초기화 완료";
}
// -------------------------------------------------------------------------
private String mask(String key, String value) {
if (value == null) {
return null;
}
String upperKey = key.toUpperCase();
for (String token : SECRET_KEY_TOKENS) {
if (upperKey.contains(token)) {
return "****(len=" + value.length() + ")";
}
}
return value;
}
private String formatTime(long epochMillis) {
if (epochMillis <= 0) {
return null;
}
return new SimpleDateFormat(DATE_FORMAT).format(new Date(epochMillis));
}
}
@@ -119,8 +119,21 @@ public class JsonReader implements StandardReader {
switch(currentNode.getNodeType()) {
case OBJECT:
if( currentItem != null && currentItem.getSize() == 0
&& StringUtils.isNotBlank(currentItem.getRefPath())
// refPath/refValue 는 "이 블록이 전문에 들어있는가" 를 판단하기 위한 조건이다.
// 고정길이(FLAT)는 블록의 존재를 알려주는 키가 없어 이 조건이 유일한 근거지만,
// JSON 은 키의 존재 자체가 답이다. 여기에 도달했다는 것은 수신 전문에 이 블록이
// 실제로 들어있다는 뜻이므로, 조건으로 다시 걸러내면 받은 데이터를 잃는다.
//
// 실제 사고(2026-09): 상대가 오류응답을 procs_rslt_dvcd=F 로 보내면서 요청응답구분
// dman_rspn_dvcd 는 요청값 S 를 그대로 에코백했다(표준은 응답 시 R). MSG 블록의
// 조건이 !S 라 MSG 이하가 통째로 버려졌고, 그 결과
// - MAIN_MSG 가 레이아웃 기본값으로 남아 오류가 "정상처리되었습니다." 로 보고되고
// - 거래로그는 StandardMessage 재직렬화본이라 MSG 부 이후가 통째로 누락됐다.
//
// 조건 불일치는 상대 헤더의 오류로 보고, 경고만 남기고 파싱은 계속한다.
// (FlatReader 는 조건이 존재 판단에 반드시 필요하므로 그대로 둔다.)
if( currentItem != null && currentItem.getSize() == 0
&& StringUtils.isNotBlank(currentItem.getRefPath())
&& StringUtils.isNotBlank(currentItem.getRefValue()) ) {
String refItemValue = itemMap.get(currentItem.getRefPath());
@@ -131,25 +144,20 @@ public class JsonReader implements StandardReader {
if (refItemValue != null) refItemValue = refItemValue.trim();
String refValue = currentItem.getRefValue();
if (refValue.startsWith("!")) { // 느낌표가 들어가면 다음에 나오는게 아닌경우
boolean negate = refValue.startsWith("!"); // 느낌표가 들어가면 다음에 나오는게 아닌경우
if (negate) {
refValue = refValue.substring(1);
if (refValue.equals(refItemValue)) {
logger.warn("@GROUP name={} SKIPPED.", currentItem.getName());
currentItem.setHidden(true);
break;
} else {
currentItem.setHidden(false);
}
} else {
if (!currentItem.getRefValue().equals(refItemValue)) {
if (logger.isDebugEnabled())
logger.warn("@GROUP name={} SKIPPED.", currentItem.getName());
currentItem.setHidden(true);
break;
} else {
currentItem.setHidden(false);
}
}
boolean matched = refValue.equals(refItemValue);
if (negate) {
matched = !matched;
}
if (!matched) {
logger.warn("@GROUP name={} 조건 불일치({}=[{}], 기대=[{}]). 수신 전문에 존재하므로 파싱은 계속한다.",
currentItem.getName(), currentItem.getRefPath(), refItemValue,
currentItem.getRefValue());
}
currentItem.setHidden(false);
}
Iterator<String> fieldNames = currentNode.fieldNames();
while(fieldNames.hasNext()) {
@@ -20,6 +20,7 @@ import com.eactive.eai.common.header.HeaderActionKeys;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.message.EAIMessageKeys;
import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.routing.Process;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.submessage.SubMessageManager;
@@ -43,6 +44,12 @@ public abstract class DefaultProcess extends Process {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
protected static Logger esbLogger = Logger.getLogger(Logger.LOGGER_ESBFW);
/** 표준 오류응답 메시지 형식을 조회할 프로퍼티 그룹/키. 없으면 기본형식을 쓴다. */
public static final String PROP_GROUP = "DefaultProcess";
public static final String PROP_STD_ERR_MSG_FORMAT = "std.error.msg.format";
/** 인자 순서: 1=오류코드, 2=오류메시지, 3=오류상세 */
public static final String DEFAULT_STD_ERR_MSG_FORMAT = "[%s] %s";
protected String guidLogPrefix = this.getClass().getSimpleName() + "] ";
protected static ThreadLocal local = new ThreadLocal(); // 경과시간 동기화를 위한 ThreadLocal
@@ -689,7 +696,7 @@ public abstract class DefaultProcess extends Process {
String errorCode = mapper.getErrorCode(resStandardMessage);
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc"));
this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
this.resEaiMsg.setRspErr("RECEAIINA001", formatStdErrorMessage(errorCode, errorMsg, errorDesc));
return;
} else {
this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신");
@@ -707,7 +714,7 @@ public abstract class DefaultProcess extends Process {
String errorCode = mapper.getErrorCode(resStandardMessage);
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc"));
this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
this.resEaiMsg.setRspErr("RECEAIINA001", formatStdErrorMessage(errorCode, errorMsg, errorDesc));
} else {
this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신");
}
@@ -720,6 +727,37 @@ public abstract class DefaultProcess extends Process {
}
}
/**
* 표준 오류응답 메시지를 조립한다.
*
* 형식은 PropManager 의 {@code DefaultProcess / std.error.msg.format} 에서 가져온다.
* 프로퍼티는 에이전트의 ReloadPropertyCommand 로 무중단 갱신될 수 있으므로,
* 캐시하지 않고 호출할 때마다 조회한다. 그룹이나 키가 없으면
* {@link #DEFAULT_STD_ERR_MSG_FORMAT} 를 쓴다.
*
* 형식 문자열은 운영에서 편집 가능하므로 잘못된 형식(예: %s 개수 불일치)이 들어올 수 있다.
* 이 메서드는 오류응답 경로에서 호출되므로 여기서 예외가 나면 오류 자체를 못 내려보낸다.
* 따라서 조립에 실패하면 경고만 남기고 기본형식으로 되돌린다.
*
* @param errorCode 오류코드 (형식 인자 1)
* @param errorMsg 오류메시지 (형식 인자 2)
* @param errorDesc 오류상세 (형식 인자 3)
*/
protected String formatStdErrorMessage(String errorCode, String errorMsg, String errorDesc) {
String format = PropManager.getInstance().getProperty(
PROP_GROUP, PROP_STD_ERR_MSG_FORMAT, DEFAULT_STD_ERR_MSG_FORMAT);
if (StringUtils.isBlank(format)) {
format = DEFAULT_STD_ERR_MSG_FORMAT;
}
try {
return String.format(format, errorCode, errorMsg, errorDesc);
} catch (Exception e) {
logger.warn(guidLogPrefix + " 표준 오류응답 형식이 잘못되어 기본형식으로 대체한다. ["
+ PROP_GROUP + "/" + PROP_STD_ERR_MSG_FORMAT + "=" + format + "] - " + e.getMessage());
return String.format(DEFAULT_STD_ERR_MSG_FORMAT, errorCode, errorMsg, errorDesc);
}
}
public boolean isTgtTranCall() {
return this.isTgtTran;
}