Compare commits

...

48 Commits

Author SHA1 Message Date
curry772 909cda9ffb JSON 메시지 이원을 위해서, 리턴을 byte[] -> String으로 변경 2026-06-10 08:51:28 +09:00
curry772 a538b9838e TemplateAdapterErrorMsgHandler 구현 작업 내역
-인바운드/아웃바운드/내부 오류 발생 시 어댑터별로 프로퍼티에 등록된 템플릿을 사용하여
 커스텀 에러 응답 메시지를 생성하는 TemplateAdapterErrorMsgHandler 구현 및 연동 작업.
- ExceptionHandler 변경
인바운드 어댑터에 ERR_MSG_HANDLER 프로퍼티가 설정된 경우 우선 실행
- ApiController에서 사용할 수 있는
generateNonStandardInboundErrorResponseMessage 추가, Controller에서도
Adapter가 식별되면 설정된 핸들러에 의해 에러메시지 행성 가능
2026-06-09 13:47:25 +09:00
curry772 d15b2d563e AdapterName이 null이어서 NullPointerException 발생하는 문제 처리 2026-06-05 17:17:41 +09:00
curry772 037fcd6302 feat: TxSiftContext 추가 및 HttpAdapterServiceSupport 적용
- TxSiftContext 클래스 추가: SIFT MDC 등록/해제 및 appender close 공통 처리
     - begin(): isSiftEnabled() 확인 후 MDC 등록, 이미 설정된 경우 스킵
     - end(): appender 즉시 close 후 MDC 제거 (UUID 등 1회성 key 용도)
   - HttpAdapterServiceSupport: MDC 직접 제어 코드를 TxSiftContext.begin/end로
교체
2026-06-05 14:05:10 +09:00
curry772 d9ff1fa40b 잘못된 주석 제거 2026-06-05 13:00:15 +09:00
curry772 4ce22937af 미확인(unkown) 로그 파일 로그 추가
- EAIDBLogControl.isEnable() DB 정상 확인
- DB 비정상일 시 파일 로그 출력
2026-06-05 10:35:17 +09:00
curry772 e1e2353139 거래데이터 DB 암호화 DAMO 모듈 적용
- DB 거래로그 : EAILogDAO.java
- FILE 거래로그 : EAIFileLogger.java
- 미확인(Unkown) 로그 : InboundErrorInfoDAO.java
2026-06-05 09:47:31 +09:00
curry772 bc16dbcf13 Outbound HTTP 거래에 대해 Simulation거래도 Rest와 동일하게 변경 2026-06-04 16:14:24 +09:00
curry772 176c9845d7 api id, eai svc cd 추출 필터 추가 2026-06-02 18:04:51 +09:00
curry772 3ee53c469c 디버그 로그 추가 2026-06-02 18:03:44 +09:00
curry772 6776153582 KJB_ROOTLESS_ARRAY -> DJB_ROOTLESS_ARRAY 변경 2026-06-02 08:31:47 +09:00
curry772 ea09113dbe XML 메시지 지원 개발 2026-06-02 08:29:11 +09:00
curry772 113b9227c9 DJB에 사용하지 않는 resource 파일 정리 2026-06-01 16:54:24 +09:00
curry772 f3c2e64191 JWT 서명 알고리즘 RS256 → PS256 교체 — PssJwtAccessTokenConverter, RsaPssSigner,
RsaPssVerifier 추가

- RsaPssSigner: spring-security-jwt Signer 구현,
RSASSA-PSS(SHA-256/MGF1/salt=32) 서명
- RsaPssVerifier: SignatureVerifier 구현, 동일 PSS 파라미터로 검증
- PssJwtAccessTokenConverter: JwtHelper를 우회하여 PS256 JWT 직접 발급·검증
  - encode/decode 오버라이드로 JwtAlgorithms PS256 미등록 문제 해결
  - 생성자에서 공개키 PEM을 보관하여 getKey() 엔드포인트 대응
- build.gradle: spring-security-jwt:1.1.1.RELEASE api 의존성 추가
- RsaPssSignerVerifierTest: 단위테스트 13개 (서명/검증/라운드트립/시각확인)
2026-05-28 13:27:34 +09:00
curry772 6809bf66b8 Dynamic IV 도출 지원 — DerivedKey iv 필드 추가, HsmKeyAndIvDerivationStrategy 구현 2026-05-27 13:47:58 +09:00
curry772 29a486bf9b RequestProcessor 메소드 시그니쳐 변경 2026-05-27 10:32:30 +09:00
curry772 7415828e82 클라이언트 유량제어 관련 단위테스트 eapim-online 이동 2026-05-26 16:36:06 +09:00
curry772 a4dbf4ab27 Client 유량제어 관련 eapim-online으로 이동 2026-05-26 16:32:32 +09:00
curry772 4f18d25a58 CryptoModuleServier 단위테스트 수정 2026-05-26 16:32:12 +09:00
curry772 0e96e9470e 어댑터 암복호화 필터 리팩토링
- 커스텀 패키지 지정
- 변수 이동
2026-05-26 09:54:11 +09:00
curry772 a51453a57f MaskingUtils elink-online-core로 이동 2026-05-20 17:16:37 +09:00
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
curry772 29b8b7eaac perf: encryptField/decryptField JSON 파싱 횟수 절감
JsonPathUtil에 JsonNode 기반 저수준 API(toTree/getAt/setAt/fromTree) 추가.
기존 getValueAtPath/setValueAtPath/mergeAtRoot는 내부적으로 이를 위임.

AbstractCryptoFilter.encryptField, decryptField는 파싱된 JsonNode를
재사용하여 JSON 파싱을 기존 2~3회에서 1회로 단축.
- encryptField non-root: 파싱 2회 → 1회
- decryptField non-root: 파싱 2회 → 1회
- decryptField toPath="/": 파싱 3회 → 2회 (body 1회 + mergeJson 1회)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:55:29 +09:00
curry772 74302df5a2 fix: decryptField toPath='/' 시 기존 필드 보존 - mergeAtRoot 연동
toPath='/'일 때 복호화된 텍스트로 body 전체를 교체하던 동작을
JsonPathUtil.mergeAtRoot 를 통해 fromPath 필드만 제거하고
복호화된 JSON을 body에 병합하는 방식으로 수정.

- AbstractCryptoFilter.decryptField: return plainText → mergeAtRoot 호출
- JsonPathUtil.mergeAtRoot: 신규 메서드 추가
- InCryptoFilterTest: 4-4 병합 동작 검증 테스트 추가, 4-2 설명 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:34:55 +09:00
curry772 9a15b5506f fix: CryptoFilterHsmIntegrationTest - BaseCryptoFilter 잔여 참조 InCryptoFilter로 교체
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:02 +09:00
curry772 0b68b8166f feat: InCryptoFilter / OutCryptoFilter 추가 및 암호화 필터 계층 재구성
- AbstractCryptoFilter (신규, http.filter 패키지): 공통 암복호화 로직 및 상수 분리
- InCryptoFilter (신규, dynamic.filter): HttpAdapterFilter 구현 - 수신 요청 복호화, 응답 암호화
- OutCryptoFilter (신규, client.impl.filter): HttpClientAdapterFilter 구현 - 송신 요청 암호화, 응답 복호화
- BaseCryptoFilter, CryptoFilter 제거 → InCryptoFilter로 통합
- CryptoFilterHsmIntegrationTest: 새 클래스명으로 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:26:36 +09:00
curry772 8db66f8bef DefaultProcess - 표준전문 오류응답을 수신 응답 설정(setResMsg) 로직 변경
- AdapterErrorMessageHandler를 사용하는 경우, BWK_FAILMSG_CODE(RIBEAI999999)를
설정하도록 변경
- AdapterErrorMessageHandler 설정이 없은 경우, Exception 발생 부분 처리
2026-05-14 10:42:12 +09:00
curry772 c4a250db08 refactor: HttpAdapterFilterFactoryKjb 제거 및 HttpAdapterFilterFactory 통합
- HttpAdapterFilterFactoryKjb 삭제
- HttpAdapterFilterFactory에 HMAC_SHA256, REFLECTALLHEADER 케이스 추가
- classForName() 헬퍼로 basePackage / customBasePackage 순차 탐색 적용
- HttpAdapterServiceSupport 임포트를 HttpAdapterFilterFactory로 교체

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 16:09:22 +09:00
curry772 db7a728344 feat: HttpClient5 어댑터 필터 패키지 eapim-online에서 이동
- HttpClient5AdapterServiceRestAddFilter 이동
- HttpClientAdapterFilter / HttpClient5AdapterFilterFactory 등 filter 패키지 이동
  (AllHeaderFilter, AsyncReponseFilter, JBOBPFilter, KAKAOBankFilter,
   KFTCFaceFilter, KFTCP2PFilter, NAVERFinFilter, NICECreditFilter,
   SimpleFrameworkFilter, SimpleframeworkBodyFilter, TOSSBankFilter, TraceBodyFilter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:58:07 +09:00
curry772 813bb99952 test: CryptoModuleServiceTest DYNAMIC AES/CBC 출력 로그 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:53:17 +09:00
curry772 becd8612e8 Merge branch 'feature/crypto-module'
CryptoFilter 프로퍼티 키 규격화, HSM 통합 테스트, SoftHSM2 연동 검증 완료
2026-05-13 13:29:14 +09:00
curry772 e208a2d64b feat: CryptoFilter 프로퍼티 키 규격화 및 HSM 통합 테스트 추가
- CryptoFilter prop 키를 UPPER_UNDERSCORE 형식으로 변경 (CRYPTO_MODULE_NAME 등)
- CRYPTO_DEC_ENABLED / CRYPTO_ENC_ENABLED 옵션 제거 (필터 설정 시 항상 암복호화)
- getProp() 빈 문자열도 기본값으로 처리 (StringUtils.isBlank 적용)
- CryptoFilterHsmIntegrationTest 추가: SoftHSM2 연동 전체 흐름 검증 (7개 케이스)
  - attributes(*, CKO_SECRET_KEY, CKK_AES): 임포트 키도 CKA_SENSITIVE=false 적용
  - HsmKeyDerivationStrategy / HsmContextSha256KeyDerivationStrategy 검증
- HsmKeyRegisterUtil 추가: SoftHSM2 마스터키 등록 유틸리티
- CryptoModuleServiceTest: GCM AAD 관련 테스트 케이스 보강

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:29:02 +09:00
curry772 444b7ad595 feat: CryptoFilter GCM AAD 지원 추가 및 단위테스트 작성
- CryptoFilter: crypto.aad.header 프로퍼티 + buildAad() 메서드 추가
- CryptoFilter: 기본 scope SCOPE_BODY → SCOPE_FIELD 변경
- CryptoFilter: decryptBody/decryptField/encryptBody/encryptField aad 파라미터 적용
- BaseCryptoFilterTest: BODY/FIELD scope, buildAad, AAD 헤더 연동 단위테스트 신규 작성
- CryptoModuleServiceTest: STATIC/DYNAMIC GCM AAD 테스트 섹션 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:15:40 +09:00
curry772 085dd024d8 feat: HsmContextSha256KeyDerivationStrategyTest common 패키지로 이동
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:37:15 +09:00
curry772 3b6b3f6e4c Merge branch 'master' into feature/crypto-module 2026-05-12 16:33:59 +09:00
curry772 f4c28e8a27 feat: HSM 키 파생 전략 클래스 추가 (common 패키지로 이동)
- BaseHsmKeyDerivationStrategy: HSM 마스터키 조회 공통 로직 추상 기반 클래스
- HsmKeyDerivationStrategy: HSM 마스터키 직접 사용 전략
- HsmSha256KeyDerivationStrategy: HSM 마스터키 SHA-256 해싱 도출 전략
- HsmContextSha256KeyDerivationStrategy: HSM 마스터키 + 컨텍스트 SHA-256 해싱 도출 전략

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:30:26 +09:00
curry772 3cda2873cc feat: HTTP 어댑터 암복호화 필터 기반 클래스 추가 및 기본 scope 수정
- BaseCryptoFilter: 빈 RuntimeContext를 반환하는 기반 클래스 추가
- CryptoFilter: 기본 scope SCOPE_BODY → SCOPE_FIELD로 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:27:21 +09:00
curry772 c0efdd89d4 Merge branch 'feature/template-adapter-error-msg-handler' 2026-05-12 13:06:08 +09:00
curry772 3eac4eeaa6 feat: 모든 변수 표현식에 :기본값 문법 추가 및 JSON 필드 파싱 지원
- ${callprop.KEY:default} / ${callprop[path]:default} / ${MSG.path:default} 기본값 문법
- ${DATA.BIZDATA.acctNo} : StandardMessage String 값이 JSON이면 Gson으로 필드 추출
  (중첩 경로 지원, 없는 필드는 빈 문자열 또는 기본값)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:05:33 +09:00
curry772 c0c3f45d18 feat: 모든 변수 표현식에 :기본값 문법 추가
- ${callprop.KEY:default} - 직접 참조 기본값
- ${callprop[MSG.path]:default} - 간접 참조 기본값
- ${MSG.path:default} - 표준전문 경로 기본값
키/경로 미존재 또는 callProp null 시 기본값 반환, 기본값 미지정 시 빈 문자열.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 11:21:58 +09:00
curry772 2686448791 fix: GenerateMethod 테스트에서 AdapterPropManager → PropManager 목 객체로 변경
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:47:23 +09:00
curry772 3d4e5762c8 Merge branch 'feature/template-adapter-error-msg-handler' into master 2026-05-12 09:44:10 +09:00
curry772 f7859a77dc feat: 템플릿 기반 어댑터 에러 메시지 핸들러 추가
AdapterPropManager "AdapterErrorMessageHandler" 그룹의
{adapterGroupId}.template 키로 등록된 템플릿을 읽어
StandardMessage 및 callProp 값으로 치환한 에러 응답을 생성한다.

변수 치환 규칙:
- ${callprop.키}         → callProp.getProperty("키")
- ${MSG.MAIN_MSG.경로}  → StandardMessage.findItemValue("경로")
- {{#foreach 경로}}...{{/foreach}} → GRID 배열 반복 (쉼표 구분)

JSON/XML 템플릿 모두 지원

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:42:32 +09:00
97 changed files with 8120 additions and 1619 deletions
+2
View File
@@ -135,6 +135,8 @@ dependencies {
compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1' compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
api "org.springframework.security:spring-security-jwt:1.1.1.RELEASE"
testRuntimeOnly 'com.h2database:h2:2.1.214' testRuntimeOnly 'com.h2database:h2:2.1.214'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
Binary file not shown.
-29
View File
@@ -1,29 +0,0 @@
#-----------------------------------------------------------------------------
# comment start with #
# level; // 0 : root ~ n
# type; // 0: Message, 1: Field, 2:Group 3, Grid, 4 : Field Array, 9 : BIZ DATA
# size // 0 : variable, > 0 fixed
# fieldType; // 0: ELEMENT, 1: ATTRIBUTE,
# length;
# dataType; // 1: String, 2: Number,11: ZZ String, 12: LL Number
#-----------------------------------------------------------------------------
# name ,level ,type ,size ,fieldType ,length ,dataType ,,refPath, refValue,value
#-----------------------------------------------------------------------------
ElinkHeader ,0,0,1,0, 0,0,,,
Header ,1,2,1,1, 0,0,,,
StndCicsTrncd ,2,1,1,1,10,0,,,
StndIntnlStndTelgmLen,2,1,1,1,10,11,,,0
StndTranBaseYmd ,2,1,1,1,10,0,,,
Common ,1,2,0,0, 0,0,Header.StndCicsTrncd,JI6H,null
TranInfo ,2,2,1,0, 0,0,,,
StndCicsTrncd ,3,1,1,0, 3,0,,,
StndTelgmRecvTranCd ,3,1,1,0,10,0,,,
StndPrcssRtdTranCd ,3,1,1,0,10,0,,,
Array ,1,3,0,0, 3,0,,,
Item1 ,2,1,1,0,10,0,,,JI6H
Group ,2,3,1,0, 1,0,,,
gitem1 ,3,1,1,0,10,1,,,726
gitem2 ,3,1,1,0,10,0,,,20220905
FArray ,1,4,0,0,10,0,,,
bizData ,1,9,1,0, 0,0,,,
zzData ,1,1,1,0, 2,10,,,ZZ
1 #-----------------------------------------------------------------------------
2 # comment start with #
3 # level; // 0 : root ~ n
4 # type; // 0: Message, 1: Field, 2:Group 3, Grid, 4 : Field Array, 9 : BIZ DATA
5 # size // 0 : variable, > 0 fixed
6 # fieldType; // 0: ELEMENT, 1: ATTRIBUTE,
7 # length;
8 # dataType; // 1: String, 2: Number,11: ZZ String, 12: LL Number
9 #-----------------------------------------------------------------------------
10 # name ,level ,type ,size ,fieldType ,length ,dataType ,,refPath, refValue,value
11 #-----------------------------------------------------------------------------
12 ElinkHeader ,0,0,1,0, 0,0,,,
13 Header ,1,2,1,1, 0,0,,,
14 StndCicsTrncd ,2,1,1,1,10,0,,,
15 StndIntnlStndTelgmLen,2,1,1,1,10,11,,,0
16 StndTranBaseYmd ,2,1,1,1,10,0,,,
17 Common ,1,2,0,0, 0,0,Header.StndCicsTrncd,JI6H,null
18 TranInfo ,2,2,1,0, 0,0,,,
19 StndCicsTrncd ,3,1,1,0, 3,0,,,
20 StndTelgmRecvTranCd ,3,1,1,0,10,0,,,
21 StndPrcssRtdTranCd ,3,1,1,0,10,0,,,
22 Array ,1,3,0,0, 3,0,,,
23 Item1 ,2,1,1,0,10,0,,,JI6H
24 Group ,2,3,1,0, 1,0,,,
25 gitem1 ,3,1,1,0,10,1,,,726
26 gitem2 ,3,1,1,0,10,0,,,20220905
27 FArray ,1,4,0,0,10,0,,,
28 bizData ,1,9,1,0, 0,0,,,
29 zzData ,1,1,1,0, 2,10,,,ZZ
-41
View File
@@ -1,41 +0,0 @@
#-----------------------------------------------------------------------------
# comment start with #
# level; // 0 : root ~ n
# type; // 0: Message, 1: Field, 2:Group 3, Grid, 4 : Field Array, 9 : BIZ DATA
# size // 0 : variable, > 0 fixed
# fieldType; // 0: ELEMENT, 1: ATTRIBUTE,
# length;
# dataType; // 1: String, 2: Number,11: ZZ String, 12: LL Number
#-----------------------------------------------------------------------------
# name ,level ,type ,size ,fieldType ,length ,dataType ,,refPath, refValue,value
#-----------------------------------------------------------------------------
# name ,level ,type ,size ,fieldType ,length ,dataType ,refPath, refValue, value
STD_HNCPHEADER_REQ ,0 ,1 ,1 ,1 ,0 ,1 , , ,,표준헤더
msg_len ,1 ,2 ,1 ,1 ,6 ,12 , , ,199,전문길이
global_id ,1 ,2 ,1 ,1 ,29 ,1 , , ,GIDNO00000000001111,Global아이디
service_id ,1 ,2 ,1 ,1 ,10 ,1 , , ,ELINKSVC01,거래코드
service_cmt ,1 ,2 ,1 ,1 ,100 ,1 , , ,ELINK TEST Service,거래코드설명
service_tn ,1 ,2 ,1 ,1 ,1 ,1 , , ,1,거래구분
Session_id ,1 ,2 ,1 ,1 ,100 ,1 , , ,SESSIONID001,Session아이디
encrypt_yn ,1 ,2 ,1 ,1 ,2 ,1 , , ,NO,전문암호화여부
http_url ,1 ,2 ,1 ,1 ,100 ,1 , , ,,HTTPURL정보
screen_id ,1 ,2 ,1 ,1 ,13 ,1 , , ,SCREEN0000001,화면ID
scr_no ,1 ,2 ,1 ,1 ,30 ,1 , , ,000000000100000000020000000003,화면번호
scr_dsc ,1 ,2 ,1 ,1 ,100 ,1 , , ,,화면명
usr_id ,1 ,2 ,1 ,1 ,10 ,1 , , ,USER000001,사용자ID
ip_addr ,1 ,2 ,1 ,1 ,47 ,1 , , ,,IPAddress
ip_mac ,1 ,2 ,1 ,1 ,12 ,1 , , ,,ClientMAC Address
com_code ,1 ,2 ,1 ,1 ,4 ,1 , , ,EACT,그룹사코드
head_code ,1 ,2 ,1 ,1 ,7 ,1 , , ,EACT001,본부코드
dept_cd ,1 ,2 ,1 ,1 ,7 ,1 , , ,ELINK01,부서코드
conn_tp ,1 ,2 ,1 ,1 ,2 ,1 , , ,CH,요청채널구분
Inter_chn ,1 ,2 ,1 ,1 ,2 ,1 , , ,EA,시스템구분
recv_tm ,1 ,2 ,1 ,1 ,17 ,1 , , ,20220928000000000,전문수신시간
send_tm ,1 ,2 ,1 ,1 ,17 ,1 , , ,,전문송신시간
return_gubun ,1 ,2 ,1 ,1 ,1 ,1 , , ,S,전문응답구분
return_value ,1 ,2 ,1 ,1 ,1 ,1 , , ,,처리결과
msg_id ,1 ,2 ,1 ,1 ,7 ,1 , , ,,오류코드
msg_nm ,1 ,2 ,1 ,1 ,250 ,1 , , ,,오류메시지
filler ,1 ,2 ,1 ,1 ,125 ,1 , , ,,Filler
bizData ,1 ,9 ,0 ,1 ,0 ,1 , , ,,업무데이터
zzData ,1 ,2 ,0 ,1 ,2 ,11 , , ,,ZZ플래그
1 #-----------------------------------------------------------------------------
2 # comment start with #
3 # level; // 0 : root ~ n
4 # type; // 0: Message, 1: Field, 2:Group 3, Grid, 4 : Field Array, 9 : BIZ DATA
5 # size // 0 : variable, > 0 fixed
6 # fieldType; // 0: ELEMENT, 1: ATTRIBUTE,
7 # length;
8 # dataType; // 1: String, 2: Number,11: ZZ String, 12: LL Number
9 #-----------------------------------------------------------------------------
10 # name ,level ,type ,size ,fieldType ,length ,dataType ,,refPath, refValue,value
11 #-----------------------------------------------------------------------------
12 # name ,level ,type ,size ,fieldType ,length ,dataType ,refPath, refValue, value
13 STD_HNCPHEADER_REQ ,0 ,1 ,1 ,1 ,0 ,1 , , ,,표준헤더
14 msg_len ,1 ,2 ,1 ,1 ,6 ,12 , , ,199,전문길이
15 global_id ,1 ,2 ,1 ,1 ,29 ,1 , , ,GIDNO00000000001111,Global아이디
16 service_id ,1 ,2 ,1 ,1 ,10 ,1 , , ,ELINKSVC01,거래코드
17 service_cmt ,1 ,2 ,1 ,1 ,100 ,1 , , ,ELINK TEST Service,거래코드설명
18 service_tn ,1 ,2 ,1 ,1 ,1 ,1 , , ,1,거래구분
19 Session_id ,1 ,2 ,1 ,1 ,100 ,1 , , ,SESSIONID001,Session아이디
20 encrypt_yn ,1 ,2 ,1 ,1 ,2 ,1 , , ,NO,전문암호화여부
21 http_url ,1 ,2 ,1 ,1 ,100 ,1 , , ,,HTTPURL정보
22 screen_id ,1 ,2 ,1 ,1 ,13 ,1 , , ,SCREEN0000001,화면ID
23 scr_no ,1 ,2 ,1 ,1 ,30 ,1 , , ,000000000100000000020000000003,화면번호
24 scr_dsc ,1 ,2 ,1 ,1 ,100 ,1 , , ,,화면명
25 usr_id ,1 ,2 ,1 ,1 ,10 ,1 , , ,USER000001,사용자ID
26 ip_addr ,1 ,2 ,1 ,1 ,47 ,1 , , ,,IPAddress
27 ip_mac ,1 ,2 ,1 ,1 ,12 ,1 , , ,,ClientMAC Address
28 com_code ,1 ,2 ,1 ,1 ,4 ,1 , , ,EACT,그룹사코드
29 head_code ,1 ,2 ,1 ,1 ,7 ,1 , , ,EACT001,본부코드
30 dept_cd ,1 ,2 ,1 ,1 ,7 ,1 , , ,ELINK01,부서코드
31 conn_tp ,1 ,2 ,1 ,1 ,2 ,1 , , ,CH,요청채널구분
32 Inter_chn ,1 ,2 ,1 ,1 ,2 ,1 , , ,EA,시스템구분
33 recv_tm ,1 ,2 ,1 ,1 ,17 ,1 , , ,20220928000000000,전문수신시간
34 send_tm ,1 ,2 ,1 ,1 ,17 ,1 , , ,,전문송신시간
35 return_gubun ,1 ,2 ,1 ,1 ,1 ,1 , , ,S,전문응답구분
36 return_value ,1 ,2 ,1 ,1 ,1 ,1 , , ,,처리결과
37 msg_id ,1 ,2 ,1 ,1 ,7 ,1 , , ,,오류코드
38 msg_nm ,1 ,2 ,1 ,1 ,250 ,1 , , ,,오류메시지
39 filler ,1 ,2 ,1 ,1 ,125 ,1 , , ,,Filler
40 bizData ,1 ,9 ,0 ,1 ,0 ,1 , , ,,업무데이터
41 zzData ,1 ,2 ,0 ,1 ,2 ,11 , , ,,ZZ플래그
@@ -1,11 +0,0 @@
layout.file.type=CSV
layout.file.path=./resources/standard-layout-sample.csv
mapper.class=com.eactive.eai.message.test.TestInterfaceMapper
mapper.definition=./resources/standard-message-mapping-config.properties
reader.JSON=com.eactive.eai.message.parser.JsonReader
reader.JSN=com.eactive.eai.message.parser.JsonReader
reader.UJN=com.eactive.eai.message.parser.JsonReader
reader.XML=com.eactive.eai.message.parser.XmlReader
reader.UXL=com.eactive.eai.message.parser.XmlReader
reader.ASC=com.eactive.eai.message.parser.FlatReader
reader.FLAT=com.eactive.eai.message.parser.FlatReader
@@ -1,20 +0,0 @@
# layout : StandardMessage layout definition
layout.file.type=CSV
layout.file.path=./resources/standard-layout.csv
layout.filter.FLAT=com.eactive.eai.message.filter.FlatMessageFilter
# mapper : StandardMessage's fields getter/setter interface
mapper.class=com.eactive.eai.message.service.DefaultInterfaceMapper
mapper.definition=./resources/standard-message-mapping-config.properties
# reader : parsing input data to StandardMessage
reader.JSON=com.eactive.eai.message.parser.JsonReader
reader.JSN=com.eactive.eai.message.parser.JsonReader
reader.UJN=com.eactive.eai.message.parser.JsonReader
reader.XML=com.eactive.eai.message.parser.XmlReader
reader.UXL=com.eactive.eai.message.parser.XmlReader
reader.ASC=com.eactive.eai.message.parser.FlatReader
reader.FLAT=com.eactive.eai.message.parser.FlatReader
encode.flat=euc-kr
encode.value=euc-kr
encode.xml=utf-8
# version.layout.name=VERSION_SAMPLE
# version.mapper.definition=./resources/version-message-mapping-config.properties
@@ -1,28 +0,0 @@
REQ_SYS_CODE=systemHeader.DMND_SYS_CD
CHANNEL_TYPE_CD=stdcommon.chnlTycd
CHANNEL_DETAIL_CD=stdcommon.chnlDtlsClcd
SEND_TIME=stdheader.send_tm
GUID=stdheader.global_id
SYSTEM_TYPE=stdheader.Inter_chn
RECV_TIME=stdheader.recv_tm
RETURN_VALUE=stdheader.return_value
GUID_ORG=stdcommon.ortrGuid
GUID_SEQ=stdheader.global_id
#SERVICE_ID=stdheader.service_id
SERVICE_ID=service_id
RETURN_SERVICE_ID=stdcommon.procsRsltRcmsSrvcId
INTERFACE_ID=stdheader.service_id
SESSION_ID=stdheader.Session_id
#SEND_RECV_DIVISION=stdheader.return_gubun
SEND_RECV_DIVISION=service_tn
IN_EX_DIVISION=stdcommon.hmabDvcd
OPERATION_ENV=stdcommon.sysOprtEnvDvcd
FIRST_REQ_IP=stdheader.ip_addr
RECOVER_YN=stdcommon.ortrRestrYn
INST_CODE=stdheader.com_code
TIMEOUT=stdheader.gnrzTmotTktm
ERROR_CODE=stdheader.msg_id
ERROR_MSG=stdheader.msg_nm
USER_ID=stdheader.usr_id
RESPONSE_TYPE=stdcommon.procsRsltDvcd
SYNC_ASYNC_TYPE=stdcommon.txSynzDvcd
@@ -1,26 +0,0 @@
REQ_SYS_CODE=Inter_chn
CHANNEL_TYPE_CD=
CHANNEL_DETAIL_CD=conn_tp
SEND_TIME=send_tm
GUID=global_id
SYSTEM_TYPE=service_tn
RECV_TIME=recv_tm
RETURN_VALUE=return_value
GUID_ORG=stdcommon.ortrGuid
GUID_SEQ=stdheader.global_id
SERVICE_ID=service_id
RETURN_SERVICE_ID=stdcommon.procsRsltRcmsSrvcId
INTERFACE_ID=service_id
SESSION_ID=Session_id
SEND_RECV_DIVISION=service_tn
IN_EX_DIVISION=stdcommon.hmabDvcd
OPERATION_ENV=stdcommon.sysOprtEnvDvcd
FIRST_REQ_IP=ip_addr
RECOVER_YN=stdcommon.ortrRestrYn
INST_CODE=stdheader.com_code
TIMEOUT=stdheader.gnrzTmotTktm
ERROR_CODE=msg_id
ERROR_MSG=msg_nm
USER_ID=usr_id
RESPONSE_TYPE=stdcommon.procsRsltDvcd
SYNC_ASYNC_TYPE=stdcommon.txSynzDvcd
@@ -1,4 +0,0 @@
service_id=header.if_id
global_id=header.guid
service_tn=header.req_type
bizData=bizData
@@ -6,6 +6,7 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -163,6 +164,9 @@ public class AdapterManager implements Lifecycle {
} }
public AdapterVO getAdapterVO(String adapterGroupName, String adapterName) { public AdapterVO getAdapterVO(String adapterGroupName, String adapterName) {
if(StringUtils.isAnyBlank(adapterGroupName, adapterName))
return null;
AdapterGroupVO gvo = adapterGroups.get(adapterGroupName); AdapterGroupVO gvo = adapterGroups.get(adapterGroupName);
if (gvo == null) if (gvo == null)
return null; return null;
@@ -2,9 +2,16 @@ package com.eactive.eai.adapter.handler;
import java.util.Properties; import java.util.Properties;
import com.eactive.eai.message.StandardMessage; import com.eactive.eai.common.message.EAIMessage;
public interface AdapterErrorMessageHandler { public interface AdapterErrorMessageHandler {
public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, Properties callProp, Object outboundRequestData, StandardMessage resStandardMessage) throws Exception; public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
} Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception;
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName,
String inboudnAdapterName, Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg)
throws Exception;
public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName,
Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception;
}
@@ -2,13 +2,25 @@ package com.eactive.eai.adapter.handler;
import java.util.Properties; import java.util.Properties;
import com.eactive.eai.message.StandardMessage; import com.eactive.eai.common.message.EAIMessage;
public class DefaultAdapterErrorMessageHandler implements AdapterErrorMessageHandler { public class DefaultAdapterErrorMessageHandler implements AdapterErrorMessageHandler {
@Override @Override
public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
Properties callProp, Object outboundRequestData, StandardMessage resStandardMessage) throws Exception { Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception {
return null;
}
@Override
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception {
return null;
}
@Override
public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName,
Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception {
return null; return null;
} }
@@ -0,0 +1,514 @@
package com.eactive.eai.adapter.handler;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.message.StandardItem;
import com.eactive.eai.message.StandardMessage;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* 템플릿 기반 어댑터 에러 메시지 핸들러.
*
* [프로퍼티 설정]
* AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서
* "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어
* StandardMessage 의 필드 값으로 치환한 결과를 반환한다.
*
* [변수 치환 규칙]
* 1. "${callprop.키}" → callProp.getProperty("키") 로 치환
* "${callprop.키:기본값}" → 키 없거나 callProp null 이면 기본값 반환
* 2. "${callprop[경로]}" → StandardMessage 경로 값을 키로 callProp 간접 조회
* "${callprop[경로]:기본값}" → 키 결정 실패 또는 키 없으면 기본값 반환
* 3. "${경로}" → StandardMessage.findItemValue("경로") 로 치환
* "${경로:기본값}" → 경로 값이 null/공백이면 기본값 반환
* 모든 형태에서 기본값 미지정 시 빈 문자열을 반환한다.
*
* [배열 반복 문법] (MSG_LIST 등 GRID 타입)
* {{#foreach MSG.MSG_LIST}}
* { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" }
* {{/foreach}}
* → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환,
* 반복 결과를 쉼표(,)로 이어 붙임
*
* [사용 예 JSON 템플릿]
* {
* "adapterName": "${callprop.ADAPTER_NAME}",
* "resultCode": "${MSG.MAIN_MSG.outp_msg_cd}",
* "resultMsg": "${MSG.MAIN_MSG.outp_msg_ctnt}",
* "errors": [
* {{#foreach MSG.MSG_LIST}}
* {
* "code": "${outp_msg_cd}",
* "msg": "${outp_msg_ctnt}",
* "desc": "${outp_msg_desc}",
* "field": "${err_occu_item_nm}"
* }
* {{/foreach}}
* ]
* }
*
* [사용 예 – XML 템플릿]
* <error>
* <adapter>${callprop.ADAPTER_NAME}</adapter>
* <code>${MSG.MAIN_MSG.outp_msg_cd}</code>
* <errors>
* {{#foreach MSG.MSG_LIST}}
* <item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>
* {{/foreach}}
* </errors>
* </error>
*
* [callProp 중첩 Properties 참조]
* callProp 의 값이 Properties 인 경우 점(.) 으로 체인 탐색이 가능하다.
* callProp.put("OUTBOUND", subProps) // subProps.setProperty("IF_ID", "SVC001")
* → 템플릿에서 ${callprop.OUTBOUND.IF_ID} 로 참조
* 중간 값이 Properties 가 아닌 경우 전체 keyPath 를 키로 폴백 조회한다.
*/
public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler {
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
/** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
static final String PROP_GROUP = "AdapterErrorMessageHandler";
/** callProp 참조 변수 접두어: ${callprop.키} */
static final String CALLPROP_PREFIX = "callprop.";
/** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */
static final String CALLPROP_INDIRECT_PREFIX = "callprop[";
/** 예외 객체 필드 참조 접두어: ${exception.필드명} */
static final String EXCEPTION_PREFIX = "exception.";
/** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 (renderRow 내부에서 사용) */
private static final Pattern VAR_PATTERN =
Pattern.compile("\\$\\{([^}]+)\\}");
/**
* foreach 블록과 스칼라 변수를 순서대로 단일 패스로 처리하는 통합 패턴.
* group(1) != null → foreach 블록: group(1)=경로, group(2)=블록 본문
* group(3) != null → 스칼라 변수: group(3)=표현식
*/
private static final Pattern COMBINED_PATTERN =
Pattern.compile(
"\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}|\\$\\{([^}]+)\\}",
Pattern.DOTALL);
@Override
public Object generateNonStandardErrorResponseMessage(
String inboundAdapterGroupName,
String inboundAdapterName,
Properties callProp,
Object outboundRequestData,
EAIMessage resEaiMsg) throws Exception {
StandardMessage resStandardMessage = resEaiMsg.getStandardMessage();
String templateKey = inboundAdapterGroupName + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template)) {
if (logger.isWarn()) {
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
+ ", key=" + templateKey);
}
return null;
}
return render(template, resStandardMessage, callProp);
}
/** 3-인자 위임: exception 없이 호출하는 기존 경로 유지 */
String render(String template, StandardMessage msg, Properties callProp) {
return render(template, msg, callProp, null);
}
/**
* 템플릿에 StandardMessage, callProp, exception 값을 치환해 최종 문자열을 반환한다.
* foreach 블록과 스칼라 변수를 좌→우 순서로 단일 패스 처리한다.
* package-private: 단위 테스트에서 직접 호출 가능
*/
String render(String template, StandardMessage msg, Properties callProp, Throwable exception) {
// 관리 UI 등에서 HTML 인코딩된 엔티티를 복원 (예: &#35; → #)
template = template.replace("&#35;", "#")
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"");
StringBuffer result = new StringBuffer();
Matcher matcher = COMBINED_PATTERN.matcher(template);
while (matcher.find()) {
String replacement;
if (matcher.group(1) != null) {
// {{#foreach path}}...{{/foreach}} 블록
String arrayPath = matcher.group(1).trim();
String blockContent = matcher.group(2);
replacement = (msg != null) ? renderForeach(arrayPath, blockContent, msg) : "";
} else {
// ${path} / ${callprop.키} / ${exception.필드} 스칼라 변수
String expr = matcher.group(3).trim();
replacement = resolveScalar(expr, msg, callProp, exception);
}
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
/**
* 변수 표현식을 해석해 값을 반환한다.
* 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
* - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회
* - "callprop.키[:기본값]" → callProp 직접/중첩 조회
* - "exception.필드[:기본값]" → 리플렉션으로 예외 객체 getter/필드 조회
* - "경로[:기본값]" → StandardMessage.findItemValue(경로)
*/
private String resolveScalar(String expr, StandardMessage msg, Properties callProp, Throwable exception) {
if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) {
return resolveIndirectCallProp(expr, msg, callProp);
}
if (expr.startsWith(CALLPROP_PREFIX)) {
String rest = expr.substring(CALLPROP_PREFIX.length());
int colonIdx = rest.indexOf(':');
String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
if (callProp == null) return defaultVal;
String value = resolveCallProp(callProp, keyPath);
return value != null ? value : defaultVal;
}
if (expr.startsWith(EXCEPTION_PREFIX)) {
String rest = expr.substring(EXCEPTION_PREFIX.length());
int colonIdx = rest.indexOf(':');
String fieldName = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
if (exception == null) return defaultVal;
String value = resolveExceptionProperty(exception, fieldName);
return value != null ? value : defaultVal;
}
int colonIdx = expr.indexOf(':');
String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : "";
String value = resolveMessagePath(msg, path);
return StringUtils.isNotEmpty(value) ? value : defaultVal;
}
/**
* StandardMessage 경로를 해석한다.
* 1. 경로에 '[n]' 구문이 있으면 배열 인덱스 접근으로 처리한다.
* 예) MSG.MSG_LIST[0].outp_msg_cd → MSG_LIST 첫 번째 행의 outp_msg_cd 값
* 2. findItemValue(path) 로 직접 조회한다.
* 3. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우
* 나머지 경로(subPath)를 키로 JSON 필드를 추출한다.
* 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo
* DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city
*/
private String resolveMessagePath(StandardMessage msg, String path) {
if (msg == null) return null;
int bracketStart = path.indexOf('[');
if (bracketStart >= 0) {
return resolveArrayIndex(msg, path, bracketStart);
}
String value = msg.findItemValue(path);
if (value != null) return value;
int dotIdx = path.lastIndexOf('.');
while (dotIdx > 0) {
String parentPath = path.substring(0, dotIdx);
String subPath = path.substring(dotIdx + 1);
String parentVal = msg.findItemValue(parentPath);
if (StringUtils.isNotBlank(parentVal)) {
String extracted = extractFromJson(parentVal, subPath);
if (extracted != null) return extracted;
}
dotIdx = parentPath.lastIndexOf('.');
}
return null;
}
/**
* 배열 인덱스 접근 구문 처리: {arrayPath}[{n}].{fieldName}
* - arrayPath : GRID 타입 아이템 경로 (예: MSG.MSG_LIST)
* - n : 0-based 행 인덱스
* - fieldName : 행 내 컬럼명 (예: outp_msg_cd)
* 인덱스가 범위를 벗어나거나 필드가 없으면 null 반환.
*/
private String resolveArrayIndex(StandardMessage msg, String path, int bracketStart) {
if (msg == null) return null;
int bracketEnd = path.indexOf(']', bracketStart);
if (bracketEnd < 0) return null;
String arrayPath = path.substring(0, bracketStart);
String indexStr = path.substring(bracketStart + 1, bracketEnd);
int index;
try {
index = Integer.parseInt(indexStr.trim());
} catch (NumberFormatException e) {
return null;
}
String fieldName = null;
if (bracketEnd + 2 <= path.length() - 1 && path.charAt(bracketEnd + 1) == '.') {
fieldName = path.substring(bracketEnd + 2);
}
if (StringUtils.isBlank(fieldName)) return null;
StandardItem arrayItem = msg.findItem(arrayPath);
if (arrayItem == null) return null;
List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
if (rows == null || index < 0 || index >= rows.size()) return null;
StandardItem item = rows.get(index).get(fieldName);
return item != null ? StringUtils.defaultString(item.getValue()) : null;
}
/**
* JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다.
* 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로,
* 오브젝트/배열이면 JSON 문자열 그대로 반환한다. 파싱 실패 시 null 반환.
*/
private String extractFromJson(String json, String fieldPath) {
try {
JsonElement el = new JsonParser().parse(json);
for (String part : fieldPath.split("\\.", -1)) {
if (!el.isJsonObject()) return null;
el = el.getAsJsonObject().get(part);
if (el == null) return null;
}
return el.isJsonPrimitive() ? el.getAsString() : el.toString();
} catch (Exception e) {
return null;
}
}
/**
* ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리.
* 1. 표준전문 경로로 callProp 키를 동적으로 결정한다.
* 2. 결정된 키로 callProp 에서 값을 조회한다.
* 3. 키 또는 값을 찾지 못하면 기본값(없으면 빈 문자열)을 반환한다.
*/
private String resolveIndirectCallProp(String expr, StandardMessage msg, Properties callProp) {
int closeIdx = expr.indexOf(']');
if (closeIdx < 0) return "";
String msgPath = expr.substring(CALLPROP_INDIRECT_PREFIX.length(), closeIdx);
String defaultVal = (closeIdx + 1 < expr.length() && expr.charAt(closeIdx + 1) == ':')
? expr.substring(closeIdx + 2)
: "";
String key = msg.findItemValue(msgPath);
if (StringUtils.isBlank(key)) return defaultVal;
if (callProp == null) return defaultVal;
String value = resolveCallProp(callProp, key);
return value != null ? value : defaultVal;
}
/**
* Properties 체인을 점(.) 구분자로 재귀 탐색한다.
* - 첫 세그먼트의 값이 Properties 이면 나머지 경로로 재귀
* - String 이면 반환, 그 외 Object 이면 toString() 반환
* - 첫 세그먼트 값이 Properties 가 아닌 경우 전체 keyPath 로 폴백 조회
*/
private String resolveCallProp(Properties props, String keyPath) {
int dotIdx = keyPath.indexOf('.');
if (dotIdx < 0) {
Object val = props.get(keyPath);
if (val == null) return null;
if (val instanceof String) return (String) val;
return val.toString();
}
String first = keyPath.substring(0, dotIdx);
String rest = keyPath.substring(dotIdx + 1);
Object val = props.get(first);
if (val instanceof Properties) {
return resolveCallProp((Properties) val, rest);
}
// 중간 값이 Properties 가 아닌 경우: 전체 keyPath 를 키로 폴백
Object direct = props.get(keyPath);
if (direct == null) return null;
if (direct instanceof String) return (String) direct;
return direct.toString();
}
/**
* GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환,
* 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
*/
private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
StandardItem arrayItem = msg.findItem(arrayPath);
if (arrayItem == null) {
if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
return "";
}
List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
if (rows == null || rows.isEmpty()) {
return "";
}
List<String> renderedItems = new ArrayList<>();
for (LinkedHashMap<String, StandardItem> row : rows) {
String rowText = renderRow(blockContent, row);
if (StringUtils.isNotBlank(rowText)) {
renderedItems.add(rowText);
}
}
return String.join(",", renderedItems);
}
/**
* 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다.
* fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다.
*/
private String renderRow(String blockContent, LinkedHashMap<String, StandardItem> row) {
StringBuffer result = new StringBuffer();
Matcher varMatcher = VAR_PATTERN.matcher(blockContent);
while (varMatcher.find()) {
String fieldName = varMatcher.group(1).trim();
String value = "";
StandardItem item = row.get(fieldName);
if (item != null) {
value = StringUtils.defaultString(item.getValue());
}
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
}
varMatcher.appendTail(result);
return result.toString().trim();
}
@Override
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception {
String templateKey = inboudnAdapterGroupName + ".sys.template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template)) {
if (logger.isWarn()) {
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
+ ", key=" + templateKey);
}
return null;
}
return render(template, resEaiMsg.getStandardMessage(), callProp);
}
@Override
public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName,
Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception {
AdapterGroupVO adapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
AdapterVO adapterVO = AdapterManager.getInstance().getAdapterVO(adapterGroupName, adapterName);
Properties httpProp = AdapterPropManager.getInstance().getProperties(adapterVO.getPropGroupName());
String adptMsgType = adapterVO.getAdapterGroupVO().getMessageType();
String encode = StringUtils.defaultIfBlank(adapterGroupVO.getMessageEncode(), "UTF-8");
if (e instanceof HttpStatusException) {
HttpStatusException e1 = (HttpStatusException) e;
int httpCode = e1.getStatus();
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e1, httpCode);
} else if (e instanceof JwtAuthException) {
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e,
HttpStatus.UNAUTHORIZED.value());
} else {
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e,
HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
private Object genInboundErrorResponse(String adapterGroupName, Properties callProp, Properties httpProp,
String adptMsgType, String encode, Throwable e1, int httpCode) {
String templateKey = adapterGroupName + ".in." + httpCode + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template)) {
templateKey = adapterGroupName + ".in.template";
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template)) {
String errorResponseFormat = httpProp.getProperty("ERROR_RESPONSE_FORMAT");
return MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode,
MessageUtil.ERROR_CODE_AP_ERROR, e1.getMessage(), errorResponseFormat);
}
}
return render(template, null, callProp, e1);
}
/**
* 리플렉션으로 예외 객체의 필드 값을 동적으로 조회한다.
* 탐색 순서: getXxx() getter → isXxx() getter → 필드 직접 접근
* 상위 클래스(Throwable 포함)까지 계층적으로 탐색한다.
*/
private String resolveExceptionProperty(Throwable e, String fieldName) {
if (StringUtils.isBlank(fieldName)) return null;
String cap = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
for (String prefix : new String[]{"get", "is"}) {
Method m = findMethod(e.getClass(), prefix + cap);
if (m != null) {
try {
Object val = m.invoke(e);
return val != null ? val.toString() : null;
} catch (Exception ignored) {}
}
}
Field f = findField(e.getClass(), fieldName);
if (f != null) {
try {
f.setAccessible(true);
Object val = f.get(e);
return val != null ? val.toString() : null;
} catch (Exception ignored) {}
}
return null;
}
private Method findMethod(Class<?> clazz, String name) {
while (clazz != null) {
try {
Method m = clazz.getDeclaredMethod(name);
m.setAccessible(true);
return m;
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
private Field findField(Class<?> clazz, String name) {
while (clazz != null) {
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
}
@@ -570,8 +570,8 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception; public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception;
protected boolean isTas(Properties tempProp) { protected boolean isTas(Properties tempProp) {
String simYn = tempProp.getProperty("SIM_YN"); String tranType = tempProp.getProperty("tranType");
return EAIServerManager.getInstance().isTASEnabledEAIServer() return EAIServerManager.getInstance().isTASEnabledEAIServer()
&& StringUtils.equals(simYn, EAIMessageKeys.TRANTYPE_TAS); && StringUtils.equals(tranType, EAIMessageKeys.TRANTYPE_TAS);
} }
} }
@@ -0,0 +1,24 @@
package com.eactive.eai.adapter.http.client.impl;
import java.nio.charset.Charset;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.net.WWWFormCodec;
/**
* HttpComponent5에서 지원하는 UrlEncodedFormEntity 클래스는 chunked 옵션 설정을 지원하지 않아 새롭게 만들었음.
*/
public class CustomUrlEncodedFormEntity extends StringEntity {
public CustomUrlEncodedFormEntity(final Iterable<? extends NameValuePair> parameters, final Charset charset,
final boolean chunked) {
super(WWWFormCodec.format(parameters,
charset != null ? charset : ContentType.APPLICATION_FORM_URLENCODED.getCharset()),
charset != null ? ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset)
: ContentType.APPLICATION_FORM_URLENCODED,
chunked);
}
}
@@ -7,6 +7,8 @@ import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
import com.eactive.eai.common.TransactionContextKeys; import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.message.MessageType; import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.util.CommonLib; import com.eactive.eai.common.util.CommonLib;
import com.eactive.eai.common.util.TxFileLogger;
import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch; import org.apache.commons.lang3.time.StopWatch;
@@ -119,6 +121,8 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData)); "SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
} }
TxFileLogger.logTxFile(tempProp, sendData, "[OUT_SEND]");
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID); String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
HttpContext context = new BasicHttpContext(); HttpContext context = new BasicHttpContext();
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid); context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
@@ -147,6 +151,7 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
} }
stopWatch.stop(); stopWatch.stop();
TxFileLogger.logTxFile(tempProp, responseString, "[OUT_RECV]");
if (status >= 400 && status < 500) { if (status >= 400 && status < 500) {
@@ -172,7 +177,8 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
+ CommonLib.getDumpMessage(responseMessage)); + CommonLib.getDumpMessage(responseMessage));
} }
return responseMessage; // return responseMessage;
return responseString;
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우 } catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
if (vo.getTraceLevel() >= 3) { if (vo.getTraceLevel() >= 3) {
@@ -6,6 +6,7 @@ import java.math.BigDecimal;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URI; import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@@ -75,6 +76,8 @@ import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.message.MessageType; import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.CommonLib; import com.eactive.eai.common.util.CommonLib;
import com.eactive.eai.common.util.TxFileLogger;
import com.eactive.eai.common.util.XMLUtils;
import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.JsonPath;
import com.kjbank.encrypt.exchange.crypto.AES256Cipher; import com.kjbank.encrypt.exchange.crypto.AES256Cipher;
@@ -116,7 +119,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; // jwhong public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; // jwhong
static final String KJB_ROOTLESS_ARRAY = "{ \"KJB_ROOTLESS_ARRAY\" : "; static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : ";
private boolean useAdapterToken; private boolean useAdapterToken;
private boolean encryptResponseApply; private boolean encryptResponseApply;
@@ -138,17 +141,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정 // 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
// 2025.05.29 프로퍼티 그룹 AUTH_TOKEN이 없을때 Null Point Exception 발생하여 수정 // 2025.05.29 프로퍼티 그룹 AUTH_TOKEN이 없을때 Null Point Exception 발생하여 수정
String bizCode = ""; String bizCode = "";
String authToken = "";
try {
Properties authProp = PropManager.getInstance().getProperties(AUTH_TOKEN); //Authorization
bizCode = vo.getAdapterGroupName().substring(1, 4);
authToken = authProp.getProperty(bizCode, "");
} catch (Exception e) {
logger.debug("Property Group AUTH_TOKEN is not Exist !!");
}
logger.debug("authToken :::::::::::::::::::::::::::::::::::" + authToken);
String headerGroupName = prop.getProperty(HEADER_GROUP); String headerGroupName = prop.getProperty(HEADER_GROUP);
String relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, ""); String relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, "");
useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y"); useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
@@ -194,11 +186,28 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
String rmethod = (String) restOptionObject.getOrDefault("method", inboundMethod); String rmethod = (String) restOptionObject.getOrDefault("method", inboundMethod);
String contentType = (String) restOptionObject.getOrDefault("contentType", "application/json"); String contentType = (String) restOptionObject.getOrDefault("contentType", "application/json");
ContentType oContentType = ContentType.parse(contentType);
Charset contentTypeCharset = oContentType.getCharset();
String charset = vo.getEncode();
if(contentTypeCharset != null)
charset = contentTypeCharset.displayName();
logger.debug("HttpClient5AdapterServiceRest] charset = {}", charset);
String mimeType = oContentType.getMimeType();
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn"); String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다. if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y"); useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
} }
String cnkUseYn = (String) restOptionObject.get("cnkUseYn");
boolean chunked = false;
if (StringUtils.isNotBlank(cnkUseYn)) {
chunked = StringUtils.equalsIgnoreCase(cnkUseYn, "Y");
}
HttpClient mclient = this.client; HttpClient mclient = this.client;
String sendData = ""; String sendData = "";
if (data instanceof String) { if (data instanceof String) {
@@ -209,27 +218,39 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
////mclient.getParams().setContentCharset(vo.getEncode()); ////mclient.getParams().setContentCharset(vo.getEncode());
JSONObject dataObject = null; Object dataObject = null;
Object httpHeader = null; Object httpHeader = null;
Object dataContent = null; //Object dataContent = null;
if (MessageType.JSON.equals(messageType)) { if (MessageType.JSON.equals(messageType)) {
Object parsed = parseJsonGeneric(sendData); Object parsed = parseJsonGeneric(sendData);
if(parsed instanceof JSONObject) { if(parsed instanceof JSONObject) {
dataObject = parseJson(sendData); JSONObject jsonObject = (JSONObject) parsed;
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) { if (jsonObject != null && StringUtils.isNotBlank(headerGroupName)) {
httpHeader = dataObject.get(headerGroupName); httpHeader = jsonObject.get(headerGroupName);
dataObject.remove(headerGroupName); jsonObject.remove(headerGroupName);
} }
dataContent = dataObject; dataObject = jsonObject;
if (dataObject.containsKey("innerList")) {
JSONArray innerList = (JSONArray) dataObject.get("innerList"); if (jsonObject.containsKey("innerList")) {
dataContent = innerList; JSONArray innerList = (JSONArray) jsonObject.get("innerList");
dataObject = innerList;
} }
} else if(parsed instanceof JSONArray) {
// not support
} }
} else if (MessageType.XML.equals(messageType)) {
Document doc = XMLUtils.convertXmlDocument(sendData);
if (doc != null && StringUtils.isNotBlank(headerGroupName)) {
Element root = doc.getRootElement();
Element httpHeaderElement = root.element(headerGroupName);
root.remove(httpHeaderElement);
httpHeader = httpHeaderElement;
}
dataObject = doc;
} }
// if (MessageType.JSON.equals(messageType)) { // if (MessageType.JSON.equals(messageType)) {
@@ -246,11 +267,15 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
Map<String, String> inboundPathVariables = (Map<String, String>) tempProp.get(HttpAdapterServiceKey.INBOUND_PATH_VARIABLES); Map<String, String> inboundPathVariables = (Map<String, String>) tempProp.get(HttpAdapterServiceKey.INBOUND_PATH_VARIABLES);
// interface 에 설정된게 우선한다. // interface 에 설정된게 우선한다.
String uri = null; String uri = null;
if(this.isTas(tempProp)) {
uri = vo.getUrl();
} else {
if (dataObject == null) { if (dataObject == null) {
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData, inboundPathVariables); uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData, inboundPathVariables);
} else { } else {
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject, inboundPathVariables); uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject, inboundPathVariables);
} }
}
logger.debug("HttpClientAdapterServiceRest] dataObject2 = [" + dataObject + "]"); logger.debug("HttpClientAdapterServiceRest] dataObject2 = [" + dataObject + "]");
@@ -350,6 +375,18 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
} }
} }
// 업무별 고정 token
String authToken = "";
try {
Properties authProp = PropManager.getInstance().getProperties(AUTH_TOKEN); //Authorization
bizCode = vo.getAdapterGroupName().substring(1, 4);
authToken = authProp.getProperty(bizCode, "");
} catch (Exception e) {
logger.debug("Property Group AUTH_TOKEN is not Exist !!");
}
logger.debug("authToken :::::::::::::::::::::::::::::::::::" + authToken);
if(!StringUtils.isBlank(authToken)) { if(!StringUtils.isBlank(authToken)) {
setAuthHeaders(method, authToken); setAuthHeaders(method, authToken);
} }
@@ -382,8 +419,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
// assignPostBody(dataObject, contentType, vo.getEncode(), method); // assignPostBody(dataObject, contentType, vo.getEncode(), method);
//assignPostBody(dataContent, contentType, vo.getEncode(), method); // jwhong commnet //assignPostBody(dataContent, contentType, vo.getEncode(), method); // jwhong commnet
// KJBank API 추적정보를 Header에 제공. jwhong // KJBank API 추적정보를 Header에 제공. jwhong
String inboundToken= assignRequestKJHeaders(method, prop, tempProp ); // String inboundToken= assignRequestKJHeaders(method, prop, tempProp );
assignPostBody(dataContent, contentType, vo.getEncode(), method, vo.getAdapterName(), auth, inboundToken, currentDate); assignPostBody(dataObject, mimeType, charset, method, chunked);
break; break;
default: default:
break; break;
@@ -402,17 +439,19 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
int status = -1; int status = -1;
Header[] responseHeaders = null;
String responseString = null;
try { try {
byte[] responseMessage = null; byte[] responseMessage = null;
byte[] responseMessageOri = null; // byte[] responseMessageOri = null;
Header[] responseHeaders; String responseContentType = null;
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) { if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
if (dataObject != null) { if (dataObject instanceof JSONObject) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = ["
+ dataObject.toJSONString() + "]"); + ((JSONObject)dataObject).toJSONString() + "]");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = " logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(dataObject.toJSONString())); + CommonLib.getDumpMessage(((JSONObject)dataObject).toJSONString()));
} else { } else {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" + sendData logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" + sendData
+ "]"); + "]");
@@ -428,6 +467,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData)); "SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
} }
TxFileLogger.logTxFile(tempProp, sendData, "[OUT_SEND]");
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID); String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
HttpContext context = new BasicHttpContext(); HttpContext context = new BasicHttpContext();
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid); context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
@@ -439,8 +480,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
} }
// encrypt algorithm type 추출 from adapter property jwhong // encrypt algorithm type 추출 from adapter property jwhong
Properties properties = AdapterPropManager.getInstance().getProperties(vo.getAdapterName()); // jwhong // Properties properties = AdapterPropManager.getInstance().getProperties(vo.getAdapterName()); // jwhong
String encryptAlgorithm = properties.getProperty("ENCRYPT_ALGORITHM"); // jwhong // String encryptAlgorithm = properties.getProperty("ENCRYPT_ALGORITHM"); // jwhong
try { try {
if (logger.isDebug()) { if (logger.isDebug()) {
@@ -465,18 +506,30 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
//responseMessage = EntityUtils.toByteArray(response.getEntity()); //responseMessage = EntityUtils.toByteArray(response.getEntity());
// 역기서 responseMessage를 decryption 해야 하나 by jwhong // 역기서 responseMessage를 decryption 해야 하나 by jwhong
responseMessageOri = EntityUtils.toByteArray(response.getEntity()); responseMessage = EntityUtils.toByteArray(response.getEntity());
logger.debug("[responseMessageOri 바이트 배열 길이 ]" + responseMessageOri.length); logger.debug("[responseMessage 바이트 배열 길이 ]" + responseMessage.length);
responseMessage = doOutboundPostFilter(responseMessageOri, encryptAlgorithm, vo.getAdapterName(), accessToken); // jwhong // responseMessage = doOutboundPostFilter(responseMessageOri, encryptAlgorithm, vo.getAdapterName(), accessToken); // jwhong
// 여기까지 // 여기까지
responseHeaders = response.getHeaders(); responseHeaders = response.getHeaders();
Header headerContentType = response.getHeader("Content-Type");
if(headerContentType != null) {
responseContentType = headerContentType.getValue();
}
if(responseContentType == null)
responseContentType = ContentType.APPLICATION_JSON.getMimeType();
tempProp.setProperty("RES_CONTENT_TYPE", responseContentType);
TxFileLogger.logTxFile(tempProp, new String(responseMessage, vo.getEncode()), "[OUT_RECV]");
if (logger.isDebug()) { if (logger.isDebug()) {
logger.debug("[received status]" + status); logger.debug("[received status]" + status);
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]"); logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]");
} }
} }
} catch (ConnectException e) { } catch (ConnectException e) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) { if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
@@ -502,15 +555,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
*/ */
boolean needReissue = false; boolean needReissue = false;
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders); responseString = new String(responseMessage, vo.getEncode());
String responseString = new String(responseMessage, vo.getEncode());
Map<String, Object> map = new HashMap<String, Object>();
tempProp.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map);
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, responseHeaderProp);
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_MESSAGE, responseString);
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_STATUS, status);
if (isStatusInIgnore(status,errIgnore )) { if (isStatusInIgnore(status,errIgnore )) {
// 이경우 정상으로 처리함 // 이경우 정상으로 처리함
@@ -580,10 +625,35 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
+ ") RETRY TOKEN = [" + newaccessToken + "]"); + ") RETRY TOKEN = [" + newaccessToken + "]");
} }
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method)) { // executor 생성 (mclient, timeout은 기존 설정 그대로)
DeadlineAwareRetryExecutor executor =
new DeadlineAwareRetryExecutor((CloseableHttpClient)mclient,
vo.getConnectionTimeout(),
vo.getTimeout());
// Properties에서 RetryPolicy 로드
RetryPolicy policy = RetryPolicy.from(prop); // 설정 로드 방식에 맞게
try (CloseableHttpResponse response = (CloseableHttpResponse) executor.execute(method, context, policy)) {
// try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method)) {
status = response.getCode(); status = response.getCode();
responseMessage = EntityUtils.toByteArray(response.getEntity()); responseMessage = EntityUtils.toByteArray(response.getEntity());
// 여기서도 decryption 해야 할듯 하나.. by jwhong logger.debug("[responseMessageOri 바이트 배열 길이 ]" + responseMessage.length);
// responseMessage = doOutboundPostFilter(responseMessageOri, encryptAlgorithm, vo.getAdapterName(), accessToken); // jwhong
responseHeaders = response.getHeaders();
Header headerContentType = response.getHeader("Content-Type");
if(headerContentType != null) {
responseContentType = headerContentType.getValue();
}
if(responseContentType == null)
responseContentType = ContentType.APPLICATION_JSON.getMimeType();
tempProp.setProperty("RES_CONTENT_TYPE", responseContentType);
TxFileLogger.logTxFile(tempProp, new String(responseMessage, vo.getEncode()), "[OUT_RECV]");
if (logger.isDebug()) { if (logger.isDebug()) {
logger.debug("[received status]" + status); logger.debug("[received status]" + status);
@@ -627,12 +697,14 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
String jsonStr = new String(responseMessage, vo.getEncode()); String jsonStr = new String(responseMessage, vo.getEncode());
if ( jsonStr.trim().startsWith("[") ) { if ( jsonStr.trim().startsWith("[") ) {
jsonStr = KJB_ROOTLESS_ARRAY + jsonStr + "}"; jsonStr = DJB_ROOTLESS_ARRAY + jsonStr + "}";
responseMessage = jsonStr.getBytes(vo.getEncode()); responseMessage = jsonStr.getBytes(vo.getEncode());
} }
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders);
assignOutboundPropertyMap(tempProp, status, responseString, responseHeaderProp);
return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), messageType, return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), responseContentType,
relayResponseHeaderKeys, status, responseHeaderProp); relayResponseHeaderKeys, status, responseHeaderProp);
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우 } catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
if (vo.getTraceLevel() >= 3) { if (vo.getTraceLevel() >= 3) {
@@ -660,6 +732,10 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
} }
logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(), logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
e); e);
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders);
assignOutboundPropertyMap(tempProp, status, responseString, responseHeaderProp);
throw e; throw e;
} finally { } finally {
if (status != HttpStatus.SC_OK) { if (status != HttpStatus.SC_OK) {
@@ -673,6 +749,15 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
} }
} }
private void assignOutboundPropertyMap(Properties tempProp, int status, String responseString,
Properties responseHeaderProp) {
Map<String, Object> map = new HashMap<String, Object>();
tempProp.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map);
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, responseHeaderProp);
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_MESSAGE, responseString);
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_STATUS, status);
}
private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) { private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) {
Properties headerProp = new Properties(); Properties headerProp = new Properties();
if (responseHeaders == null || responseHeaders.length == 0) { if (responseHeaders == null || responseHeaders.length == 0) {
@@ -699,13 +784,32 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
* @throws Exception * @throws Exception
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private String assignResponseHeaders(HttpUriRequestBase method, byte[] responseMessage, String headerGroupName, private Object assignResponseHeaders(HttpUriRequestBase method, byte[] responseMessage, String headerGroupName,
String encode, String messageType, String relayHeaderKeys, int status, Properties responseHeaderProp) throws Exception { String encode, String responseContentType, String relayHeaderKeys, int status, Properties responseHeaderProp) throws Exception {
if (!MessageType.JSON.equals(messageType) || StringUtils.isBlank(headerGroupName) String messageType = MessageType.JSON;
|| StringUtils.isBlank(relayHeaderKeys)) { if (StringUtils.startsWith(responseContentType, ContentType.APPLICATION_XML.getMimeType())) {
return new String(responseMessage, encode); logger.debug("Content-Type xml received.");
messageType = MessageType.XML;
} else if (StringUtils.startsWith(responseContentType, ContentType.APPLICATION_JSON.getMimeType())) {
logger.debug("Content-Type json received.");
messageType = MessageType.JSON;
} else {
messageType = "ETC";
} }
// charset 추출, 없으면 파라미터 encdoe 이용
ContentType oContentType = ContentType.parse(responseContentType);
Charset contentTypeCharset = oContentType.getCharset();
String charset = encode;
if (contentTypeCharset != null)
charset = contentTypeCharset.displayName();
if (!StringUtils.equalsAny(messageType, MessageType.JSON, MessageType.XML) || StringUtils.isBlank(headerGroupName)
|| StringUtils.isBlank(relayHeaderKeys)) {
return new String(responseMessage, charset);
}
if(StringUtils.equalsAny(messageType, MessageType.JSON)) {
JSONObject headerJson = new JSONObject(); JSONObject headerJson = new JSONObject();
if (StringUtils.equalsIgnoreCase(relayHeaderKeys, "ALL")) { if (StringUtils.equalsIgnoreCase(relayHeaderKeys, "ALL")) {
for (Enumeration<Object> e = responseHeaderProp.keys(); e.hasMoreElements(); ) { for (Enumeration<Object> e = responseHeaderProp.keys(); e.hasMoreElements(); ) {
@@ -725,31 +829,54 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
headerJson.put(HTTP_STATUS, String.valueOf(status)); headerJson.put(HTTP_STATUS, String.valueOf(status));
if (headerJson.size() <= 0) { if (headerJson.size() <= 0) {
return new String(responseMessage, encode); return new String(responseMessage, charset);
} }
JSONObject message = null; JSONObject message = null;
if (responseMessage == null || responseMessage.length == 0) { if (responseMessage == null || responseMessage.length == 0) {
message = new JSONObject(); message = new JSONObject();
} else { } else {
message = parseJson(new String(responseMessage, encode)); message = parseJson(new String(responseMessage, charset));
if (message == null) { if (message == null) {
message = new JSONObject(); message = new JSONObject();
message.put("Malformed_Response_Message", new String(responseMessage, encode)); message.put("Malformed_Response_Message", new String(responseMessage, charset));
} }
} }
if (StringUtils.isNotBlank(headerGroupName) && message != null) if (StringUtils.isNotBlank(headerGroupName) && message != null)
message.put(headerGroupName, headerJson); message.put(headerGroupName, headerJson);
return message.toJSONString();
return message;
} else if (StringUtils.equalsAny(messageType, MessageType.XML)) {
Document doc = XMLUtils.convertXmlDocument(new String(responseMessage, charset));
Element root = doc.getRootElement();
Element headerGroupEle = root.addElement(headerGroupName);
if (StringUtils.equalsIgnoreCase(relayHeaderKeys, "ALL")) {
for (Enumeration<Object> e = responseHeaderProp.keys(); e.hasMoreElements(); ) {
String key = (String)e.nextElement();
headerGroupEle.addElement(StringUtils.lowerCase(key)).setText(responseHeaderProp.getProperty(key));
}
} else {
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
for (String key : relayKeyArr) {
String value = responseHeaderProp.getProperty(key);
if (StringUtils.isNotBlank(value)) {
headerGroupEle.addElement(StringUtils.lowerCase(key)).setText(value);
}
}
}
return doc.asXML();
} else {
return new String(responseMessage, charset);
}
} }
private void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception { protected void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
method.setHeader("Authorization", method.setHeader("Authorization",
String.format("%s", authorization)); String.format("%s", authorization));
} }
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception { protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
method.setHeader("Authorization", method.setHeader("Authorization",
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken())); String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
} }
@@ -816,7 +943,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
return false; return false;
} }
private void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader, Properties prop) { protected void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader, Properties prop) {
if (httpHeader != null) { if (httpHeader != null) {
if (httpHeader instanceof JSONObject) { if (httpHeader instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) httpHeader; JSONObject jsonObject = (JSONObject) httpHeader;
@@ -870,7 +997,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
} }
private void assignFilterHeaders(HttpUriRequestBase method, Properties tempProp) { protected void assignFilterHeaders(HttpUriRequestBase method, Properties tempProp) {
try { try {
Properties filterHeaders = (Properties)tempProp.get("FILTERHEADER"); Properties filterHeaders = (Properties)tempProp.get("FILTERHEADER");
if (filterHeaders != null){ if (filterHeaders != null){
@@ -963,45 +1090,64 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
return map; return map;
} }
@SuppressWarnings("deprecation") protected String assignPostBody(Object eaiBody, String mimeType, String charset, HttpUriRequestBase method,
private void assignPostBody(Object eaiBody, String contentType, String charset, HttpUriRequestBase method, String adapterName, String niceAuth, String inboundToken, Date currentDate) throws Exception { boolean chunked) throws Exception {
if (eaiBody == null) { if (eaiBody == null) {
return; return "";
} }
if (eaiBody instanceof JSONObject) { if (eaiBody instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) eaiBody; JSONObject jsonObject = (JSONObject) eaiBody;
if( jsonObject.containsKey("KJB_ROOTLESS_ARRAY") ) { if( jsonObject.containsKey("DJB_ROOTLESS_ARRAY") ) {
eaiBody = jsonObject.get("KJB_ROOTLESS_ARRAY"); eaiBody = jsonObject.get("DJB_ROOTLESS_ARRAY");
} }
} }
if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) { if (StringUtils.contains(mimeType, "application/x-www-form-urlencoded")) {
if (eaiBody instanceof JSONObject) {
List<NameValuePair> params = new ArrayList<>(); List<NameValuePair> params = new ArrayList<>();
if (eaiBody instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) eaiBody; JSONObject jsonObject = (JSONObject) eaiBody;
for (Object key : jsonObject.keySet()) { for (Object key : jsonObject.keySet()) {
if( jsonObject.get(key) instanceof String ) {
params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key))); params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key)));
} }
} else if (eaiBody instanceof Document) {
Document bodyDoc = (Document) eaiBody;
Element root = bodyDoc.getRootElement();
List<Element> childrenEle = XMLUtils.getChilds(root);
for (Element child : childrenEle) {
params.add(new BasicNameValuePair(child.getName(), child.getText()));
} }
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, StandardCharsets.UTF_8); ContentType contentTypeObj = ContentType.create(mimeType, charset);
CustomUrlEncodedFormEntity entity = new CustomUrlEncodedFormEntity(params, contentTypeObj.getCharset(), chunked);
method.setEntity(entity); method.setEntity(entity);
} return params.toString();
} else { } else {
ContentType contentTypeObj = ContentType.create(contentType, charset); ContentType contentTypeObj = ContentType.create(mimeType, charset);
String eaiBodyMessage = doOutboundPreFilter(eaiBody, adapterName, niceAuth, inboundToken, currentDate); // jwhong
StringEntity entity = new StringEntity(eaiBodyMessage, contentTypeObj);
// 요청 본문을 StringEntity 객체로 생성 // 요청 본문을 StringEntity 객체로 생성
//StringEntity entity = new StringEntity(eaiBody.toString(), contentTypeObj); String body = "";
if (eaiBody instanceof Document) {
body = ((Document)eaiBody).asXML();
} else if(eaiBody instanceof JSONObject) {
body = ((JSONObject)eaiBody).toJSONString();
} else {
body = eaiBody.toString();
}
StringEntity entity = new StringEntity(body, contentTypeObj, chunked);
// 요청에 본문 추가 // 요청에 본문 추가
method.setEntity(entity); method.setEntity(entity);
return body;
// ContentType contentTypeObj = ContentType.create(contentType, charset);
//
// String eaiBodyMessage = doOutboundPreFilter(eaiBody, adapterName, niceAuth, inboundToken, currentDate); // jwhong
// StringEntity entity = new StringEntity(eaiBodyMessage, contentTypeObj);
// // 요청 본문을 StringEntity 객체로 생성
// //StringEntity entity = new StringEntity(eaiBody.toString(), contentTypeObj);
//
// // 요청에 본문 추가
// method.setEntity(entity);
} }
} }
@@ -0,0 +1,142 @@
package com.eactive.eai.adapter.http.client.impl;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Node;
import org.json.simple.JSONObject;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.client.impl.filter.HttpClient5AdapterFilterFactory;
import com.eactive.eai.adapter.http.client.impl.filter.HttpClientAdapterFilter;
/**
* 1. 기능 : HttpClient5AdapterServiceRest 호출 전후 Filter 적용할 수 있는 기능을 제공한다.<br>
* 2. 처리 개요 : <br>
* 3. 주의사항 <br>
*
* @author :
* @version : v 1.0.0
* @see : HttpClientAdapterServiceFactory.java,
* HttpClientAdapterServiceSupport.java, HttpClient5AdapterServiceRest.java
* @since :
*
*/
public class HttpClient5AdapterServiceRestAddFilter extends HttpClient5AdapterServiceRest
implements HttpClientAdapterServiceKey {
// 요청전 수행할 필터(쉼표(,)로 구분)
static final String PRE_FILTERS = "PRE_FILTERS";
// 요청후 수행할 필터(쉼표(,)로 구분)
static final String POST_FILTERS = "POST_FILTERS";
// // Adapter에서 Exception이 발생한 경우, 처리할 필터
// static final String EXCEPTION_FILTER = "EXCEPTION_FILTER";
/**
* 1. 기능 : HttpClient 호출 전후 Filter 적용용 2. 처리 개요 : <br>
* 3. 주의사항 <br>
*
* @param prop Http Adapter 속성 정보
* @return 반환 된 Object
* @exception Exception 수동 시스템 간 통신 중 발생
*/
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
String adptGrpName = tempProp.getProperty(ADAPTER_GROUP_NAME);
String adptName = tempProp.getProperty(ADAPTER_NAME);
data = doPreFilters(adptGrpName, adptName, prop, data, tempProp);
Object adapterResponse = super.execute(prop, data, tempProp);
adapterResponse = doPostFilters(adptGrpName, adptName, prop, adapterResponse, tempProp);
if (adapterResponse instanceof JSONObject)
adapterResponse = ((JSONObject) adapterResponse).toJSONString();
if (adapterResponse instanceof Node)
adapterResponse = ((Node) adapterResponse).asXML();
return adapterResponse;
}
protected Object doPreFilters(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
// boolean isSetCommonFilterInAdapterProp = false;
// 1. adapter에 설정된 필터 수행
String preFiltersStr = prop.getProperty(PRE_FILTERS);
if (StringUtils.isNotEmpty(preFiltersStr)) {
String[] preFilters = StringUtils.split(preFiltersStr, ",");
for (String filterName : preFilters) {
if (StringUtils.isBlank(filterName)) {
continue;
}
logger.debug("HttpClient5AdapterServiceRestAddFilter] Processing Start [" + filterName + "]");
HttpClientAdapterFilter adapterFilter = HttpClient5AdapterFilterFactory.createFilter(filterName.trim());
if (adapterFilter != null) {
// if (adapterFilter instanceof IBKOutCommonFilter)
// isSetCommonFilterInAdapterProp = true;
message = adapterFilter.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
} else {
throw new Exception("Failed get Filter Class: " + filterName);
}
}
}
// // 2. IBKOutCommonFilter 필터 수행(adapter 필터에서 수행하지 않을 때만)
// if (!isSetCommonFilterInAdapterProp) {
// HttpClientAdapterFilter ibkFilter = HttpClient5AdapterFilterFactory
// .createFilter(IBKOutCommonFilter.class.getName());
// if (ibkFilter != null) {
// message = ibkFilter.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
// }
// } else {
// logger.debug("IBKOutCommonFilter isSetCommonFilterInAdapterProp 'POST_FILTERS'. already in adapter filters");
// }
return message;
}
protected Object doPostFilters(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
// boolean isSetCommonFilterInAdapterProp = false;
// 1. adapter에 설정된 필터 수행
String postFiltersStr = prop.getProperty(POST_FILTERS);
if (StringUtils.isNotEmpty(postFiltersStr)) {
String[] postFilters = StringUtils.split(postFiltersStr, ",");
for (String filterName : postFilters) {
if (StringUtils.isBlank(filterName)) {
continue;
}
HttpClientAdapterFilter adapterFilter = HttpClient5AdapterFilterFactory.createFilter(filterName.trim());
if (adapterFilter != null) {
// if (adapterFilter instanceof IBKOutCommonFilter)
// isSetCommonFilterInAdapterProp = true;
message = adapterFilter.doPostFilter(adptGrpName, adptName, prop, message, tempProp);
} else {
throw new Exception("Failed get Filter Class: " + filterName);
}
}
}
// // 2. IBKOutCommonFilter 필터 수행
// if (!isSetCommonFilterInAdapterProp) {
// HttpClientAdapterFilter ibkFilter = HttpClient5AdapterFilterFactory
// .createFilter(IBKOutCommonFilter.class.getName());
// if (ibkFilter != null) {
// message = ibkFilter.doPostFilter(adptGrpName, adptName, prop, message, tempProp);
// }
// } else {
// logger.debug("IBKOutCommonFilter isSetCommonFilterInAdapterProp 'PRE_FILTERS'. already in adapter filters");
// }
return message;
}
}
@@ -0,0 +1,121 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class AllHeaderFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
public static final String HEADER_KEY_NAMES = "AllHeaderFilter.blackList";
String[] HttpHeaderRelayBlackList = {
"Content-Length",
"Transfer-Encoding",
"Host",
"Authorization",
"Accept",
"Host",
"Cookie",
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"TE",
"Trailer",
"Transfer-Encoding",
"Upgrade",
"Content-Type",
"Content-Encoding",
"Content-Language",
"Content-Location",
"Content-MD5",
"Expect",
};
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("doPreFilter Processing Start.");
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
String[] userSettingBlackList = propValue.split(",");
if( userSettingBlackList.length > 0 ) {
HttpHeaderRelayBlackList = Stream
.concat(Arrays.stream(HttpHeaderRelayBlackList), Arrays.stream(userSettingBlackList))
.toArray(String[]::new);
}
for(String keyName : inboundHeaderMap.keySet() ) {
if( StringUtils.equalsAnyIgnoreCase( keyName, HttpHeaderRelayBlackList ) ) {
logger.debug("Skip Processing Key ["+keyName+"], value ["+inboundHeaderMap.get(keyName)+"] in HttpHeaderRelayBlackList");
continue;
}
filterHeaders.put(keyName, inboundHeaderMap.get(keyName));
logger.debug("Processing Key ["+keyName+"], value ["+inboundHeaderMap.get(keyName)+"]");
}
logger.debug("doPreFilter Processing End.");
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,118 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class AsyncReponseFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String X_TRACE_ID = "partnerTraceId";
public static final String TRACE_ID = "traceId";
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
public static final String HEADER_KEY_NAMES = "AsyncReponseFilter";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("AsyncReponseFilter] PreFilter Processing Start!!");
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
String[] headerKeyNames = propValue.split(",");
headerKeyNames = Arrays.stream(headerKeyNames)
.map(String::trim)
.toArray(String[]::new);
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(X_TRACE_ID)) {
filterHeaders.put(X_TRACE_ID, inboundHeaderMap.get(X_TRACE_ID));
logger.debug("AsyncReponseFilter] Processing Key ["+X_TRACE_ID+"], value ["+inboundHeaderMap.get(X_TRACE_ID)+"]");
}
if (inboundHeaderMap.containsKey(TransactionContextKeys.X_LOAN_TOKEN)) {
filterHeaders.put(TransactionContextKeys.X_LOAN_TOKEN, inboundHeaderMap.get(TransactionContextKeys.X_LOAN_TOKEN));
logger.debug("AsyncReponseFilter] Processing Key ["+TransactionContextKeys.X_LOAN_TOKEN+"], value ["+inboundHeaderMap.get(TransactionContextKeys.X_LOAN_TOKEN)+"]");
}
}
filterHeaders.setProperty(TRACE_ID, traceId);
logger.debug("AsyncReponseFilter] Processing Key ["+TRACE_ID+"], value ["+traceId+"]");
for (String keyName : inboundHeaderMap.keySet()) {
if (StringUtils.equalsAnyIgnoreCase(keyName, headerKeyNames)) {
String value = inboundHeaderMap.get(keyName);
filterHeaders.put(keyName, value);
logger.debug("AsyncReponseFilter] Processing Key [" + keyName + "], value [" + value + "]");
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,90 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.concurrent.ConcurrentHashMap;
import com.eactive.eai.common.util.Logger;
public class HttpClient5AdapterFilterFactory {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
static String customBasePackage = "com.eactive.eai.custom.adapter.http.client.impl.filter";
static String basePackage = "com.eactive.eai.adapter.http.client.impl.filter";
private static ConcurrentHashMap<String, HttpClientAdapterFilter> h = new ConcurrentHashMap<>();
private HttpClient5AdapterFilterFactory() {
throw new IllegalStateException("Utility class");
}
public static HttpClientAdapterFilter createFilter(String type) {
if (h.containsKey(type)) {
return h.get(type);
}
HttpClientAdapterFilter filter = null;
switch (HttpClient5AdapterFilterType.getValue(type)) {
case SIMPLEFRAMEWORK:
filter = new SimpleFrameworkFilter();
break;
case SIMPLEFRAMEWORKBODY:
filter = new SimpleframeworkBodyFilter();
break;
case JBOBP:
filter = new JBOBPFilter();
break;
case KFTCFACE:
filter = new KFTCFaceFilter();
break;
case KFTCP2P:
filter = new KFTCP2PFilter();
break;
case KAKAOBANK:
filter = new KAKAOBankFilter();
break;
case NAVERFIN:
filter = new NAVERFinFilter();
break;
case NICECREDIT:
filter = new NICECreditFilter();
break;
case TRACEBODY:
filter = new TraceBodyFilter();
break;
case TOSSBANK:
filter = new TOSSBankFilter();
break;
case ASYNCRESPONSE:
filter = new AsyncReponseFilter();
break;
case ALLHEADER:
filter = new AllHeaderFilter();
break;
default:
filter = classForName(type);
if (filter == null) {
filter = classForName(customBasePackage + "." + type);
}
if (filter == null) {
filter = classForName(basePackage + "." + type);
}
break;
}
if (filter != null) {
h.put(type, filter);
return filter;
} else {
return null;
}
}
private static HttpClientAdapterFilter classForName(String type) {
try {
Class<?> cl = Class.forName(type);
return (HttpClientAdapterFilter) cl.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
logger.error("Cannot create a HttpClientAdapterFilter. - {}", type);
return null;
}
}
}
@@ -0,0 +1,27 @@
package com.eactive.eai.adapter.http.client.impl.filter;
public enum HttpClient5AdapterFilterType {
SIMPLEFRAMEWORK,
SIMPLEFRAMEWORKBODY,
JBOBP,
KFTCFACE,
KFTCP2P,
KAKAOBANK,
NAVERFIN,
NICECREDIT,
TOSSBANK,
TRACEBODY,
ASYNCRESPONSE,
ALLHEADER,
UNKNOWN;
public static HttpClient5AdapterFilterType getValue(String type) {
try {
return valueOf(type.toUpperCase());
} catch (NullPointerException e) {
return UNKNOWN;
} catch (Exception e) {
return UNKNOWN;
}
}
}
@@ -0,0 +1,12 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Properties;
public interface HttpClientAdapterFilter {
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception;
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception;
}
@@ -0,0 +1,35 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Properties;
import lombok.Getter;
public class HttpClientAdapterFilterException extends RuntimeException {
private static final long serialVersionUID = -1208983360831093751L;
@Getter
private final String code;
@Getter
private final Properties prop;
@Getter
private final Properties tempProp;
public HttpClientAdapterFilterException(String code, String msg, Properties prop, Properties tmepProp) {
super(msg);
this.code = code;
this.prop = prop;
this.tempProp = tmepProp;
}
public HttpClientAdapterFilterException(String code, String msg, Properties prop, Properties tempProp, Throwable cause) {
super(msg, cause);
this.code = code;
this.prop = prop;
this.tempProp = tempProp;
}
@Override
public String toString() {
return "HttpClientAdapterFilterException [code=" + code + ", prop=" + prop + ", tempProp=" + tempProp + ", parent=" + super.toString() + "]";
}
}
@@ -0,0 +1,106 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class JBOBPFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String X_OBP_PARTNERCODE = "x-obp-partnercode";
public static final String X_OBP_TXID = "x-obp-txid";
public static final String X_OBP_TRUST_SYSTEM = "x-obp-trust-system";
String systemTrustKey = "APIMGW-0001-QVBJTUdXLTAwMDE=";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("JBOBPFilter] PreFilter Processing Start!!");
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) systemTrustKey = propGroupVo.getProperty(X_OBP_TRUST_SYSTEM, systemTrustKey);
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
String obpPartnerCode = "000000-00";
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(X_OBP_PARTNERCODE)) {
filterHeaders.put(X_OBP_PARTNERCODE, inboundHeaderMap.get(X_OBP_PARTNERCODE));
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+inboundHeaderMap.get(X_OBP_PARTNERCODE)+"]");
} else {
filterHeaders.put(X_OBP_PARTNERCODE, obpPartnerCode);
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+obpPartnerCode+"]");
}
} else {
filterHeaders.put(X_OBP_PARTNERCODE, obpPartnerCode);
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+obpPartnerCode+"]");
}
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
if (StringUtils.isNotBlank(uuid)) {
filterHeaders.setProperty(X_OBP_TXID, uuid); // JB OBP Framework용
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_TXID+"], value ["+uuid+"]");
}
if (StringUtils.isNotBlank(systemTrustKey)) {
filterHeaders.setProperty(X_OBP_TRUST_SYSTEM, systemTrustKey); // JB OBP Framework용
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_TRUST_SYSTEM+"], value ["+systemTrustKey+"]");
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,135 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class KAKAOBankFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String PROP_PREFIX = "kakaobank";
public static final String X_KKB_PARTNER_CODE = "X-KKB-PARTNER-CODE";
public static final String X_KKB_API_NAME = "X-KKB-API-NAME";
public static final String X_KKB_API_TX_ID = "X-KKB-API-TX-ID";
public static final String X_KKB_TX_TIME = "X-KKB-TX-TIME";
public static final String X_KKB_CMPR_MGMT_NO = "X-KKB-CMPR-MGMT-NO";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("KAKAOBankFilter] PreFilter Processing Start!!");
Object returnMessage = message;
String partnerCode = "KJB";
String apiName = "status_change";
String apiTxId = "0";
String txTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String cmprMgmtNo = "0";
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) {
partnerCode = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_PARTNER_CODE);
apiName = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_API_NAME);
apiTxId = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_API_TX_ID);
cmprMgmtNo = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_CMPR_MGMT_NO);
}
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(X_KKB_PARTNER_CODE)) {
filterHeaders.put(X_KKB_PARTNER_CODE, inboundHeaderMap.get(X_KKB_PARTNER_CODE));
} else {
filterHeaders.put(X_KKB_PARTNER_CODE, partnerCode);
}
if (inboundHeaderMap.containsKey(X_KKB_API_NAME)) {
filterHeaders.put(X_KKB_API_NAME, inboundHeaderMap.get(X_KKB_API_NAME));
} else {
filterHeaders.put(X_KKB_API_NAME, apiName);
}
if (inboundHeaderMap.containsKey(X_KKB_API_TX_ID)) {
filterHeaders.put(X_KKB_API_TX_ID, inboundHeaderMap.get(X_KKB_API_TX_ID));
} else {
filterHeaders.put(X_KKB_API_TX_ID, apiTxId);
}
if (inboundHeaderMap.containsKey(X_KKB_TX_TIME)) {
filterHeaders.put(X_KKB_TX_TIME, inboundHeaderMap.get(X_KKB_TX_TIME));
} else {
filterHeaders.put(X_KKB_TX_TIME, txTime);
}
if (inboundHeaderMap.containsKey(X_KKB_CMPR_MGMT_NO)) {
filterHeaders.put(X_KKB_CMPR_MGMT_NO, inboundHeaderMap.get(X_KKB_CMPR_MGMT_NO));
} else {
filterHeaders.put(X_KKB_CMPR_MGMT_NO, cmprMgmtNo);
}
} else {
filterHeaders.put(X_KKB_PARTNER_CODE, partnerCode);
filterHeaders.put(X_KKB_API_NAME, apiName);
filterHeaders.put(X_KKB_API_TX_ID, apiTxId);
filterHeaders.put(X_KKB_TX_TIME, txTime);
filterHeaders.put(X_KKB_CMPR_MGMT_NO, cmprMgmtNo);
}
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_PARTNER_CODE+"], value ["+filterHeaders.getProperty(X_KKB_PARTNER_CODE)+"]");
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_API_NAME+"], value ["+filterHeaders.getProperty(X_KKB_API_NAME)+"]");
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_API_TX_ID+"], value ["+filterHeaders.getProperty(X_KKB_API_TX_ID)+"]");
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_TX_TIME+"], value ["+filterHeaders.getProperty(X_KKB_TX_TIME)+"]");
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_CMPR_MGMT_NO+"], value ["+filterHeaders.getProperty(X_KKB_CMPR_MGMT_NO)+"]");
} catch (Exception e) {
logger.error(e.getMessage());
}
return returnMessage;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,154 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.Logger;
public class KFTCFaceFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String PROP_PREFIX = "kftcface";
public static final String H_CLIENT_ID = "Client-Id";
public static final String B_ORG_CODE = "org_code";
public static final String B_TRANSACTION_ID = "transaction_id";
public static final String B_REQUEST_DATETIME = "request_datetime";
public static String instId = null;
static {
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
instId = eaiServerManager.getInstId();
}
@SuppressWarnings({ "unchecked" })
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("KFTCFaceFilter] PreFilter Processing Start!!");
JSONObject returnMessage = null;
String clientId = "";
String orgCode = "034"; // 광주은행 행코드
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) {
if (StringUtils.isEmpty(clientId))
clientId = propGroupVo.getProperty(PROP_PREFIX + "." + H_CLIENT_ID);
orgCode = propGroupVo.getProperty(PROP_PREFIX + "." + B_ORG_CODE);
}
String request_datetime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String reqDate = new SimpleDateFormat("yyMMdd").format(new Date());
String reqTime = new SimpleDateFormat("HHmmss").format(new Date());
// 기관코드(3) + 요청일자(6) -- 금결원 표준화 항목, 고정 9자리.
// 요청시간(6) + 인스턴스ID(2) + RandomString(3) -- 기관별 생성 거래고유번호(11자리)
// 기관코드(3) + 요청일자(6) + 요청시간(6) + 인스턴스ID(2) + RandomString(3) -- 20자리.
String transaction_id = orgCode + reqDate + reqTime + instId + RandomStringUtils.random(3, true, true).toUpperCase();
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(H_CLIENT_ID)) {
filterHeaders.put(H_CLIENT_ID, inboundHeaderMap.get(H_CLIENT_ID));
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+inboundHeaderMap.get(H_CLIENT_ID)+"]");
} else {
filterHeaders.put(H_CLIENT_ID, clientId);
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+clientId+"]");
}
} else {
filterHeaders.setProperty(H_CLIENT_ID, clientId);
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+clientId+"]");
}
} catch (Exception e) {
logger.error(e.getMessage());
}
AdapterManager adapterManager = AdapterManager.getInstance();
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(adptGrpName);
String encode = adptGrpVO.getMessageEncode();
if (message == null) {
returnMessage = new JSONObject();
} else {
if (message instanceof JSONObject) {
returnMessage = (JSONObject) message;
} else if (message instanceof String) {
returnMessage = parseJson((String) message);
} else if (message instanceof byte[]) {
returnMessage = parseJson(new String((byte[]) message, encode));
}
}
returnMessage.put(B_ORG_CODE, orgCode);
returnMessage.put(B_TRANSACTION_ID, transaction_id);
returnMessage.put(B_REQUEST_DATETIME, request_datetime);
return returnMessage.toJSONString();
}
private JSONObject parseJson(String message) throws Exception {
if (message == null) {
return null;
}
return (JSONObject) JSONValue.parse(message);
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,122 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.RandomStringUtils;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.Logger;
public class KFTCP2PFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String PROP_PREFIX = "kftcp2p";
public static final String H_ORG_CODE = "org_code";
public static final String H_TRX_NO = "api_trx_no";
public static final String H_TRX_DTM = "api_trx_dtm";
public static String instId = null;
static {
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
instId = eaiServerManager.getInstId();
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("KFTCP2PFilter] PreFilter Processing Start!!");
String orgCode = "D210400012"; // 광주은행 개발 기관코드(운영: K210800020)
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) {
orgCode = propGroupVo.getProperty(PROP_PREFIX + "." + H_ORG_CODE);
}
String apiTrxDtm = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
String apiTrxTm = new SimpleDateFormat("HHmmss").format(new Date());
// 기관코드(10) + 인스턴스ID(2) + 일시(6) + RandomString(2) -- 총 20자리
String apiTrxNo = orgCode + instId + apiTrxTm + RandomStringUtils.random(2, true, true).toUpperCase();
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(H_TRX_NO)) {
filterHeaders.put(H_TRX_NO, inboundHeaderMap.get(H_TRX_NO));
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+inboundHeaderMap.get(H_TRX_NO)+"]");
} else {
filterHeaders.put(H_TRX_NO, apiTrxNo);
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+apiTrxNo+"]");
}
if (inboundHeaderMap.containsKey(H_TRX_DTM)) {
filterHeaders.put(H_TRX_DTM, inboundHeaderMap.get(H_TRX_DTM));
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+inboundHeaderMap.get(H_TRX_DTM)+"]");
} else {
filterHeaders.put(H_TRX_DTM, apiTrxDtm);
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+apiTrxDtm+"]");
}
} else {
filterHeaders.put(H_TRX_NO, apiTrxNo);
filterHeaders.put(H_TRX_DTM, apiTrxDtm);
}
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+filterHeaders.getProperty(H_TRX_NO)+"]");
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+filterHeaders.getProperty(H_TRX_DTM)+"]");
} catch (Exception e) {
logger.error(e.getMessage());
}
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,107 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class NAVERFinFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String PROP_PREFIX = "naverfin";
public static final String X_PARTNER_ID = "X-Partner-Id";
public static final String X_FINTECH_ID = "X-Fintech-Id";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("NAVERFinFilter] PreFilter Processing Start!!");
String partnerID = "r0jtqwC9gAW5";
String fintechId = "NF";
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) {
partnerID = propGroupVo.getProperty(PROP_PREFIX + "." + X_PARTNER_ID);
fintechId = propGroupVo.getProperty(PROP_PREFIX + "." + X_FINTECH_ID);
}
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(X_PARTNER_ID)) {
filterHeaders.put(X_PARTNER_ID, inboundHeaderMap.get(X_PARTNER_ID));
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+inboundHeaderMap.get(X_PARTNER_ID)+"]");
} else {
filterHeaders.put(X_PARTNER_ID, partnerID);
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+partnerID+"]");
}
if (inboundHeaderMap.containsKey(X_FINTECH_ID)) {
filterHeaders.put(X_FINTECH_ID, inboundHeaderMap.get(X_FINTECH_ID));
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+inboundHeaderMap.get(X_FINTECH_ID)+"]");
} else {
filterHeaders.put(X_FINTECH_ID, fintechId);
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+fintechId+"]");
}
} else {
filterHeaders.put(X_PARTNER_ID, partnerID);
filterHeaders.put(X_FINTECH_ID, fintechId);
}
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+filterHeaders.getProperty(X_PARTNER_ID)+"]");
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+filterHeaders.getProperty(X_FINTECH_ID)+"]");
} catch (Exception e) {
logger.error(e.getMessage());
}
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,138 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Map.Entry;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class NICECreditFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String PROP_PREFIX = "nicecredit";
public static final String H_PRODUCTID = "ProductID";
public static final String B_REQ_DTM = "req_dtm";
public static final String B_GOODS_CLS = "goods_cls";
@SuppressWarnings({ "unchecked" })
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
JSONObject returnMessage = null;
String productId = "2303102120";
String reqDtm = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String goodsCls = "KJBAPI0001";
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) {
productId = propGroupVo.getProperty(PROP_PREFIX + "." + H_PRODUCTID);
goodsCls = propGroupVo.getProperty(PROP_PREFIX + "." + B_GOODS_CLS);
}
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (inboundHeaderMap != null && inboundHeaderMap.containsKey(H_PRODUCTID)) {
for (Entry<String, String> e : inboundHeaderMap.entrySet()) {
String key = (String) e.getKey();
String value = (String) e.getValue();
if (StringUtils.equalsIgnoreCase(key, H_PRODUCTID)) {
filterHeaders.setProperty(key, value);
}
if (logger.isDebugEnabled()) {
logger.debug("Request Header :" + key + "=[" + value + "]");
}
}
} else {
filterHeaders.setProperty(H_PRODUCTID, productId);
}
} catch (Exception e) {
logger.error(e.getMessage());
}
AdapterManager adapterManager = AdapterManager.getInstance();
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(adptGrpName);
String encode = adptGrpVO.getMessageEncode();
if (message == null) {
returnMessage = new JSONObject();
} else {
if (message instanceof JSONObject) {
returnMessage = (JSONObject) message;
} else if (message instanceof String) {
returnMessage = parseJson((String) message);
} else if (message instanceof byte[]) {
returnMessage = parseJson(new String((byte[]) message, encode));
}
}
returnMessage.put(B_REQ_DTM, reqDtm);
returnMessage.put(B_GOODS_CLS, goodsCls);
return returnMessage.toJSONString();
}
private JSONObject parseJson(String message) throws Exception {
if (message == null) {
return null;
}
return (JSONObject) JSONValue.parse(message);
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,85 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
/**
* HTTP 클라이언트 어댑터용 암복호화 필터 (송신 방향).
*
* doPreFilter : 외부 시스템으로 보내는 요청 암호화 (CRYPTO_ENC_* 프로퍼티 기반)
* doPostFilter : 외부 시스템에서 받은 응답 복호화 (CRYPTO_DEC_* 프로퍼티 기반)
*
* DYNAMIC 키 컨텍스트가 필요한 경우 {@link #buildRuntimeContext}를 오버라이드한다.
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
*
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
* AAD 관련 프로퍼티:
* PROP_AAD_HEADER tempProp에서 AAD 값을 조회할 키 이름.
* 미설정 또는 해당 키 없으면 aad=null → IV를 AAD 대체값으로 사용.
*/
public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientAdapterFilter {
/**
* DYNAMIC 키 도출용 컨텍스트를 구성한다.
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
* 서브클래스에서 오버라이드하여 tempProp으로부터 값을 추출할 수 있다.
*/
protected Map<String, String> buildRuntimeContext(Properties prop, Properties tempProp) {
return new HashMap<>();
}
/**
* CRYPTO_AAD_PROP 프로퍼티에 지정된 키로 tempProp에서 AAD 값을 조회한다.
* 미설정 또는 값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
*/
protected byte[] buildAad(Properties prop, Properties tempProp) {
String propKey = prop.getProperty(PROP_AAD_HEADER);
if (StringUtils.isBlank(propKey) || tempProp == null) {
return null;
}
String value = tempProp.getProperty(propKey);
return StringUtils.isBlank(value) ? null : value.getBytes(StandardCharsets.UTF_8);
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
String moduleName = required(prop, PROP_MODULE_NAME);
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
String body = toBodyString(message);
Map<String, String> runtimeCtx = buildRuntimeContext(prop, tempProp);
byte[] aad = buildAad(prop, tempProp);
if (SCOPE_FIELD.equals(scope)) {
return encryptField(body, moduleName, runtimeCtx, aad,
getProp(tempProp, PROP_ENC_FROM_PATH, PATH_ROOT),
getProp(tempProp, PROP_ENC_TO_PATH, PATH_ENCDATA));
}
return encryptBody(body, moduleName, runtimeCtx, aad);
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
String moduleName = required(prop, PROP_MODULE_NAME);
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
String body = toBodyString(message);
Map<String, String> runtimeCtx = buildRuntimeContext(prop, tempProp);
byte[] aad = buildAad(prop, tempProp);
if (SCOPE_FIELD.equals(scope)) {
return decryptField(body, moduleName, runtimeCtx, aad,
getProp(tempProp, PROP_DEC_FROM_PATH, PATH_ENCDATA),
getProp(tempProp, PROP_DEC_TO_PATH, PATH_ROOT));
}
return decryptBody(body, moduleName, runtimeCtx, aad);
}
}
@@ -0,0 +1,109 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.util.Logger;
public class SimpleFrameworkFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String CLIENTID = "clientId";
public static final String TRACEID = "traceId";
public static final String GUID = "guid";
public static final String RECEIVEDTIMESTAMP = "receivedTimestamp";
public static final String PARTNER_TRACE_ID = "partnerTraceId";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("SimpleFrameworkFilter] PreFilter Processing Start!!");
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
try {
/* 업체정보 */
String clientId = tempProp.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
if (StringUtils.isEmpty(clientId)) clientId = "";
filterHeaders.put(CLIENTID, clientId);
logger.debug("SimpleFrameworkFilter] Processing Key ["+CLIENTID+"], value ["+clientId+"]");
/* APIM 거래추적자 */
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
filterHeaders.put(TRACEID, uuid);
logger.debug("SimpleFrameworkFilter] Processing Key ["+TRACEID+"], value ["+uuid+"]");
/* GUID */
String guid = tempProp.getProperty(TransactionContextKeys.GUID, "");
filterHeaders.put(GUID, guid);
logger.debug("SimpleFrameworkFilter] Processing Key ["+GUID+"], value ["+guid+"]");
/* 최초요청시간 */
String receivedTimestamp = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_REQUESTED_TIME, "");
filterHeaders.put(RECEIVEDTIMESTAMP, receivedTimestamp);
logger.debug("SimpleFrameworkFilter] Processing Key ["+RECEIVEDTIMESTAMP+"], value ["+receivedTimestamp+"]");
String partnerTraceId = inboundHeaderMap.getOrDefault(PARTNER_TRACE_ID, "");
filterHeaders.put(PARTNER_TRACE_ID, partnerTraceId);
logger.debug("SimpleFrameworkFilter] Processing Key ["+PARTNER_TRACE_ID+"], value ["+partnerTraceId+"]");
String x_loan_token = inboundHeaderMap.getOrDefault(TransactionContextKeys.X_LOAN_TOKEN, "");
filterHeaders.put(TransactionContextKeys.X_LOAN_TOKEN, x_loan_token);
logger.debug("SimpleFrameworkFilter] Processing Key ["+TransactionContextKeys.X_LOAN_TOKEN+"], value ["+x_loan_token+"]");
} catch (Exception e) {
logger.error(e.getMessage());
}
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,28 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Properties;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.eactive.eai.common.TransactionContextKeys;
public class SimpleframeworkBodyFilter extends SimpleFrameworkFilter {
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
Object returnMessage = super.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
JSONObject bizJsonStr = null;
if ( returnMessage instanceof String ) {
bizJsonStr = (JSONObject) JSONValue.parse( (String)returnMessage);
if( bizJsonStr != null ) {
bizJsonStr.put("GUID", tempProp.getOrDefault(TransactionContextKeys.GUID, "") );
}
}
return bizJsonStr != null ? bizJsonStr.toJSONString() : returnMessage;
}
}
@@ -0,0 +1,97 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
public class TOSSBankFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String KJB_HEADER = "KJB_Header";
public static final String PROP_PREFIX = "tossbank";
public static final String X_TOSSBANK_LOAN_TRACE_ID = "X-TOSSBANK-LOAN-TRACE-ID";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("TOSSBankFilter] PreFilter Processing Start!!");
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
if (propGroupVo != null) {
}
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
if (filterHeaders == null) {
filterHeaders = new Properties();
tempProp.put("FILTERHEADER", filterHeaders);
}
try {
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
if (!inboundHeaderMap.isEmpty()) {
if (inboundHeaderMap.containsKey(X_TOSSBANK_LOAN_TRACE_ID)) {
filterHeaders.put(X_TOSSBANK_LOAN_TRACE_ID, inboundHeaderMap.get(X_TOSSBANK_LOAN_TRACE_ID));
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+inboundHeaderMap.get(X_TOSSBANK_LOAN_TRACE_ID)+"]");
} else {
filterHeaders.put(X_TOSSBANK_LOAN_TRACE_ID, traceId);
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+traceId+"]");
}
} else {
/* APIM 거래추적자 */
filterHeaders.setProperty(X_TOSSBANK_LOAN_TRACE_ID, traceId);
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+traceId+"]");
}
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+filterHeaders.getProperty(X_TOSSBANK_LOAN_TRACE_ID)+"]");
} catch (Exception e) {
logger.error(e.getMessage());
}
return message;
}
public Map<String, String> getInboundHeaderProp(Properties prop) {
Properties header = new Properties();
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
try {
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
if (headerObj instanceof Properties) { //tomcat
header = (Properties) headerObj;
for (String name : header.stringPropertyNames()) {
inboundHeaderMap.put(name, header.getProperty(name));
}
} else if (headerObj instanceof String) { //weblogic
String str = (String) headerObj;
str = str.substring(1, str.length() - 1);
// key=value 쌍 분리
String[] entries = str.split(",");
for (String entry : entries) {
String[] kv = entry.split("=", 2);
if (kv.length == 2) {
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return inboundHeaderMap;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
return message;
}
}
@@ -0,0 +1,71 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Properties;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.util.Logger;
import com.nimbusds.jose.shaded.gson.JsonObject;
public class TraceBodyFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String BIZ_HEADER = "header";
public static final String GUID = "guid";
public static final String TRACEID = "traceId";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
JSONObject bizJsonStr = null;
if (message instanceof String) {
bizJsonStr = (JSONObject) JSONValue.parse((String) message);
if ( bizJsonStr == null ) return message; //json string이 아님.
if (bizJsonStr.containsKey(BIZ_HEADER)) {
JSONObject bizHeader = (JSONObject) bizJsonStr.get(BIZ_HEADER);
putBizHeaderData(bizHeader, tempProp);
}else {
JSONObject bizHeader = new JSONObject();
bizJsonStr.put(BIZ_HEADER, bizHeader);
putBizHeaderData(bizHeader, tempProp);
}
}
return bizJsonStr != null ? bizJsonStr.toJSONString() : message;
}
static private void putBizHeaderData(JSONObject bizHeader, Properties tempProp) {
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
String guid = tempProp.getProperty(TransactionContextKeys.GUID, "");
bizHeader.put(GUID, guid);
logger.debug("Processing Key [" + GUID + "], value [" + guid + "]");
bizHeader.put(TRACEID, traceId);
logger.debug("Processing Key [" + TRACEID + "], value [" + traceId + "]");
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
// TODO Auto-generated method stub
return message;
}
}
@@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager; import com.eactive.eai.adapter.AdapterManager;
@@ -13,10 +14,13 @@ import com.eactive.eai.adapter.RequestDispatcher;
import com.eactive.eai.adapter.http.HttpStatusException; import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey; import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter; import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactoryKjb; import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactory;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType; import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; 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.Logger;
import com.eactive.eai.common.util.TxSiftContext;
import com.eactive.eai.common.util.UUIDGenerator;
public abstract class HttpAdapterServiceSupport implements HttpAdapterService, HttpAdapterServiceKey { public abstract class HttpAdapterServiceSupport implements HttpAdapterService, HttpAdapterServiceKey {
private static final String LOG_PREFIX = "HttpAdapterServiceSupport] "; private static final String LOG_PREFIX = "HttpAdapterServiceSupport] ";
@@ -27,7 +31,16 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
public Object service(String adptGrpName, String adptName, Object message, Properties prop, public Object service(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception { 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);
}
bMDCput = TxSiftContext.begin(uuid);
try {
String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID); String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID);
if (StringUtils.isNotBlank(transactionId)) { if (StringUtils.isNotBlank(transactionId)) {
prop.put(HttpClientAdapterServiceKey.TRANSACTION_ID, transactionId); prop.put(HttpClientAdapterServiceKey.TRANSACTION_ID, transactionId);
@@ -44,6 +57,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
prop.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId); prop.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
} }
} catch (Exception e) { } catch (Exception e) {
logger.warn(LOG_PREFIX + "Failed to read request headers: " + e.getMessage());
} }
Object obj = null; Object obj = null;
@@ -75,6 +89,10 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
} else { } else {
throw new Exception("RECEAIAHA001"); throw new Exception("RECEAIAHA001");
} }
} finally {
if(bMDCput)
TxSiftContext.end();
}
} }
private Object doPreFilters(String adptGrpName, String adptName, Object message, Properties prop, private Object doPreFilters(String adptGrpName, String adptName, Object message, Properties prop,
@@ -85,7 +103,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
if (!"HTC".equals(group.getType())) { if (!"HTC".equals(group.getType())) {
String allowIp = prop.getProperty(ALLOW_IP,""); String allowIp = prop.getProperty(ALLOW_IP,"");
if (StringUtils.isNotBlank(allowIp)) { if (StringUtils.isNotBlank(allowIp)) {
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(HttpAdapterFilterType.ADAPTERALLOWIPLISTFILTER.toString()); HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(HttpAdapterFilterType.ADAPTERALLOWIPLISTFILTER.toString());
if (adapterFilter != null) { if (adapterFilter != null) {
message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response); message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response);
} }
@@ -103,7 +121,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
continue; continue;
} }
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(filterName.trim()); HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(filterName.trim());
if (adapterFilter != null) { if (adapterFilter != null) {
message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response); message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response);
} else { } else {
@@ -126,7 +144,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
continue; continue;
} }
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(filterName.trim()); HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(filterName.trim());
if (adapterFilter != null) { if (adapterFilter != null) {
resultMessage = adapterFilter.doPostFilter(adptGrpName, adptName, resultMessage, prop, request, resultMessage = adapterFilter.doPostFilter(adptGrpName, adptName, resultMessage, prop, request,
response); response);
@@ -93,7 +93,12 @@ public class ApiAuthFilter implements HttpAdapterFilter {
@Override @Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop, public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest request, HttpServletResponse response) throws Exception {
String apiId = assignApiId(adptGrpName, adptName, message, prop, request); String apiId = prop.getProperty("FINAL_STD_MESSAGE_KEY");
String eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
if(StringUtils.isEmpty(eaiSvcCd)) {
if(StringUtils.isEmpty(apiId))
apiId = assignApiId(adptGrpName, adptName, message, prop, request);
if (StringUtils.isBlank(apiId)) { if (StringUtils.isBlank(apiId)) {
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info"); throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info");
} }
@@ -103,7 +108,8 @@ public class ApiAuthFilter implements HttpAdapterFilter {
* ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음 * ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음
*/ */
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId); StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
String eaiSvcCd = StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage); eaiSvcCd = StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
}
// if (StringUtils.isNotEmpty(eaiSvcCd) && !StringUtils.equals(eaiSvcCd, )) // if (StringUtils.isNotEmpty(eaiSvcCd) && !StringUtils.equals(eaiSvcCd, ))
// eaiSvcCd = prop.getProperty("API_SERVICE_CODE"); // eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
EAIMessage eaiMessage = EAIMessageManager.getInstance().getEAIMessage(eaiSvcCd); EAIMessage eaiMessage = EAIMessageManager.getInstance().getEAIMessage(eaiSvcCd);
@@ -0,0 +1,95 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.message.EAIMessageManager;
import com.eactive.eai.common.stdmessage.STDMessageManager;
import com.eactive.eai.common.stdmessage.STDMsgInfoAddOnVO;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.inbound.action.ActionFactory;
import com.eactive.eai.inbound.action.RequestAction;
import com.eactive.eai.inbound.processor.Processor;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.manager.StandardMessageManager;
import com.eactive.eai.message.service.InterfaceMapper;
/**
* 요청 url를 기반으로 interface id 찾기
* tx prop에 STD_MESSAGE_KEY, FINAL_STD_MESSAGE_KEY, INTERFACE_ID, EAI_MESSAGE 세팅
*/
public class ApiKeyExtractFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String STD_MESSAGE_KEY = "STD_MESSAGE_KEY";
public static final String FINAL_STD_MESSAGE_KEY = "FINAL_STD_MESSAGE_KEY";
public static final String API_SERVICE_CODE = "API_SERVICE_CODE";
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
RequestAction action = ActionFactory.createAction(actionName);
action.setAdapterInfo(adptGrpName, adptName, prop);
String[] keys = action.perform(message);
String requestPath = keys[0];
prop.setProperty(STD_MESSAGE_KEY, requestPath);// action class 통해서 생성
// PathVariable 지원 추가
String ruledPath = getMatchedKey(requestPath);
if(ruledPath == null)
throw new IllegalAccessException("path not found - " + requestPath);
prop.setProperty(FINAL_STD_MESSAGE_KEY, ruledPath);// StandardMessageUtil.getMatchedKey(), url
// pathvariable도 처리
String apiId = prop.getProperty(ApiKeyExtractFilter.FINAL_STD_MESSAGE_KEY);
if (StringUtils.isBlank(apiId)) {
throw new JwtAuthException(MessageUtil.ERROR_CODE_AUTH_FAIL, "Can not find apiId(apiCode) info");
}
String apiSvcCode = this.getEaiSvcCode(apiId);
prop.put(API_SERVICE_CODE, apiSvcCode);
return message;
}
private String getEaiSvcCode(String apiId) throws Exception {
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
return StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
return resultMessage;
}
/**
* API 서비스에서 실제 요청된 path를 설정된 PathVariable 형태의 key를 찾는다
* ex) /api/bank/account/12345 --> /api/bank/account/{accountNo}
* @param key
* @param actionName
* @return
*/
public static String getMatchedKey(String key) {
STDMessageManager stdMessageManager = STDMessageManager.getInstance();
String[] keys = stdMessageManager.getAllSTDMessageKeys();
for (String keyStr : keys) {
if (StringUtils.equals(key, keyStr)) {
return keyStr;
}
}
return stdMessageManager.getMatchedPathVariable(key);
}
}
@@ -1,222 +0,0 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.JsonPathUtil;
/**
* HTTP 어댑터 요청/응답 암복호화 필터 기반 클래스.
*
* 서브클래스는 {@link #buildRuntimeContext}를 구현하여
* DYNAMIC 키 도출에 필요한 컨텍스트 맵을 제공한다.
* STATIC 키 모듈을 사용하는 경우 빈 Map을 반환하면 된다.
* (CryptoModuleManager가 KEY_SOURCE_TYPE=STATIC이면 runtimeContext를 무시하고 고정 키를 사용한다.)
*
* 어댑터 프로퍼티 키:
* crypto.module.name 필수. DB에 등록된 암호화 모듈명.
* crypto.scope BODY | FIELD (기본 BODY)
* BODY : 메시지 전체를 암복호화 (Base64 인코딩/디코딩)
* FIELD : 특정 JSON 경로의 값만 암복호화
* crypto.dec.enabled Y | N (기본 Y). doPreFilter(요청) 복호화 수행 여부.
* crypto.enc.enabled Y | N (기본 N). doPostFilter(응답) 암호화 수행 여부.
*
* FIELD 범위 전용:
* crypto.dec.from.path 복호화 대상 JSON 경로 (예: /encryptedData)
* crypto.dec.to.path 복호화 결과 반영 경로. "/" = 전체 body 교체.
* crypto.enc.from.path 암호화 대상 JSON 경로. "/" = 전체 body 암호화.
* crypto.enc.to.path 암호화 결과(Base64)를 넣을 JSON 경로 (예: /encryptedData)
*/
public abstract class CryptoFilter implements HttpAdapterFilter {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROP_MODULE_NAME = "crypto.module.name";
public static final String PROP_SCOPE = "crypto.scope";
public static final String PROP_DEC_ENABLED = "crypto.dec.enabled";
public static final String PROP_ENC_ENABLED = "crypto.enc.enabled";
public static final String PROP_DEC_FROM_PATH = "crypto.dec.from.path";
public static final String PROP_DEC_TO_PATH = "crypto.dec.to.path";
public static final String PROP_ENC_FROM_PATH = "crypto.enc.from.path";
public static final String PROP_ENC_TO_PATH = "crypto.enc.to.path";
protected static final String SCOPE_BODY = "BODY";
protected static final String SCOPE_FIELD = "FIELD";
protected static final String PATH_ROOT = "/";
// ------------------------------------------------------------------
// 추상 메서드
// ------------------------------------------------------------------
/**
* DYNAMIC 키 도출에 필요한 runtimeContext 맵을 구성한다.
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
*/
protected abstract Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request);
// ------------------------------------------------------------------
// doPreFilter: 요청 복호화
// ------------------------------------------------------------------
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!isEnabled(prop, PROP_DEC_ENABLED, "Y")) {
return message;
}
String moduleName = required(prop, PROP_MODULE_NAME);
String scope = prop.getProperty(PROP_SCOPE, SCOPE_BODY).toUpperCase();
String body = toBodyString(message);
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
if (SCOPE_FIELD.equals(scope)) {
return decryptField(body, moduleName, runtimeCtx,
required(prop, PROP_DEC_FROM_PATH),
prop.getProperty(PROP_DEC_TO_PATH, PATH_ROOT));
}
return decryptBody(body, moduleName, runtimeCtx);
}
// ------------------------------------------------------------------
// doPostFilter: 응답 암호화
// ------------------------------------------------------------------
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!isEnabled(prop, PROP_ENC_ENABLED, "N")) {
return resultMessage;
}
String moduleName = required(prop, PROP_MODULE_NAME);
String scope = prop.getProperty(PROP_SCOPE, SCOPE_BODY).toUpperCase();
String body = toBodyString(resultMessage);
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
if (SCOPE_FIELD.equals(scope)) {
return encryptField(body, moduleName, runtimeCtx,
prop.getProperty(PROP_ENC_FROM_PATH, PATH_ROOT),
required(prop, PROP_ENC_TO_PATH));
}
return encryptBody(body, moduleName, runtimeCtx);
}
// ------------------------------------------------------------------
// 복호화 구현
// ------------------------------------------------------------------
private String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx) throws Exception {
byte[] cipherBytes = Base64.getDecoder().decode(body.trim());
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, cipherBytes);
return new String(plainBytes, StandardCharsets.UTF_8);
}
/**
* fromPath의 Base64 값을 복호화하여 toPath에 반영.
* toPath = "/" → 복호화된 텍스트로 body 전체 교체.
*/
private String decryptField(String body, String moduleName, Map<String, String> runtimeCtx,
String fromPath, String toPath) throws Exception {
String encBase64 = JsonPathUtil.getValueAtPath(body, fromPath);
if (StringUtils.isBlank(encBase64)) {
logger.warn("CryptoFilter] 복호화 대상 필드 없음: path=" + fromPath);
return body;
}
byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim());
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, cipherBytes);
String plainText = new String(plainBytes, StandardCharsets.UTF_8);
if (PATH_ROOT.equals(toPath)) {
return plainText;
}
return JsonPathUtil.setValueAtPath(body, toPath, plainText, false);
}
// ------------------------------------------------------------------
// 암호화 구현
// ------------------------------------------------------------------
private String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx) throws Exception {
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx,
body.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(cipherBytes);
}
/**
* fromPath 값을 암호화하여 toPath(Base64)에 반영.
* fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑.
*/
private String encryptField(String body, String moduleName, Map<String, String> runtimeCtx,
String fromPath, String toPath) throws Exception {
String plainText;
if (PATH_ROOT.equals(fromPath)) {
plainText = body;
} else {
plainText = JsonPathUtil.getValueAtPath(body, fromPath);
if (StringUtils.isBlank(plainText)) {
logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath);
return body;
}
}
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx,
plainText.getBytes(StandardCharsets.UTF_8));
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
if (PATH_ROOT.equals(fromPath)) {
String fieldName = toPath.replaceFirst("^/", "");
return "{\"" + fieldName + "\":\"" + encBase64 + "\"}";
}
return JsonPathUtil.setValueAtPath(body, toPath, encBase64, false);
}
// ------------------------------------------------------------------
// 유틸
// ------------------------------------------------------------------
/**
* 필터로 전달되는 message 객체를 JSON 문자열로 변환한다.
* <ul>
* <li>{@code String} — 그대로 반환</li>
* <li>{@code byte[]} — UTF-8 디코딩</li>
* <li>{@code JSONObject} / 기타 — {@code toString()} 호출
* (json-simple JSONObject는 toString()이 toJSONString()을 위임함)</li>
* </ul>
*/
protected String toBodyString(Object message) {
if (message instanceof String) {
return (String) message;
}
if (message instanceof byte[]) {
return new String((byte[]) message, StandardCharsets.UTF_8);
}
return message.toString();
}
private boolean isEnabled(Properties prop, String key, String defaultVal) {
return !"N".equalsIgnoreCase(prop.getProperty(key, defaultVal));
}
protected String required(Properties prop, String key) {
String v = prop.getProperty(key);
if (StringUtils.isBlank(v)) {
throw new IllegalArgumentException("CryptoFilter 필수 프로퍼티 누락: " + key);
}
return v;
}
}
@@ -28,7 +28,7 @@ import com.eactive.eai.common.util.Logger;
public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter { public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter {
static final String KJB_ROOTLESS_ARRAY = "{ \"KJB_ROOTLESS_ARRAY\" : "; static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : ";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
@@ -63,8 +63,8 @@ public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter {
String chkBody = inboundBody; String chkBody = inboundBody;
String clientSecret = assignClientSecret(prop, request); String clientSecret = assignClientSecret(prop, request);
if (chkBody.startsWith(KJB_ROOTLESS_ARRAY)) { if (chkBody.startsWith(DJB_ROOTLESS_ARRAY)) {
chkBody = chkBody.substring(KJB_ROOTLESS_ARRAY.length()); chkBody = chkBody.substring(DJB_ROOTLESS_ARRAY.length());
if (chkBody.endsWith("}")) { if (chkBody.endsWith("}")) {
chkBody = chkBody.substring(0, chkBody.length() - 1); chkBody = chkBody.substring(0, chkBody.length() - 1);
@@ -161,8 +161,8 @@ public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter {
Object root = JSONValue.parse(outboundBody); Object root = JSONValue.parse(outboundBody);
if (root instanceof JSONObject && ((JSONObject) root).containsKey("KJB_ROOTLESS_ARRAY")) { if (root instanceof JSONObject && ((JSONObject) root).containsKey("DJB_ROOTLESS_ARRAY")) {
JSONArray jsonArray = (JSONArray) ((JSONObject) root).get("KJB_ROOTLESS_ARRAY"); JSONArray jsonArray = (JSONArray) ((JSONObject) root).get("DJB_ROOTLESS_ARRAY");
outboundBody = jsonArray.toJSONString(); outboundBody = jsonArray.toJSONString();
} }
@@ -6,13 +6,10 @@ import com.eactive.eai.common.util.Logger;
public class HttpAdapterFilterFactory { public class HttpAdapterFilterFactory {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
static String basePackage = "com.eactive.eai.adapter.http.dynamic.filter";
static String customBasePackage = "com.eactive.eai.custom.adapter.http.dynamic.filter";
static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>(); static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>();
public HttpAdapterFilterFactory() {
super();
}
@SuppressWarnings("rawtypes")
public static HttpAdapterFilter createFilter(String type) { public static HttpAdapterFilter createFilter(String type) {
if (h.containsKey(type)) { if (h.containsKey(type)) {
return (HttpAdapterFilter) h.get(type); return (HttpAdapterFilter) h.get(type);
@@ -32,12 +29,19 @@ public class HttpAdapterFilterFactory {
case ADAPTERALLOWIPLISTFILTER: case ADAPTERALLOWIPLISTFILTER:
filter = new AdapterAllowIPListFilter(); filter = new AdapterAllowIPListFilter();
break; break;
case HMAC_SHA256:
filter = new HmacSha256VerifyFilterKjb();
break;
case REFLECTALLHEADER:
filter = new ReflectAllHeaderFilter();
break;
default: default:
try { filter = classForName(type);
Class cl = Class.forName(type); if (filter == null) {
filter = (HttpAdapterFilter) cl.newInstance(); filter = classForName(basePackage + "." + type);
} catch (Exception e) { }
logger.error("Cannot create a HttpAdapterFilter. - {}", type); if (filter == null) {
filter = classForName(customBasePackage + "." + type);
} }
break; break;
} }
@@ -49,4 +53,14 @@ public class HttpAdapterFilterFactory {
return null; return null;
} }
} }
private static HttpAdapterFilter classForName(String type) {
try {
Class<?> cl = Class.forName(type);
return (HttpAdapterFilter) cl.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
return null;
}
}
} }
@@ -1,67 +0,0 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.util.concurrent.ConcurrentHashMap;
import com.eactive.eai.common.util.Logger;
public class HttpAdapterFilterFactoryKjb extends HttpAdapterFilterFactory {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
static String basePackage = "com.eactive.eai.custom.adapter.http.dynamic.filter";
static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>();
public HttpAdapterFilterFactoryKjb() {
super();
}
@SuppressWarnings("rawtypes")
public static HttpAdapterFilter createFilter(String type) {
if (h.containsKey(type)) {
return (HttpAdapterFilter) h.get(type);
}
HttpAdapterFilter filter = null;
switch (HttpAdapterFilterType.getValue(type)) {
case APIAUTHFILTER:
filter = new ApiAuthFilter();
break;
case JWTAUTHFILTER:
filter = new JwtAuthFilter();
break;
case IPWHITELISTFILTER:
filter = new IpWhiteListFilter();
break;
case ADAPTERALLOWIPLISTFILTER:
filter = new AdapterAllowIPListFilter();
break;
case HMAC_SHA256:
filter = new HmacSha256VerifyFilterKjb();
break;
case REFLECTALLHEADER:
filter = new ReflectAllHeaderFilter();
break;
default:
filter = classForName(type);
if (filter == null) {
filter = classForName(basePackage + "." + type);
}
break;
}
if (filter != null) {
h.put(type, filter);
return filter;
} else {
return null;
}
}
private static HttpAdapterFilter classForName(String type) {
try {
Class<?> cl = Class.forName(type);
return (HttpAdapterFilter) cl.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
return null;
}
}
}
@@ -0,0 +1,85 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
/**
* HTTP 서버 어댑터용 암복호화 필터 (수신 방향).
*
* doPreFilter : 수신 요청 복호화 (CRYPTO_DEC_* 프로퍼티 기반)
* doPostFilter : 송신 응답 암호화 (CRYPTO_ENC_* 프로퍼티 기반)
*
* DYNAMIC 키 컨텍스트가 필요한 경우 {@link #buildRuntimeContext}를 오버라이드한다.
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
*
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
*/
public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterFilter {
/**
* DYNAMIC 키 도출용 컨텍스트를 구성한다.
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
* 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다.
*/
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
return new HashMap<>();
}
/**
* CRYPTO_AAD_HEADER 프로퍼티에 지정된 헤더값을 UTF-8 바이트로 반환한다.
* 미설정 또는 헤더값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
*/
protected byte[] buildAad(Properties prop, HttpServletRequest request) {
String headerName = prop.getProperty(PROP_AAD_HEADER);
if (StringUtils.isBlank(headerName) || request == null) {
return null;
}
String headerValue = request.getHeader(headerName);
return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String moduleName = required(prop, PROP_MODULE_NAME);
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
String body = toBodyString(message);
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
byte[] aad = buildAad(prop, request);
if (SCOPE_FIELD.equals(scope)) {
return decryptField(body, moduleName, runtimeCtx, aad,
getProp(prop, PROP_DEC_FROM_PATH, PATH_ENCDATA),
getProp(prop, PROP_DEC_TO_PATH, PATH_ROOT));
}
return decryptBody(body, moduleName, runtimeCtx, aad);
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String moduleName = required(prop, PROP_MODULE_NAME);
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
String body = toBodyString(resultMessage);
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
byte[] aad = buildAad(prop, request);
if (SCOPE_FIELD.equals(scope)) {
return encryptField(body, moduleName, runtimeCtx, aad,
getProp(prop, PROP_ENC_FROM_PATH, PATH_ROOT),
getProp(prop, PROP_ENC_TO_PATH, PATH_ENCDATA));
}
return encryptBody(body, moduleName, runtimeCtx, aad);
}
}
@@ -194,7 +194,7 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
Object root = JSONValue.parse(body); Object root = JSONValue.parse(body);
if( root instanceof JSONArray ) { if( root instanceof JSONArray ) {
JSONObject wrappedObject = new JSONObject(); JSONObject wrappedObject = new JSONObject();
wrappedObject.put("KJB_ROOTLESS_ARRAY", root); wrappedObject.put("DJB_ROOTLESS_ARRAY", root);
requestBytes = wrappedObject.toJSONString().getBytes(encode); requestBytes = wrappedObject.toJSONString().getBytes(encode);
} }
@@ -256,7 +256,7 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
Object root = JSONValue.parse(responseData); Object root = JSONValue.parse(responseData);
if( root instanceof JSONObject ) { if( root instanceof JSONObject ) {
JSONObject wrappedObject = (JSONObject) root; JSONObject wrappedObject = (JSONObject) root;
responseData = wrappedObject.get("KJB_ROOTLESS_ARRAY").toString(); responseData = wrappedObject.get("DJB_ROOTLESS_ARRAY").toString();
} }
}catch (Exception e) { }catch (Exception e) {
@@ -0,0 +1,153 @@
package com.eactive.eai.adapter.http.filter;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.JsonPathUtil;
import com.fasterxml.jackson.databind.JsonNode;
/**
* InCryptoFilter / OutCryptoFilter 공통 기반 클래스.
*
* BODY/FIELD 범위 암복호화 로직, 프로퍼티 상수, 유틸리티 메서드를 제공한다.
* HTTP 서버 어댑터용은 {@link InCryptoFilter}, 클라이언트 어댑터용은 {@link OutCryptoFilter} 참조.
*
* 프로퍼티 키:
* CRYPTO_MODULE_NAME 필수. DB에 등록된 암호화 모듈명.
* CRYPTO_SCOPE BODY | FIELD (기본값: FIELD)
* BODY : 메시지 전체를 암복호화 (Base64 인코딩/디코딩)
* FIELD : 특정 JSON 경로의 값만 암복호화
*
* FIELD 범위 전용:
* CRYPTO_DEC_FROM_PATH 복호화 대상 JSON 경로 (기본값: /encrypted_data)
* CRYPTO_DEC_TO_PATH 복호화 결과 반영 경로. "/" = 전체 body 교체.
* CRYPTO_ENC_FROM_PATH 암호화 대상 JSON 경로. "/" = 전체 body 암호화.
* CRYPTO_ENC_TO_PATH 암호화 결과(Base64)를 넣을 JSON 경로 (기본값: /encrypted_data)
*
* GCM AAD 전용 (선택):
* CRYPTO_AAD_HEADER AAD로 사용할 HTTP 요청 헤더명 (InCryptoFilter 전용).
* 미설정 또는 헤더값 없으면 aad=null → IV를 AAD 대체값으로 사용.
*/
public abstract class AbstractCryptoFilter {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROP_MODULE_NAME = "CRYPTO_MODULE_NAME";
public static final String PROP_SCOPE = "CRYPTO_SCOPE";
public static final String PROP_DEC_FROM_PATH = "CRYPTO_DEC_FROM_PATH";
public static final String PROP_DEC_TO_PATH = "CRYPTO_DEC_TO_PATH";
public static final String PROP_ENC_FROM_PATH = "CRYPTO_ENC_FROM_PATH";
public static final String PROP_ENC_TO_PATH = "CRYPTO_ENC_TO_PATH";
public static final String PROP_AAD_HEADER = "CRYPTO_AAD_HEADER";
public static final String SCOPE_BODY = "BODY";
public static final String SCOPE_FIELD = "FIELD";
public static final String PATH_ROOT = "/";
public static final String PATH_ENCDATA = "/encrypted_data";
// ------------------------------------------------------------------
// 복호화 구현
// ------------------------------------------------------------------
protected String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx,
byte[] aad) throws Exception {
byte[] cipherBytes = Base64.getDecoder().decode(body.trim());
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
return new String(plainBytes, StandardCharsets.UTF_8);
}
/**
* fromPath의 Base64 값을 복호화하여 toPath에 반영. JSON 파싱 1회.
* toPath = "/" → fromPath 필드를 제거하고 복호화된 JSON을 body에 병합.
*/
protected String decryptField(String body, String moduleName, Map<String, String> runtimeCtx,
byte[] aad, String fromPath, String toPath) throws Exception {
JsonNode root = JsonPathUtil.toTree(body);
String encBase64 = JsonPathUtil.getAt(root, fromPath);
if (StringUtils.isBlank(encBase64)) {
logger.warn("CryptoFilter] 복호화 대상 필드 없음: path=" + fromPath);
return body;
}
byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim());
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
String plainText = new String(plainBytes, StandardCharsets.UTF_8);
if (PATH_ROOT.equals(toPath)) {
return JsonPathUtil.mergeAtRoot(root, fromPath, plainText);
}
JsonPathUtil.setAt(root, toPath, plainText);
return JsonPathUtil.fromTree(root);
}
// ------------------------------------------------------------------
// 암호화 구현
// ------------------------------------------------------------------
protected String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx,
byte[] aad) throws Exception {
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
body.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(cipherBytes);
}
/**
* fromPath 값을 암호화하여 toPath(Base64)에 반영. JSON 파싱 1회.
* fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑 (파싱 불필요).
*/
protected String encryptField(String body, String moduleName, Map<String, String> runtimeCtx,
byte[] aad, String fromPath, String toPath) throws Exception {
if (PATH_ROOT.equals(fromPath)) {
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
body.getBytes(StandardCharsets.UTF_8));
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
String fieldName = toPath.replaceFirst("^/", "");
return "{\"" + fieldName + "\":\"" + encBase64 + "\"}";
}
JsonNode root = JsonPathUtil.toTree(body);
String plainText = JsonPathUtil.getAt(root, fromPath);
if (StringUtils.isBlank(plainText)) {
logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath);
return body;
}
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
plainText.getBytes(StandardCharsets.UTF_8));
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
JsonPathUtil.setAt(root, toPath, encBase64);
return JsonPathUtil.fromTree(root);
}
// ------------------------------------------------------------------
// 유틸
// ------------------------------------------------------------------
/**
* 필터로 전달되는 message 객체를 JSON 문자열로 변환한다.
*/
protected String toBodyString(Object message) {
if (message instanceof String) {
return (String) message;
}
if (message instanceof byte[]) {
return new String((byte[]) message, StandardCharsets.UTF_8);
}
return message.toString();
}
protected String required(Properties prop, String key) {
String v = prop.getProperty(key);
if (StringUtils.isBlank(v)) {
throw new IllegalArgumentException("CryptoFilter 필수 프로퍼티 누락: " + key);
}
return v;
}
protected String getProp(Properties prop, String key, String defaultVal) {
String v = prop.getProperty(key);
return StringUtils.isBlank(v) ? defaultVal : v;
}
}
@@ -1,5 +1,6 @@
package com.eactive.eai.agent.encryption; package com.eactive.eai.agent.encryption;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.exception.ExceptionUtil;
@@ -11,6 +12,8 @@ import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.Keys; import com.eactive.eai.common.server.Keys;
import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.eactive.ext.kjb.safedb.KjbSafedbWrapper;
import com.eactive.ext.kjb.safedb.Utils;
@Component @Component
public class EncryptionManager implements Lifecycle { public class EncryptionManager implements Lifecycle {
@@ -18,6 +21,8 @@ public class EncryptionManager implements Lifecycle {
private static final String GROUP_NAME = "ENCRYPTION"; private static final String GROUP_NAME = "ENCRYPTION";
private String encryptYN = ""; private String encryptYN = "";
private String dbEncryptSolutionName = "DAMO";
private boolean started; private boolean started;
private LifecycleSupport lifecycle = new LifecycleSupport(this); private LifecycleSupport lifecycle = new LifecycleSupport(this);
@@ -55,8 +60,15 @@ public class EncryptionManager implements Lifecycle {
encryptYN = "N"; encryptYN = "N";
} }
} }
dbEncryptSolutionName = PropManager.getInstance().getProperty(GROUP_NAME, "dbEncryptSolutionName");
logger.warn("EncryptionManager] init :: encryption YN [ " + encryptYN + " ]"); if (dbEncryptSolutionName == null) {
dbEncryptSolutionName = "DAMO";
} else {
if (dbEncryptSolutionName.trim().length() == 0) {
dbEncryptSolutionName = "DAMO";
}
}
logger.warn("EncryptionManager] init :: encryption YN [ " + encryptYN + " ], dbEncryptSolutionName [ " + dbEncryptSolutionName + " ]");
} }
public synchronized void reload() throws Exception { public synchronized void reload() throws Exception {
@@ -99,4 +111,101 @@ public class EncryptionManager implements Lifecycle {
public String getEncryptYN() { public String getEncryptYN() {
return encryptYN; return encryptYN;
} }
public String getDBEncryptSolutionName() {
return dbEncryptSolutionName;
}
public boolean isEncrypt() {
return "Y".equalsIgnoreCase(encryptYN);
}
public String encryptDBData(String plainText) {
if(isEncrypt()) {
if ("DAMO".equals(dbEncryptSolutionName)) {
com.eactive.ext.djb.DamoManager damoManager = new com.eactive.ext.djb.DamoManager();
return damoManager.encrypt(plainText);
} else if ("SAFEDB".equals(dbEncryptSolutionName)) {
String originalAttribute = plainText;
if (StringUtils.isNotEmpty(plainText)) {
KjbSafedbWrapper wrapper = KjbSafedbWrapper.getInstance();
plainText = wrapper.encryptNotRnnoString(plainText);
if (logger.isInfo()) {
// originalAttribute 홀수 글자 마스킹
StringBuilder sb = new StringBuilder();
for (int i = 0; i < originalAttribute.length(); i++) {
if (i % 2 == 0) {
sb.append(originalAttribute.charAt(i));
} else {
sb.append("*");
}
}
originalAttribute = sb.toString();
// System.out.println("DB 암호화 #2 : {"+ originalAttribute +"} ->
// {"+attribute+"}");
// logger.debug("DB 암호화 #2 : {} -> {}", originalAttribute, attribute);
}
}
return plainText;
} else {
return plainText;
}
} else {
return plainText;
}
}
public String decryptDBData(String dbData) {
if (StringUtils.isNotEmpty(dbData)) {
if (this.isEncrypted(dbData)) {
KjbSafedbWrapper safeDBWrapper = KjbSafedbWrapper.getInstance();
try {
dbData = safeDBWrapper.decryptNotRnno(dbData);
} catch (Exception e) {
// 복호화 실패시 원본 반환
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
logger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
return dbData;
}
} else {
logger.debug("DB 복호화 경고: Base64 형식이 아님. 복호화하지 않고 원본 반환. dbData={}", dbData);
}
}
// logger.debug("DB 복호화 #2 : {} -> {}", dbDataOriginal, dbData);
return dbData;
}
/**
* 암호화 여부 확인(base64 인코딩 여부로 판단, 또는 0-9, - 로 구성된 경우 암호화 되지 않은 걸로 판단(연락처))
*
* @param data
* @return
*/
private boolean isEncrypted(String data) {
if (StringUtils.isEmpty(data)) {
return false;
}
// 숫자, - 로만 구성된 경우 암호화 되지 않은 걸로 판단 (ex: 연락처)
if (data.matches("^[0-9-]+$")) {
return false;
}
data = data.trim();
// Base64 형식 체크
if (!Utils.isBase64(data)) {
return false;
}
// Base64 길이 체크 (4의 배수)
if (data.length() % 4 != 0) {
return false;
}
return true;
}
} }
@@ -1,60 +0,0 @@
package com.eactive.eai.agent.inflow;
import com.eactive.eai.agent.command.Command;
import com.eactive.eai.agent.command.CommandException;
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
import com.eactive.eai.common.inflow.InflowControlUtil;
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
import com.eactive.eai.common.util.Logger;
public class ReloadInflowClientControlCommand extends Command {
private static final long serialVersionUID = 1L;
public ReloadInflowClientControlCommand() {
super("ReloadInflowClientControlCommand");
}
public Object execute() throws CommandException {
Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
if (logger.isInfo())
logger.info(this.name + " is executed");
if (!(args instanceof String)) {
String rspErrorCode = "RECEAIMCM001";
String msg = makeException(rspErrorCode, null);
if (logger.isError())
logger.error(msg);
throw new CommandException(msg);
}
String keyName = (String) args;
try {
AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager();
if (!(base instanceof ClientDualInflowControlManager)) {
throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다.");
}
ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base;
if (keyName != null) {
if ("ALL".equals(keyName)) {
manager.reloadClient();
if (logger.isWarn())
logger.warn(this.name + "] all rule Reload.");
} else {
manager.reloadClient(keyName);
if (logger.isWarn())
logger.warn(this.name + "] " + keyName + " Reload.");
}
}
} catch (Exception e) {
String rspErrorCode = "RECEAIMCM002";
String msg = makeException(rspErrorCode, e);
if (logger.isError())
logger.error(msg, e);
throw new CommandException(msg);
}
return "success";
}
}
@@ -1,50 +0,0 @@
package com.eactive.eai.agent.inflow;
import com.eactive.eai.agent.command.Command;
import com.eactive.eai.agent.command.CommandException;
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
import com.eactive.eai.common.inflow.InflowControlUtil;
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
import com.eactive.eai.common.util.Logger;
public class RemoveInflowClientControlCommand extends Command {
private static final long serialVersionUID = 1L;
public RemoveInflowClientControlCommand() {
super("RemoveInflowClientControlCommand");
}
public Object execute() throws CommandException {
Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
if (logger.isInfo())
logger.info(this.name + " is executed");
if (!(args instanceof String)) {
String rspErrorCode = "RECEAIMCM001";
String msg = makeException(rspErrorCode, null);
if (logger.isError())
logger.error(msg);
throw new CommandException(msg);
}
String key = (String) args;
try {
AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager();
if (!(base instanceof ClientDualInflowControlManager)) {
throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다.");
}
ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base;
manager.removeClient(key);
if (logger.isWarn())
logger.warn(this.name + "] " + key + " removed.");
} catch (Exception e) {
String rspErrorCode = "RECEAIMCM002";
String msg = makeException(rspErrorCode, e);
if (logger.isError())
logger.error(msg, e);
throw new CommandException(msg);
}
return "success";
}
}
@@ -0,0 +1,123 @@
package com.eactive.eai.authserver.jwt;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* PS256(RSA-PSS) JWT를 발급·검증하는 JwtAccessTokenConverter 확장 클래스.
*
* spring-security-jwt 1.1.1은 JwtAlgorithms에 PS256을 등록하지 않아
* JwtHelper.encode/decodeAndVerify가 PS256 헤더를 처리하지 못한다.
* 따라서 encode/decode를 직접 구현하여 JwtHelper를 완전히 우회한다.
*/
public class PssJwtAccessTokenConverter extends JwtAccessTokenConverter {
/** PS256 JWT 헤더 {"alg":"PS256","typ":"JWT"} 의 Base64URL 인코딩 (고정값) */
private static final String ENCODED_HEADER = Base64.getUrlEncoder().withoutPadding()
.encodeToString("{\"alg\":\"PS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8));
private final RsaPssSigner pssSigner;
private final RsaPssVerifier pssVerifier;
private final String publicKeyPem;
private final ObjectMapper jackson = new ObjectMapper();
public PssJwtAccessTokenConverter(RSAPrivateKey privateKey, RSAPublicKey publicKey) {
this.pssSigner = new RsaPssSigner(privateKey);
this.pssVerifier = new RsaPssVerifier(publicKey);
this.publicKeyPem = "-----BEGIN PUBLIC KEY-----\n"
+ Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(publicKey.getEncoded())
+ "\n-----END PUBLIC KEY-----";
// 부모 setVerifierKey — afterPropertiesSet() 내부에서 참조하는 verifierKey 필드 초기화
setVerifierKey(this.publicKeyPem);
// getKey() (/oauth/token_key 엔드포인트)가 동작하도록 부모의 verifier 필드를 주입
try {
Field f = JwtAccessTokenConverter.class.getDeclaredField("verifier");
f.setAccessible(true);
f.set(this, pssVerifier);
} catch (Exception ignored) { }
}
@Override
public void afterPropertiesSet() throws Exception {
// 부모 afterPropertiesSet()은 signingKey 기반 초기화(JwtHelper 의존)를 시도하므로 건너뜀
}
/**
* PS256 JWT 발급. JwtHelper.encode를 사용하지 않고 직접 구성한다.
* signingInput = Base64URL(header) + "." + Base64URL(payload)
* JWT = signingInput + "." + Base64URL(PSS_sign(signingInput))
*/
@Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, ?> claims = getAccessTokenConverter().convertAccessToken(accessToken, authentication);
String payload;
try {
payload = jackson.writeValueAsString(claims);
} catch (Exception e) {
throw new IllegalStateException("Access token을 JSON으로 변환 실패", e);
}
return buildJwt(payload);
}
/**
* PS256 JWT 검증 및 클레임 추출. JwtHelper.decodeAndVerify를 사용하지 않고 직접 파싱한다.
*/
@Override
protected Map<String, Object> decode(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("잘못된 JWT 형식");
}
byte[] signingInput = (parts[0] + "." + parts[1]).getBytes(StandardCharsets.US_ASCII);
byte[] sig = Base64.getUrlDecoder().decode(padBase64Url(parts[2]));
pssVerifier.verify(signingInput, sig); // 실패 시 InvalidSignatureException
byte[] payloadBytes = Base64.getUrlDecoder().decode(padBase64Url(parts[1]));
try {
@SuppressWarnings("unchecked")
Map<String, Object> result = jackson.readValue(payloadBytes, Map.class);
// JwtAccessTokenConverter 원본 동작 유지: exp 필드 Integer → Long 변환
if (result.containsKey(EXP) && result.get(EXP) instanceof Integer) {
result.put(EXP, ((Integer) result.get(EXP)).longValue());
}
return result;
} catch (Exception e) {
throw new IllegalArgumentException("JWT 페이로드 파싱 실패", e);
}
}
/** /oauth/token_key 엔드포인트 응답 — alg와 PEM 공개키 반환 */
@Override
public Map<String, String> getKey() {
Map<String, String> result = new LinkedHashMap<>();
result.put("alg", "PS256");
result.put("value", publicKeyPem);
return result;
}
public String buildJwt(String payload) {
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
.encodeToString(payload.getBytes(StandardCharsets.UTF_8));
String signingInput = ENCODED_HEADER + "." + encodedPayload;
byte[] sig = pssSigner.sign(signingInput.getBytes(StandardCharsets.US_ASCII));
return signingInput + "." + Base64.getUrlEncoder().withoutPadding().encodeToString(sig);
}
private static String padBase64Url(String s) {
int mod = s.length() % 4;
if (mod == 2) return s + "==";
if (mod == 3) return s + "=";
return s;
}
}
@@ -0,0 +1,46 @@
package com.eactive.eai.authserver.jwt;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import org.springframework.security.jwt.crypto.sign.Signer;
/**
* PS256 (RSA-PSS + SHA-256) 서명 구현체.
* RFC 7518 §3.5 파라미터: Hash=SHA-256, MGF=MGF1/SHA-256, saltLen=32, trailer=0xBC
* JDK 11+ 내장 RSASSA-PSS 사용. JDK 8 환경은 BouncyCastle 프로바이더 등록이 필요하다.
*
* JwtHelper를 우회하는 PssJwtAccessTokenConverter와 함께 사용한다.
* (spring-security-jwt 1.1.1은 JwtAlgorithms에 PS256을 등록하지 않아 JwtHelper 직접 사용 불가)
*/
public class RsaPssSigner implements Signer {
private static final PSSParameterSpec PSS_SPEC =
new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
private final RSAPrivateKey privateKey;
public RsaPssSigner(RSAPrivateKey privateKey) {
this.privateKey = privateKey;
}
@Override
public byte[] sign(byte[] bytes) {
try {
Signature sig = Signature.getInstance("RSASSA-PSS");
sig.setParameter(PSS_SPEC);
sig.initSign(privateKey);
sig.update(bytes);
return sig.sign();
} catch (Exception e) {
throw new RuntimeException("RSA-PSS 서명 실패", e);
}
}
@Override
public String algorithm() {
return "PS256";
}
}
@@ -0,0 +1,47 @@
package com.eactive.eai.authserver.jwt;
import java.security.Signature;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
/**
* PS256 (RSA-PSS + SHA-256) 서명 검증 구현체.
* RFC 7518 §3.5 파라미터: Hash=SHA-256, MGF=MGF1/SHA-256, saltLen=32, trailer=0xBC
*/
public class RsaPssVerifier implements SignatureVerifier {
private static final PSSParameterSpec PSS_SPEC =
new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
private final RSAPublicKey publicKey;
public RsaPssVerifier(RSAPublicKey publicKey) {
this.publicKey = publicKey;
}
@Override
public void verify(byte[] content, byte[] signature) {
try {
Signature sig = Signature.getInstance("RSASSA-PSS");
sig.setParameter(PSS_SPEC);
sig.initVerify(publicKey);
sig.update(content);
if (!sig.verify(signature)) {
throw new InvalidSignatureException("RSA-PSS 서명 검증 실패");
}
} catch (InvalidSignatureException e) {
throw e;
} catch (Exception e) {
throw new InvalidSignatureException("RSA-PSS 서명 검증 실패: " + e.getMessage());
}
}
@Override
public String algorithm() {
return "PS256";
}
}
@@ -13,15 +13,21 @@ import com.eactive.eai.common.lifecycle.Lifecycle;
import com.eactive.eai.common.lifecycle.LifecycleException; import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.lifecycle.LifecycleListener; import com.eactive.eai.common.lifecycle.LifecycleListener;
import com.eactive.eai.common.lifecycle.LifecycleSupport; 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.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.mchange.v1.cachedstore.CachedStore.Manager;
import com.openbanking.eai.common.token.AccessTokenVO; import com.openbanking.eai.common.token.AccessTokenVO;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.*; import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Supplier;
/** /**
* 1. 기능 : Access 토큰 정보를 DB로 부터 로딩하고 만료시 재발급 정보를 메모리에 관리하는 Manager 클래스 * 1. 기능 : Access 토큰 정보를 DB로 부터 로딩하고 만료시 재발급 정보를 메모리에 관리하는 Manager 클래스
@@ -57,11 +63,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
*/ */
private boolean started; private boolean started;
/**
* 토큰 정보를 저장하는 collection
*/
private HashMap<String, AccessTokenVO> tokens;
/** /**
* 인증 정보를 저장하는 Map * 인증 정보를 저장하는 Map
*/ */
@@ -78,7 +79,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
private static final int TOKEN_SCHEDULER_THREAD_POOL_SIZE = 30; private static final int TOKEN_SCHEDULER_THREAD_POOL_SIZE = 30;
public AccessTokenManagerByDB() { public AccessTokenManagerByDB() {
tokens = new HashMap<>();
scheduledTasks = new ConcurrentHashMap<>(); scheduledTasks = new ConcurrentHashMap<>();
outboundOAuthCredentialVos = new HashMap<>(); outboundOAuthCredentialVos = new HashMap<>();
runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<>()); runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
@@ -117,7 +117,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
lifecycle.fireLifecycleEvent(STARTING_EVENT, this); lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
try { try {
tokens.clear();
init(); init();
} catch(Exception e) { } catch(Exception e) {
throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201")); throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201"));
@@ -168,7 +167,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
logger.error("Scheduler shutdown interrupted", e); logger.error("Scheduler shutdown interrupted", e);
} }
this.tokens = null;
started = false; started = false;
if (logger.isWarn()){ if (logger.isWarn()){
@@ -240,27 +238,34 @@ public class AccessTokenManagerByDB implements Lifecycle {
try { try {
logger.debug("Executing token issuance for adapter group: {}", adapterGroupName); logger.debug("Executing token issuance for adapter group: {}", adapterGroupName);
// 현재 토큰 가져오기 SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function<AccessTokenVO, AccessTokenVO>() {
AccessTokenVO accessToken = tokens.get(adapterGroupName);
@Override
public AccessTokenVO apply(AccessTokenVO accessToken) {
// 다음 스케줄 시간 계산
long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000); long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000);
// 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급 // // 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급
if (accessToken == null) { if (accessToken == null) {
issueToken(credential, false); return issueToken(credential);
} else if(accessToken.getExpiration() != null) { } else if(accessToken.getExpiration() != null) {
// 토큰이 있고 만료시간이 설정 된 경우 // 토큰이 있고 만료시간이 설정 된 경우
if(accessToken.getExpiration().before(new Date(intervalTime))){ if(accessToken.getExpiration().before(new Date(intervalTime))){
// 다음 스케줄 전에 토큰이 만료되는 경우 재발급 // 다음 스케줄 전에 토큰이 만료되는 경우 재발급
issueToken(credential, true); return issueToken(credential);
} else { } else {
// 토큰이 아직 유효한 경우 // 토큰이 아직 유효한 경우
logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration()); logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
return accessToken;
} }
} else { } else {
//토큰이 있지만 만료 시간이 없는 경우 //토큰이 있지만 만료 시간이 없는 경우
logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName); logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName);
return accessToken;
} }
}
});
logger.debug("Token issuance completed for adapter group: {}", adapterGroupName); logger.debug("Token issuance completed for adapter group: {}", adapterGroupName);
} catch (Exception e) { } catch (Exception e) {
@@ -282,19 +287,24 @@ public class AccessTokenManagerByDB implements Lifecycle {
} }
} }
private void issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo, boolean isTokenExpiredWithinInterval) throws Exception { private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo) {
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName(); String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
boolean isExpired = true; 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 (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) {
logger.info("Issuing new token for adapter group: {}", adapterGroupName); logger.info("Issuing new token for adapter group: {}", adapterGroupName);
tokens.remove(adapterGroupName); // tokens.remove(adapterGroupName);
AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName); AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
@@ -306,16 +316,23 @@ public class AccessTokenManagerByDB implements Lifecycle {
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type); HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
if(service != null) { if(service != null) {
logger.debug("Executing token service of type: {} for adapter group: {}", type, adapterGroupName); logger.debug("Executing token service of type: {} for adapter group: {}", type, adapterGroupName);
AccessTokenVO accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo); accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
this.tokens.put(adapterGroupName, accessToken);
logger.info("New token issued successfully for adapter group: {}", adapterGroupName); logger.info("New token issued successfully for adapter group: {}", adapterGroupName);
return accessToken;
} else { } else {
logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName); logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
} }
} else { } else {
logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName); 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) { public void stopToken(String adapterGroupName) {
@@ -324,7 +341,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
task.cancel(true); task.cancel(true);
logger.warn("Token task stopped for adapter group: {}", adapterGroupName); logger.warn("Token task stopped for adapter group: {}", adapterGroupName);
} }
tokens.remove(adapterGroupName); SessionManager.getInstance().removeOutboundAccessToken(adapterGroupName);
logger.debug("Token removed for adapter group: {}", adapterGroupName); logger.debug("Token removed for adapter group: {}", adapterGroupName);
} }
@@ -385,9 +402,22 @@ public class AccessTokenManagerByDB implements Lifecycle {
AccessTokenVO accessToken = null; AccessTokenVO accessToken = null;
try { try {
accessToken = tokens.get(adapterGroupName); accessToken = SessionManager.getInstance().getOutboundAccessToken(adapterGroupName,
} catch(Exception e) { new Function<AccessTokenVO, AccessTokenVO>() {
if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
@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; return accessToken;
@@ -405,7 +435,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
String accessToken = null; String accessToken = null;
try { try {
accessToken = tokens.get(adapterGroupName).getAccessToken(); accessToken = getAccessTokenVO(adapterGroupName).getAccessToken();
} catch(Exception e) { } catch(Exception e) {
if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e); if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
} }
@@ -435,7 +465,8 @@ public class AccessTokenManagerByDB implements Lifecycle {
* 3. 주의사항 * 3. 주의사항
* *
**/ **/
public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties, String oldToken) throws Exception { public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties,
String oldToken) throws Exception {
AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName); AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName);
@@ -443,17 +474,18 @@ public class AccessTokenManagerByDB implements Lifecycle {
if (accessToken == null || accessToken.isExpired() if (accessToken == null || accessToken.isExpired()
|| StringUtils.equals(accessToken.getAccessToken(), oldToken)) { || StringUtils.equals(accessToken.getAccessToken(), oldToken)) {
if(isOAuthCredentialRegistered(adapterGroupName) == false){ if (isOAuthCredentialRegistered(adapterGroupName) == false) {
throw new Exception("There is no OAuthCredentialRegistered information, or whether to use it is 'N'."); throw new Exception(
"There is no OAuthCredentialRegistered information, or whether to use it is 'N'.");
} }
String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE"); String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type); HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName); OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
if(service != null) { if (service != null) {
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo); accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
outboundOAuthCredentialVo);
} }
tokens.put(adapterGroupName, accessToken);
} }
return accessToken; return accessToken;
@@ -336,7 +336,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
switch (event.getEventType()) { switch (event.getEventType()) {
case ERROR : case ERROR :
TypedCircuitBreakerManager.logger TypedCircuitBreakerManager.logger
.error("CirbuitBreaker ERROR event occurred. CircuitBreakerEvent={}" + event); .error("CircuitBreaker ERROR event occurred. CircuitBreakerEvent={}" + event);
break; break;
case STATE_TRANSITION : case STATE_TRANSITION :
if (event instanceof CircuitBreakerOnStateTransitionEvent) { if (event instanceof CircuitBreakerOnStateTransitionEvent) {
@@ -345,7 +345,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
CircuitBreaker.State toState = stateTransition.getToState(); CircuitBreaker.State toState = stateTransition.getToState();
CircuitBreaker.State fromState = stateTransition.getFromState(); CircuitBreaker.State fromState = stateTransition.getFromState();
TypedCircuitBreakerManager.logger.error( 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}); new Object[]{event.getCircuitBreakerName(), fromState, toState, event, this.vo});
// if (!toState.equals(State.OPEN) && !toState.equals(State.HALF_OPEN) // if (!toState.equals(State.OPEN) && !toState.equals(State.HALF_OPEN)
// && !toState.equals(State.CLOSED)) { // && !toState.equals(State.CLOSED)) {
@@ -367,7 +367,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
} else { } else {
TypedCircuitBreakerManager.logger.error( TypedCircuitBreakerManager.logger.error(
"CirbuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}", "CircuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
new Object[]{event, this.vo}); new Object[]{event, this.vo});
} }
break; break;
@@ -7,6 +7,10 @@ import org.json.simple.JSONValue;
import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager; import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.adapter.handler.AdapterErrorMessageHandler;
import com.eactive.eai.adapter.handler.AdapterErrorMessageHandlerFactory;
import com.eactive.eai.common.EAIKeys; import com.eactive.eai.common.EAIKeys;
import com.eactive.eai.common.logger.EAILogSender; import com.eactive.eai.common.logger.EAILogSender;
import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.message.EAIMessage;
@@ -749,6 +753,53 @@ public class ExceptionHandler {
String serviceType, String psvItfTp, String charset) { String serviceType, String psvItfTp, String charset) {
String error = null; String error = null;
EAIMessage resultEAIMessage = null; EAIMessage resultEAIMessage = null;
// AdapterErrorMessageHandler(TemplateAdapterErrorMsgHandler) 설정이 있으면 우선 호출
try {
AdapterGroupVO inboundAdapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(sngSysItfTp);
if (inboundAdapterGroupVO != null) {
AdapterVO inboundAdapterVO = inboundAdapterGroupVO.nextAdapterVO();
if (inboundAdapterVO != null) {
String errorHandlerClass = AdapterPropManager.getInstance().getProperty(
inboundAdapterVO.getPropGroupName(), "ERR_MSG_HANDLER");
if (StringUtils.isNotBlank(errorHandlerClass)) {
if (logger.isInfo())
logger.info("ExceptionHandler] errorSyncSend ERR_MSG_HANDLER=" + errorHandlerClass);
AdapterErrorMessageHandler handler = AdapterErrorMessageHandlerFactory.createHandler(errorHandlerClass);
if (handler != null) {
// 표준전문에 에러코드/에러메시지 세팅
try {
StandardMessageManager standardManager = StandardMessageManager.getInstance();
standardManager.getMessageCoordinator().coordinateSetStandardMessageError(
eaiMessage.getStandardMessage(), eaiMessage.getMapper(), errCode, errMsg);
} catch (Exception e) {
if (logger.isWarn())
logger.warn("ExceptionHandler] errorSyncSend coordinateSetStandardMessageError 처리 실패: " + e.getMessage());
}
Object responseObj = handler.generateNonStandardInternalErrorResponseMessage(
sngSysItfTp, inboundAdapterVO.getName(), null, tmpMsg, eaiMessage);
if (responseObj != null) {
resultEAIMessage = eaiMessage;
try {
resultEAIMessage.getStandardMessage().setBizData(responseObj, charset);
} catch (Exception e) {
logger.error("setBizData error", e);
}
resultEAIMessage.getMapper().setResponseType(
resultEAIMessage.getStandardMessage(), STDMessageKeys.RESPONSE_TYPE_CODE_E);
resultEAIMessage.setRspErrCd(EAIMessageKeys.EAI_SUCCESS_CODE, false);
return resultEAIMessage;
}
}
}
}
}
} catch (Exception e) {
if (logger.isWarn())
logger.warn("ExceptionHandler] errorSyncSend AdapterErrorMessageHandler 처리 실패: " + e.getMessage());
}
try { try {
if (transClassify) { if (transClassify) {
if (logger.isInfo()) { if (logger.isInfo()) {
@@ -24,13 +24,17 @@ import com.eactive.eai.common.util.Logger;
* SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11) * SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11)
* *
* PropManager 그룹 "HSM" 에서 읽는 키: * PropManager 그룹 "HSM" 에서 읽는 키:
* PKCS11_CONFIG - pkcs11.cfg 파일 내용 (name/library/slot 설정을 DB에 직접 저장) * PKCS11_CONFIG - pkcs11.cfg 파일 내용
* PIN - HSM 슬롯 PIN * PIN - HSM 슬롯 PIN
* *
* PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력): * PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력):
* name = ProtectServer * name = ProtectServer
* library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so * library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
* slot = 0 * slot = 0
*
* name = SoftHSM
* library = C:/SoftHSM2/lib/softhsm2-x64.dll
* slotListIndex = 0
*/ */
@Component @Component
public class HsmManager implements Lifecycle { public class HsmManager implements Lifecycle {
@@ -1,22 +0,0 @@
package com.eactive.eai.common.inflow.dual;
import com.eactive.eai.common.inflow.InflowTargetVO;
/**
* DualBucket에 클라이언트 유량제어 메서드를 추가한 인터페이스.
* {@link ClientDualInflowControlManager}가 구현한다.
*/
public interface ClientDualBucket extends DualBucket {
/**
* 클라이언트 유량제어 체크 — 차단 원인 포함.
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
*/
String isClientPassDetail(String clientId);
/**
* 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용.
* @return 등록된 설정이 없으면 null
*/
InflowTargetVO getClientInflowThreshold(String clientId);
}
@@ -1,112 +0,0 @@
package com.eactive.eai.common.inflow.dual;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
import com.eactive.eai.common.inflow.Bucket;
import com.eactive.eai.common.inflow.InflowTargetVO;
import com.eactive.eai.common.inflow.InflowType;
import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;
/**
* 어댑터/인터페이스 이중 버킷(DualInflowControlManager)에 클라이언트 유량제어를 추가한 관리자.
*
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
*/
@Component
public class ClientDualInflowControlManager extends DualInflowControlManager implements ClientDualBucket {
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
public static synchronized Bucket getBucket() {
return ApplicationContextProvider.getContext().getBean(ClientDualInflowControlManager.class);
}
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> clientMetricsMap = new ConcurrentHashMap<>();
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
@Override
public void start() throws LifecycleException {
super.start();
try {
initClients();
} catch (Exception e) {
throw new LifecycleException("ClientDualInflowControlManager init failed: " + e.getMessage());
}
}
// -------------------------------------------------------------------------
// 초기화 / 리로드
// -------------------------------------------------------------------------
private void initClients() throws Exception {
this.clientBucketMap = loadBucketMap(InflowType.CLIENT);
if (logger.isInfo()) logger.info("ClientDualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets");
}
public synchronized void reloadClient() throws Exception {
initClients();
clientMetricsMap.clear();
}
public synchronized void reloadClient(String clientId) throws Exception {
reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId);
TargetMetrics m = clientMetricsMap.get(clientId);
if (m != null) m.reset();
}
public synchronized void removeClient(String clientId) {
clientBucketMap.remove(clientId);
clientMetricsMap.remove(clientId);
}
// -------------------------------------------------------------------------
// 클라이언트 메트릭 접근자
// -------------------------------------------------------------------------
public TargetMetrics getClientMetrics(String clientId) { return clientMetricsMap.get(clientId); }
public Map<String, TargetMetrics> getAllClientMetrics() { return Collections.unmodifiableMap(clientMetricsMap); }
public void resetClientMetrics(String clientId) { TargetMetrics m = clientMetricsMap.get(clientId); if (m != null) m.reset(); }
public void resetAllClientMetrics() { clientMetricsMap.values().forEach(TargetMetrics::reset); }
// -------------------------------------------------------------------------
// ClientDualBucket — 클라이언트 유량제어
// -------------------------------------------------------------------------
@Override
public String isClientPassDetail(String clientId) {
DualCustomBucket b = clientBucketMap.get(clientId);
if (b == null) return DualCustomBucket.RESULT_PASS;
String result = b.tryConsume();
recordMetrics(clientMetricsMap, clientId, result);
return result;
}
@Override
public InflowTargetVO getClientInflowThreshold(String clientId) {
DualCustomBucket b = clientBucketMap.get(clientId);
return (b == null) ? null : b.getInflowTargetVo();
}
// -------------------------------------------------------------------------
// 모니터링용 접근자
// -------------------------------------------------------------------------
public DualCustomBucket getClientBucket(String clientId) {
return clientBucketMap.get(clientId);
}
public Map<String, DualCustomBucket> getClientBucketMap() {
return clientBucketMap;
}
}
@@ -254,6 +254,8 @@ public class EAIFileLogger
**/ **/
public void setLog(EAIMessage message, Properties prop) throws Exception{ public void setLog(EAIMessage message, Properties prop) throws Exception{
if(!tranLogger.isError())
return;
// 업무데이터 size 체크하여 업무데이터1,2에 insert하고 // 업무데이터 size 체크하여 업무데이터1,2에 insert하고
// addBwkDataSub(message) 호출 여부 결정 // addBwkDataSub(message) 호출 여부 결정
@@ -517,27 +519,28 @@ public class EAIFileLogger
//전장표 데이터부 JSON Array로 저장 //전장표 데이터부 JSON Array로 저장
//암호화 //암호화
Charset hexaCharset = Charset.forName("euc-kr"); biz = EncryptionManager.getInstance().encryptDBData(bizMsgStr);
if("Y".equalsIgnoreCase(EncryptionManager.getInstance().getEncryptYN()) ){ // Charset hexaCharset = Charset.forName("euc-kr");
try{ // if("Y".equalsIgnoreCase(EncryptionManager.getInstance().getEncryptYN()) ){
biz = HexaConverter.bytesToHexa(bizMsgStr.getBytes(hexaCharset)); // try{
}catch(Exception e){ // biz = HexaConverter.bytesToHexa(bizMsgStr.getBytes(hexaCharset));
logger.error("EAIFileLogger] bizMsgStr encryption Error - " + e.getMessage()); // }catch(Exception e){
} // logger.error("EAIFileLogger] bizMsgStr encryption Error - " + e.getMessage());
try{ // }
header = HexaConverter.bytesToHexa(header.getBytes(hexaCharset)); // try{
}catch(Exception e){ // header = HexaConverter.bytesToHexa(header.getBytes(hexaCharset));
logger.error("EAIFileLogger] header encryption Error - " + e.getMessage()); // }catch(Exception e){
} // logger.error("EAIFileLogger] header encryption Error - " + e.getMessage());
try{ // }
mdclData = HexaConverter.bytesToHexa(mdclData.getBytes(hexaCharset)); // try{
}catch(Exception e){ // mdclData = HexaConverter.bytesToHexa(mdclData.getBytes(hexaCharset));
logger.error("EAIFileLogger] mdclData encryption Error - " + e.getMessage()); // }catch(Exception e){
} // logger.error("EAIFileLogger] mdclData encryption Error - " + e.getMessage());
// }
}else { //
biz = bizMsgStr; // }else {
} // biz = bizMsgStr;
// }
//임시코드 시작 //임시코드 시작
String logType = PropManager.getInstance().getProperty("BIZ_LOG_TYPE"); String logType = PropManager.getInstance().getProperty("BIZ_LOG_TYPE");
@@ -3,8 +3,6 @@ package com.eactive.eai.common.logger;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Properties; import java.util.Properties;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.vo.ClientVO;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@@ -14,13 +12,10 @@ import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager; import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.agent.encryption.EncryptionManager; import com.eactive.eai.agent.encryption.EncryptionManager;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.vo.ClientVO;
import com.eactive.eai.common.dao.DAOException; import com.eactive.eai.common.dao.DAOException;
import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.logger.EAILogLogger;
import com.eactive.eai.common.logger.StatAdapterLogLogger;
import com.eactive.eai.common.logger.StatLogLogger;
import com.eactive.eai.common.logger.StatTranLogLogger;
import com.eactive.eai.common.logger.SubMessageLogLogger;
import com.eactive.eai.common.logger.mapper.StatAdapterLogMapper; import com.eactive.eai.common.logger.mapper.StatAdapterLogMapper;
import com.eactive.eai.common.logger.mapper.StatLogMapper; import com.eactive.eai.common.logger.mapper.StatLogMapper;
import com.eactive.eai.common.logger.mapper.StatTranLogMapper; import com.eactive.eai.common.logger.mapper.StatTranLogMapper;
@@ -30,7 +25,6 @@ import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.message.ServiceMessage; import com.eactive.eai.common.message.ServiceMessage;
import com.eactive.eai.common.monitor.StatMonitorLogVO; import com.eactive.eai.common.monitor.StatMonitorLogVO;
import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager; import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.server.EAIServerVO; import com.eactive.eai.common.server.EAIServerVO;
import com.eactive.eai.common.server.loader.EAIServerLoader; import com.eactive.eai.common.server.loader.EAIServerLoader;
@@ -55,11 +49,10 @@ import com.eactive.eai.transformer.layout.Item;
import com.eactive.eai.transformer.layout.Layout; import com.eactive.eai.transformer.layout.Layout;
import com.eactive.eai.transformer.transform.Transform; import com.eactive.eai.transformer.transform.Transform;
import com.eactive.eai.transformer.transform.TransformManager; import com.eactive.eai.transformer.transform.TransformManager;
import com.eactive.eai.util.HexaConverter;
import com.ext.eai.common.stdmessage.STDMessageKeys;
// safeDB // safeDB
import com.eactive.ext.kjb.safedb.KjbSafedbWrapper; import com.eactive.ext.kjb.safedb.KjbSafedbWrapper;
import com.eactive.ext.kjb.safedb.Utils; import com.eactive.ext.kjb.safedb.Utils;
import com.ext.eai.common.stdmessage.STDMessageKeys;
@Service @Service
@@ -478,20 +471,12 @@ public class EAILogDAO {
} }
*/ */
// jwhong safeDB 암호화
if ("Y".equalsIgnoreCase(EncryptionManager.getInstance().getEncryptYN())) {
try { try {
biz = EncryptSafeDb(bizMsgStr); biz = EncryptionManager.getInstance().encryptDBData(bizMsgStr);
} catch (Exception e) { } catch (Exception e) {
logger.error("EAIFileLogger] bizMsgStr encryption Error - " + e.getMessage()); logger.error("EAIFileLogger] bizMsgStr encryption Error - " + e.getMessage());
biz = bizMsgStr; biz = bizMsgStr;
} }
} else {
biz = bizMsgStr;
}
// jwhong safeDB end
// 임시코드 시작 // 임시코드 시작
String logType = PropManager.getInstance().getProperty("BIZ_LOG_TYPE"); String logType = PropManager.getInstance().getProperty("BIZ_LOG_TYPE");
@@ -862,88 +847,6 @@ public class EAILogDAO {
} }
private String EncryptSafeDb(String attribute) {
String originalAttribute = attribute;
if (StringUtils.isNotEmpty(attribute)) {
KjbSafedbWrapper wrapper = KjbSafedbWrapper.getInstance();
attribute = wrapper.encryptNotRnnoString(attribute);
if (logger.isInfo()) {
// originalAttribute 홀수 글자 마스킹
StringBuilder sb = new StringBuilder();
for (int i = 0; i < originalAttribute.length(); i++) {
if (i % 2 == 0) {
sb.append(originalAttribute.charAt(i));
} else {
sb.append("*");
}
}
originalAttribute = sb.toString();
// System.out.println("DB 암호화 #2 : {"+ originalAttribute +"} -> {"+attribute+"}");
//logger.debug("DB 암호화 #2 : {} -> {}", originalAttribute, attribute);
}
}
return attribute;
}
public String DecryptSafeDb(String dbData) {
String dbDataOriginal = dbData;
if (StringUtils.isNotEmpty(dbData)) {
if (this.isEncrypted(dbData)) {
KjbSafedbWrapper safeDBWrapper = KjbSafedbWrapper.getInstance();
try {
dbData = safeDBWrapper.decryptNotRnno(dbData);
} catch (Exception e) {
// 복호화 실패시 원본 반환
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
logger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
return dbData;
}
} else {
logger.debug("DB 복호화 경고: Base64 형식이 아님. 복호화하지 않고 원본 반환. dbData={}", dbData);
}
}
//logger.debug("DB 복호화 #2 : {} -> {}", dbDataOriginal, dbData);
return dbData;
}
/**
* 암호화 여부 확인(base64 인코딩 여부로 판단, 또는 0-9, - 로 구성된 경우 암호화 되지 않은 걸로 판단(연락처))
* @param data
* @return
*/
private boolean isEncrypted(String data) {
if (StringUtils.isEmpty(data)) {
return false;
}
// 숫자, - 로만 구성된 경우 암호화 되지 않은 걸로 판단 (ex: 연락처)
if (data.matches("^[0-9-]+$")) {
return false;
}
data = data.trim();
// Base64 형식 체크
if (!Utils.isBase64(data)) {
return false;
}
// Base64 길이 체크 (4의 배수)
if (data.length() % 4 != 0) {
return false;
}
return true;
}
/* /*
private String EncryptSafeDb(String msg) { private String EncryptSafeDb(String msg) {
PropManager propManager = PropManager.getInstance(); PropManager propManager = PropManager.getInstance();
@@ -64,7 +64,8 @@ public class IFRouter
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(service.getPsvSysItfTp()); AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(service.getPsvSysItfTp());
if( EAIMessageKeys.TRANTYPE_TAS.equals(eaiMessage.getTranType()) if( EAIMessageKeys.TRANTYPE_TAS.equals(eaiMessage.getTranType())
// RESTAdapter는 교체하지 않는다 // RESTAdapter는 교체하지 않는다
&& !StringUtils.equals(adptGrpVO.getType(), "RST")) { && !StringUtils.equals(adptGrpVO.getType(), "RST")
&& !StringUtils.equals(adptGrpVO.getType(), "HTT")) {
// isLocal = true; // isLocal = true;
// if (logger.isInfo()) logger.info(guidLogPrefix + " TRANTYPE TAS [" + eaiMessage.getTranType() + "]"); // if (logger.isInfo()) logger.info(guidLogPrefix + " TRANTYPE TAS [" + eaiMessage.getTranType() + "]");
PropManager manager = PropManager.getInstance(); PropManager manager = PropManager.getInstance();
@@ -144,8 +144,12 @@ public class CryptoModuleManager implements Lifecycle {
dynamicKeyCache.put(cacheKey, new CachedDerivedKey(derivedKey, ttl)); dynamicKeyCache.put(cacheKey, new CachedDerivedKey(derivedKey, ttl));
} }
} }
byte[] iv = null;
byte[] iv = vo.getIvHex() != null ? DatatypeConverter.parseHexBinary(vo.getIvHex()) : null; if(derivedKey.getIv() != null) {
iv = derivedKey.getIv();
} else if (vo.getIvHex() != null){
iv = DatatypeConverter.parseHexBinary(vo.getIvHex());
}
return buildExtension(vo, iv, derivedKey.getEncKey(), derivedKey.getDecKey()); return buildExtension(vo, iv, derivedKey.getEncKey(), derivedKey.getDecKey());
} }
@@ -4,10 +4,19 @@ public class DerivedKey {
private final byte[] encKey; private final byte[] encKey;
private final byte[] decKey; private final byte[] decKey;
private final byte[] iv;
public DerivedKey(byte[] encKey, byte[] decKey) { public DerivedKey(byte[] encKey, byte[] decKey) {
this.encKey = encKey; this.encKey = encKey;
this.decKey = decKey != null ? decKey : encKey; this.decKey = decKey != null ? decKey : encKey;
this.iv = null;
}
public DerivedKey(byte[] encKey, byte[] decKey, byte[] iv) {
this.encKey = encKey;
this.decKey = decKey != null ? decKey : encKey;
this.iv = iv;
} }
public byte[] getEncKey() { public byte[] getEncKey() {
@@ -17,4 +26,8 @@ public class DerivedKey {
public byte[] getDecKey() { public byte[] getDecKey() {
return decKey; return decKey;
} }
public byte[] getIv() {
return iv;
}
} }
@@ -4,6 +4,11 @@ import java.util.Map;
public interface KeyDerivationStrategy { public interface KeyDerivationStrategy {
public static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
public static final String PARAM_HSM_IV_ALIAS = "hsmIvAlias";
public static final String PARAM_CONTEXT_KEY = "contextKey";
/** /**
* 키를 도출한다. * 키를 도출한다.
* *
@@ -0,0 +1,43 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.util.Map;
import javax.crypto.SecretKey;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.hsm.HsmException;
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HSM 마스터키를 이용하는 전략 클래스들의 기본 클래스
*/
public abstract class BaseHsmKeyDerivationStrategy implements KeyDerivationStrategy {
protected byte[] getHsmKey(Map<String, String> params) throws HsmException {
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
return getHsmKey(hsmKeyAlias);
}
protected byte[] getHsmKey(String hsmKeyAlias) throws HsmException {
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
return masterKey.getEncoded();
}
protected byte[] getHsmIv(Map<String, String> params) throws HsmException {
String hsmKeyAlias = required(params, PARAM_HSM_IV_ALIAS);
return getHsmKey(hsmKeyAlias);
}
private String required(Map<String, String> params, String key) {
String value = params.get(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
}
return value;
}
private HsmCryptoService hsmCryptoService() {
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
}
}
@@ -0,0 +1,62 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import javax.crypto.SecretKey;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HSM 마스터키 + 런타임 컨텍스트값을 연결(concatenate)한 뒤 SHA-256 해싱으로 키를 도출하는 전략.
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
*
* key_deriv_params (JSON) 예시:
* {
* "hsmKeyAlias" : "MASTER_KEY_AES",
* "contextKey" : "X-Api-Group-Seq" -- runtimeContext에서 꺼낼 키 이름
* }
*/
public class HsmContextSha256KeyDerivationStrategy implements KeyDerivationStrategy {
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
String contextKey = required(params, PARAM_CONTEXT_KEY);
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
byte[] masterKeyBytes = masterKey.getEncoded();
byte[] contextBytes = runtimeContext.getOrDefault(contextKey, "")
.getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[masterKeyBytes.length + contextBytes.length];
System.arraycopy(masterKeyBytes, 0, combined, 0, masterKeyBytes.length);
System.arraycopy(contextBytes, 0, combined, masterKeyBytes.length, contextBytes.length);
byte[] derived = MessageDigest.getInstance("SHA-256").digest(combined);
return new DerivedKey(derived, derived);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
String contextValue = runtimeContext.getOrDefault(contextKey, "");
return cryptoName + ":" + contextValue;
}
private String required(Map<String, String> params, String key) {
String value = params.get(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
}
return value;
}
private HsmCryptoService hsmCryptoService() {
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
}
}
@@ -26,8 +26,6 @@ public class HsmContextXorKeyDerivationStrategy implements KeyDerivationStrategy
/** DB key_deriv_strategy 컬럼에 사용하는 FQCN 참조용 상수. */ /** DB key_deriv_strategy 컬럼에 사용하는 FQCN 참조용 상수. */
public static final String STRATEGY_CLASS = HsmContextXorKeyDerivationStrategy.class.getName(); public static final String STRATEGY_CLASS = HsmContextXorKeyDerivationStrategy.class.getName();
private static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
private static final String PARAM_CONTEXT_KEY = "contextKey";
private static final String PARAM_OFFSET = "offset"; private static final String PARAM_OFFSET = "offset";
private static final String PARAM_LENGTH = "length"; private static final String PARAM_LENGTH = "length";
@@ -0,0 +1,25 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.util.Map;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
/**
* HSM 마스터키를 사용하는 전략
*
* key_deriv_params (JSON) 예시: { "hsmKeyAlias" : "MASTER_KEY_AES", "hsmIvAlias" : "MASTER_IV_AES"}
*/
public class HsmKeyAndIvDerivationStrategy extends BaseHsmKeyDerivationStrategy {
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
byte[] masterKeyBytes = super.getHsmKey(params);
byte[] iv = super.getHsmIv(params);
return new DerivedKey(masterKeyBytes, masterKeyBytes, iv);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
return cryptoName;
}
}
@@ -0,0 +1,24 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.util.Map;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
/**
* HSM 마스터키를 사용하는 전략
*
* key_deriv_params (JSON) 예시: { "hsmKeyAlias" : "MASTER_KEY_AES"}
*/
public class HsmKeyDerivationStrategy extends BaseHsmKeyDerivationStrategy {
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
byte[] masterKeyBytes = super.getHsmKey(params);
return new DerivedKey(masterKeyBytes, masterKeyBytes);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
return cryptoName;
}
}
@@ -0,0 +1,30 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.security.MessageDigest;
import java.util.Map;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
/**
* HSM 마스터키를 SHA-256 해싱으로 키를 도출하는 전략.
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
*
* key_deriv_params (JSON) 예시:
* {
* "hsmKeyAlias" : "MASTER_KEY_AES"
* }
*/
public class HsmSha256KeyDerivationStrategy extends BaseHsmKeyDerivationStrategy {
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
byte[] masterKeyBytes = super.getHsmKey(params);
byte[] derived = MessageDigest.getInstance("SHA-256").digest(masterKeyBytes);
return new DerivedKey(derived, derived);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
return cryptoName;
}
}
@@ -1,5 +1,10 @@
package com.eactive.eai.common.session; 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.authserver.service.BearerTokenInfo;
import com.eactive.eai.common.lifecycle.Lifecycle; import com.eactive.eai.common.lifecycle.Lifecycle;
import com.eactive.eai.common.lifecycle.LifecycleException; 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.server.EAIServerVO;
import com.eactive.eai.common.sessioninfo.TerminalInfoVO; import com.eactive.eai.common.sessioninfo.TerminalInfoVO;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.openbanking.eai.common.token.AccessTokenVO;
/** /**
* 1. 기능 : * 1. 기능 :
@@ -60,6 +66,7 @@ public abstract class SessionManager implements Lifecycle {
public static final String WEBSOCKER_CAHCE_NAME = "webSocketTimeout"; public static final String WEBSOCKER_CAHCE_NAME = "webSocketTimeout";
public static final String TERMINAL_CAHCE_NAME = "terminalSession"; public static final String TERMINAL_CAHCE_NAME = "terminalSession";
public static final String CAGATEWAY_TOKEN_CACHE_NAME = "CAGatewayToken"; 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_GROUPNAME = "RMIInfo";
public static final String SESSION_MANAGER_CACHETYPE = "cacheType"; public static final String SESSION_MANAGER_CACHETYPE = "cacheType";
@@ -204,6 +211,13 @@ public abstract class SessionManager implements Lifecycle {
public abstract void removeCAToken(String key); 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 // HTTP Cache
public abstract SessionVO getHttpLogin(String key); public abstract SessionVO getHttpLogin(String key);
@@ -245,4 +259,5 @@ public abstract class SessionManager implements Lifecycle {
public abstract void closeManager(); public abstract void closeManager();
public abstract String getCacheStatus(); public abstract String getCacheStatus();
} }
@@ -2,6 +2,8 @@ package com.eactive.eai.common.session;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.TimeUnit; 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.authserver.service.BearerTokenInfo;
import com.eactive.eai.common.ehcache.CustomRMICacheReplicatorFactory; 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.sessioninfo.TerminalInfoVO;
import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.util.HealthCheckManager; import com.eactive.eai.util.HealthCheckManager;
import com.openbanking.eai.common.token.AccessTokenVO;
import net.sf.ehcache.Cache; import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException; import net.sf.ehcache.CacheException;
@@ -937,4 +940,19 @@ public class SessionManagerForEhcache extends SessionManager {
public void removeCAToken(String key) { public void removeCAToken(String key) {
throw new UnsupportedOperationException(); 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.List;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.TimeUnit; 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.Cache;
import javax.cache.configuration.Factory; 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.Future;
import com.eactive.eai.common.worker.ResponseMap; import com.eactive.eai.common.worker.ResponseMap;
import com.eactive.eai.util.HealthCheckManager; import com.eactive.eai.util.HealthCheckManager;
import com.openbanking.eai.common.token.AccessTokenVO;
import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Level;
@@ -66,6 +71,8 @@ import ch.qos.logback.classic.Level;
//------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------
public class SessionManagerForIgnite extends SessionManager { public class SessionManagerForIgnite extends SessionManager {
private static final String DISTRIBUTED_TOKEN_LOCK = "DISTRIBUTED_TOKEN_LOCK";
// evictMaster Instance - single socket 관련 // evictMaster Instance - single socket 관련
private static IgniteCache<String, String> evictMasterCache = null; private static IgniteCache<String, String> evictMasterCache = null;
// Socket Session cache 정보 // Socket Session cache 정보
@@ -76,8 +83,11 @@ public class SessionManagerForIgnite extends SessionManager {
private static IgniteCache<String, CacheStoreObject> cacheStore = null; private static IgniteCache<String, CacheStoreObject> cacheStore = null;
// CA Gateway Token 관리용 // CA Gateway Token 관리용
private static IgniteCache<String, BearerTokenInfo> cacheCAToken = null; private static IgniteCache<String, BearerTokenInfo> cacheCAToken = null;
// OutboundAccessToekn 관리용
private static IgniteCache<String, AccessTokenVO> cacheOutBoundAccessToken = null;
// webSocket Login cache 정보 // webSocket Login cache 정보
private static IgniteCache<String, SessionVO> cacheLogin = null; // key = loginId private static IgniteCache<String, SessionVO> cacheLogin = null; // key = loginId
private static IgniteCache<String, String> cacheConvertUserId = null; // 단말ID 를 userID로 변경 private static IgniteCache<String, String> cacheConvertUserId = null; // 단말ID 를 userID로 변경
// HTTP Polling 에 대한 timeout 관리용 // HTTP Polling 에 대한 timeout 관리용
private static IgniteCache<String, SessionVO> httpLogin = null; // key == uuid 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, sessionTimeout, bootstrapAsynchronously, USERID_CAHCE_NAME, 20000, false));
caches.add(initCache(config, webSocketTimeout, bootstrapAsynchronously, WEBSOCKER_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, 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)); caches.add(initCache(config, 0, bootstrapAsynchronously, CAGATEWAY_TOKEN_CACHE_NAME, 1000, false));
config.setCacheConfiguration(caches.stream().toArray(CacheConfiguration[]::new)); config.setCacheConfiguration(caches.stream().toArray(CacheConfiguration[]::new));
@@ -421,6 +432,7 @@ public class SessionManagerForIgnite extends SessionManager {
cacheTopic = manager.cache(TOPIC_CAHCE_NAME); cacheTopic = manager.cache(TOPIC_CAHCE_NAME);
cacheStore = manager.cache(STORE_CAHCE_NAME); cacheStore = manager.cache(STORE_CAHCE_NAME);
cacheCAToken = manager.cache(CAGATEWAY_TOKEN_CACHE_NAME); cacheCAToken = manager.cache(CAGATEWAY_TOKEN_CACHE_NAME);
cacheOutBoundAccessToken = manager.cache(OUTBOUND_ACCESS_TOKEN_CACHE_NAME);
cacheLogin = manager.cache(LOGIN_CAHCE_NAME); cacheLogin = manager.cache(LOGIN_CAHCE_NAME);
httpLogin = manager.cache(HTTP_CAHCE_NAME); httpLogin = manager.cache(HTTP_CAHCE_NAME);
@@ -429,8 +441,9 @@ public class SessionManagerForIgnite extends SessionManager {
cacheTerminal = manager.cache(TERMINAL_CAHCE_NAME); cacheTerminal = manager.cache(TERMINAL_CAHCE_NAME);
} }
private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs, String bootstrapAsynchronously, private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs,
String name, int maxsize, boolean useCacheWriteModeAsync) { String bootstrapAsynchronously, String name, int maxsize, boolean useCacheWriteModeAsync,
boolean transactionMode) {
if (logger.isDebug()) { if (logger.isDebug()) {
logger.debug(logPrefix + " create " + name + " cache expiryDurationSecs=" + expiryDurationSecs); logger.debug(logPrefix + " create " + name + " cache expiryDurationSecs=" + expiryDurationSecs);
} }
@@ -441,12 +454,15 @@ public class SessionManagerForIgnite extends SessionManager {
nearConfiguration.setNearEvictionPolicy(nearEvictionPolicy); nearConfiguration.setNearEvictionPolicy(nearEvictionPolicy);
CacheConfiguration cacheConfig = new CacheConfiguration(name); CacheConfiguration cacheConfig = new CacheConfiguration(name);
// REPLICATED,PARTITIONED(default)
// cacheConfig.setCacheMode(CacheMode.REPLICATED);
cacheConfig.setCacheMode(CacheMode.PARTITIONED); cacheConfig.setCacheMode(CacheMode.PARTITIONED);
// ATOMIC,TRANSACTIONAL // ATOMIC,TRANSACTIONAL
if (transactionMode) {
cacheConfig.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
} else {
cacheConfig.setAtomicityMode(CacheAtomicityMode.ATOMIC); cacheConfig.setAtomicityMode(CacheAtomicityMode.ATOMIC);
}
// Set the number of backups (replicas) for each partition : default(0) // Set the number of backups (replicas) for each partition : default(0)
cacheConfig.setBackups(1); cacheConfig.setBackups(1);
@@ -461,18 +477,21 @@ public class SessionManagerForIgnite extends SessionManager {
cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.PRIMARY_SYNC); cacheConfig.setWriteSynchronizationMode(CacheWriteSynchronizationMode.PRIMARY_SYNC);
} }
if(expiryDurationSecs > 0) { if (expiryDurationSecs > 0) {
cacheConfig cacheConfig.setExpiryPolicyFactory(
.setExpiryPolicyFactory(ModifiedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, expiryDurationSecs))); ModifiedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, expiryDurationSecs)));
} } else {
else { cacheConfig.setExpiryPolicyFactory(ModifiedExpiryPolicy.factoryOf(Duration.ETERNAL));
cacheConfig
.setExpiryPolicyFactory( ModifiedExpiryPolicy.factoryOf(Duration.ETERNAL) );
} }
cacheConfig.setNearConfiguration(nearConfiguration); cacheConfig.setNearConfiguration(nearConfiguration);
return cacheConfig; return cacheConfig;
} }
private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs, String bootstrapAsynchronously,
String name, int maxsize, boolean useCacheWriteModeAsync) {
return initCache(config, expiryDurationSecs, bootstrapAsynchronously, name, maxsize, useCacheWriteModeAsync, false);
}
// WebSocket Login 관련 // WebSocket Login 관련
public SessionVO getLogin(String key) { public SessionVO getLogin(String key) {
try { try {
@@ -869,4 +888,113 @@ public class SessionManagerForIgnite extends SessionManager {
} }
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.setHeaderList(headerVoList);
httpAdapterExtraLogVo.setHttpStatus(httpStatus); httpAdapterExtraLogVo.setHttpStatus(httpStatus);
logger.debug("[{}] Insert HTTP Log. - {}", guid, httpAdapterExtraLogVo);
if(ElinkConfig.isUseAsyncLogging()) { if(ElinkConfig.isUseAsyncLogging()) {
AsyncHttpLoggingPoolManager.getInstance().publish(httpAdapterExtraLogVo); AsyncHttpLoggingPoolManager.getInstance().publish(httpAdapterExtraLogVo);
} else { } else {
@@ -0,0 +1,423 @@
package com.eactive.eai.common.util;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
public class JSONUtils {
private JSONUtils() {
throw new IllegalStateException("Utility class");
}
public static JSONObject parseJson(Object message) {
if (message == null) {
return new JSONObject();
}
if (message instanceof JSONObject)
return (JSONObject) message;
else if (message instanceof String)
try {
return (JSONObject) JSONValue.parseWithException((String) message);
} catch (ParseException e) {
throw new RuntimeException("json parsing exception : " + message, e);
}
else
throw new IllegalArgumentException("not support json type. " + message.getClass());
}
public static JSONArray parseJsonArray(Object message) {
if (message == null) {
return new JSONArray();
}
if (message instanceof JSONArray)
return (JSONArray) message;
else if (message instanceof String)
try {
return (JSONArray) JSONValue.parseWithException((String) message);
} catch (ParseException e) {
throw new RuntimeException("json parsing exception : " + message, e);
}
else
throw new IllegalArgumentException("not support json type. " + message.getClass());
}
@SuppressWarnings("unchecked")
public static Set<Map.Entry<String, Object>> getField(JSONObject json) {
return json.entrySet();
}
public static JSONObject getChildJson(JSONObject json, String childName) {
return (JSONObject)json.get(childName);
}
@SuppressWarnings("unchecked")
public static void setChild(JSONObject json, String childName, Object value) {
json.put(childName, value);
}
@SuppressWarnings("unchecked")
public static void setChildWithPath(JSONObject json, String path, Object value) {
String[] keys = path.split("\\.");
JSONObject current = json;
for(int i = 0; i < keys.length - 1; i++) {
current = (JSONObject) current.get(keys[i]);
if(current == null)
return;
}
current.put(keys[keys.length - 1], value);
}
/**
* json array의 String 값의 ',' 구분자
* @param jsonArray
* @return
*/
public static String toString(JSONArray jsonArray) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for(Object ele : jsonArray ) {
if(ele instanceof String || ele instanceof Number) {
if(first)
first = false;
else
sb.append(",");
sb.append(ele);
}
else
throw new IllegalArgumentException("not support type - " + ele.getClass().getName());
}
return sb.toString();
}
public static String toString2(JSONArray jsonArray) {
String strJsonArray = jsonArray.toJSONString();
strJsonArray = replace(strJsonArray);
return strJsonArray;
}
private static String replace(String strJsonArray) {
strJsonArray = StringUtils.replace(strJsonArray, "\"", "");
strJsonArray = StringUtils.replace(strJsonArray, "[", "");
strJsonArray = StringUtils.replace(strJsonArray, "]", "");
return strJsonArray;
}
public static JSONArray getJSONArray(JSONObject jsonObject, String path) {
if(jsonObject == null || path == null || path.isEmpty())
return null;
String[] keys = path.split("\\.");
JSONObject current = jsonObject;
for(int i = 0; i < keys.length - 1; i++) {
current = (JSONObject) current.get(keys[i]);
if(current == null)
return null;
}
return (JSONArray)current.get(keys[keys.length - 1]);
}
public static String getStringWithPath(JSONObject jsonObject, String path) {
if(jsonObject == null || path == null || path.isEmpty())
return null;
String[] keys = path.split("\\.");
JSONObject current = jsonObject;
for(int i = 0; i < keys.length - 1; i++) {
current = (JSONObject) current.get(keys[i]);
if(current == null)
return null;
}
return (String)current.get(keys[keys.length - 1]);
}
public static String removeStringWithPath(JSONObject jsonObject, String path) {
if(jsonObject == null || path == null || path.isEmpty())
return null;
String[] keys = path.split("\\.");
JSONObject current = jsonObject;
for(int i = 0; i < keys.length - 1; i++) {
current = (JSONObject) current.get(keys[i]);
if(current == null)
return null;
}
return (String)current.remove(keys[keys.length - 1]);
}
public static void removeEmptyWithPath(JSONObject jsonObject, String path) {
if(jsonObject == null || path == null || path.isEmpty())
return;
String[] keys = path.split("\\.");
JSONObject current = jsonObject;
for(int i = 0; i < keys.length - 1; i++) {
current = (JSONObject) current.get(keys[i]);
if(current == null)
return;
}
String value = (String) current.get(keys[keys.length - 1]);
if (StringUtils.isEmpty(value)) {
current.remove(keys[keys.length - 1]);
return;
} else {
return;
}
}
protected static com.eactive.eai.common.util.Logger logger = com.eactive.eai.common.util.Logger
.getLogger(com.eactive.eai.common.util.Logger.LOGGER_DEFAULT);
public static void removeEmptyWithPath2(JSONObject jsonObject, String path) {
if (jsonObject == null || path == null || path.isEmpty())
return;
String[] keys = path.split("\\.");
Object current = jsonObject;
for (int i = 0; i < keys.length; i++) {
String key = keys[i];
if (current instanceof JSONObject) {
JSONObject currentObj = (JSONObject) current;
if (currentObj.get(key) == null) {
if (i == keys.length - 1) {
// 마지막
currentObj.remove(key);
return;
} else {
logger.debug("경로를 찾을 수 없음. {} of {}", path, key);
return;
}
} else {
if (i == keys.length - 1) {
if (currentObj.get(key) instanceof String && StringUtils.isEmpty((String)currentObj.get(key))) {
// 마지막
currentObj.remove(key);
return;
} else {
logger.debug("경로 값이 빈값이 아님. {} of {}'s value [{}]", path, key, currentObj.get(key));
return;
}
} else {
current = currentObj.get(key);
}
}
} else if (current instanceof JSONArray) {
JSONArray currentArray = (JSONArray) current;
for (int j = 0; j < currentArray.size(); j++) {
Object arrayElement = currentArray.get(j);
if (arrayElement instanceof JSONObject) {
List<String> lastPath = new ArrayList<>();
for (int k = i; k < keys.length; k++)
lastPath.add(keys[k]);
removeEmptyWithPath2((JSONObject) arrayElement, String.join(".", lastPath));
}
}
}
}
}
public static void removeZeroWithPath2(JSONObject jsonObject, String path) {
if (jsonObject == null || path == null || path.isEmpty())
return;
String[] keys = path.split("\\.");
Object current = jsonObject;
for (int i = 0; i < keys.length; i++) {
String key = keys[i];
if (current instanceof JSONObject) {
JSONObject currentObj = (JSONObject) current;
if (currentObj.get(key) == null) {
if (i == keys.length - 1) {
logger.debug("경로 값이 null. {} of {}", path, key);
return;
} else {
logger.debug("경로를 찾을 수 없음. {} of {}", path, key);
return;
}
} else {
if (i == keys.length - 1) {
if (isZero(key, currentObj)) {
// 마지막
currentObj.remove(key);
return;
} else {
logger.debug("경로 값이 0이 아님. {} of {}'s value [{}]", path, key, currentObj.get(key));
return;
}
} else {
current = currentObj.get(key);
}
}
} else if (current instanceof JSONArray) {
JSONArray currentArray = (JSONArray) current;
for (int j = 0; j < currentArray.size(); j++) {
Object arrayElement = currentArray.get(j);
if (arrayElement instanceof JSONObject) {
List<String> lastPath = new ArrayList<>();
for (int k = i; k < keys.length; k++)
lastPath.add(keys[k]);
removeZeroWithPath2((JSONObject) arrayElement, String.join(".", lastPath));
}
}
}
}
}
private static final Pattern numberPattern = Pattern.compile("[+-]?\\d*(\\.\\d+)?");
private static boolean isZero(String key, JSONObject currentObj) {
Object value = currentObj.get(key);
if (value instanceof Number && ((Number) value).doubleValue() == 0) {
return true;
} else if (value instanceof String && StringUtils.isNotEmpty((String) value)
&& numberPattern.matcher((String) value).matches()) {
try {
return Double.parseDouble((String) value) == 0.0;
} catch (NumberFormatException e) {
logger.debug("숫자 형식이 아님. [{}]", value);
return false;
}
}
return false;
}
/**
* (문자열)'"aaa", "bbb", "ccc"' -> (json array)["aaa", "bbb", "ccc"]
* @param doublequotedSeperatedStr
* @return
*/
public static JSONArray createJSONArray(String doublequotedSeperatedStr) {
return (JSONArray) JSONValue.parse("[" + doublequotedSeperatedStr + "]");
}
@SuppressWarnings("unchecked")
public static JSONArray createJSONArray(String[] strArray ) {
JSONArray jsonArray = new JSONArray();
for(String str : strArray) {
jsonArray.add(str);
}
return jsonArray;
}
@SuppressWarnings("unchecked")
public static JSONArray createJSONArray(List<String> strList ) {
JSONArray jsonArray = new JSONArray();
for(String str : strList) {
jsonArray.add(str);
}
return jsonArray;
}
@SuppressWarnings("unchecked")
public static JSONObject replaceSpacesWith(JSONObject json, String plus) {
if (json == null || plus == null || plus.isEmpty())
return json;
JSONObject result = new JSONObject();
for (Object key : json.keySet()) {
Object value = json.get(key);
if(value instanceof String) {
result.put(key, ((String)value).replace(" ", plus));
} else if (value instanceof JSONObject) {
result.put(key, replaceSpacesWith((JSONObject)value, plus));
} else if ( value instanceof JSONArray) {
result.put(key, replaceSpacesInArrayWith((JSONArray)value, plus));
} else {
result.put(key, value);
}
}
return result;
}
@SuppressWarnings("unchecked")
public static JSONArray replaceSpacesInArrayWith(JSONArray array, String plus) {
JSONArray result = new JSONArray();
for(int i =0; i < array.size(); i++) {
Object value = array.get(i);
if(value instanceof String) {
result.add(((String)value).replace(" ", plus));
} else if(value instanceof JSONObject) {
result.add(replaceSpacesWith((JSONObject)value, plus));
} else if(value instanceof JSONArray) {
result.add(replaceSpacesInArrayWith((JSONArray)value, plus));
} else {
result.add(value);
}
}
return result;
}
@SuppressWarnings("unchecked")
public static JSONObject stripTrailingZeros(JSONObject json, String exceptionHeaderName) {
if (json == null)
return json;
JSONObject result = new JSONObject();
for (Object key : json.keySet()) {
Object value = json.get(key);
if(value instanceof Number) {
BigDecimal bd = new BigDecimal(value.toString());
BigDecimal stripped = bd.stripTrailingZeros();
if(stripped.scale() <= 0)
result.put(key, stripped.intValue());
else
result.put(key, stripped);
} else if (value instanceof JSONObject) {
result.put(key, stripTrailingZeros((JSONObject)value, exceptionHeaderName));
} else if ( value instanceof JSONArray) {
result.put(key, stripTrailingZerosInArrayWith((JSONArray)value, exceptionHeaderName));
} else {
result.put(key, value);
}
}
return result;
}
@SuppressWarnings("unchecked")
public static JSONArray stripTrailingZerosInArrayWith(JSONArray array, String exceptionHeaderName) {
JSONArray result = new JSONArray();
for(int i =0; i < array.size(); i++) {
Object value = array.get(i);
if(value instanceof Number) {
BigDecimal bd = new BigDecimal(value.toString());
BigDecimal stripped = bd.stripTrailingZeros();
if(stripped.scale() <= 0)
result.add(stripped.intValue());
else
result.add(stripped);
} else if(value instanceof JSONObject) {
result.add(stripTrailingZeros((JSONObject)value, exceptionHeaderName));
} else if(value instanceof JSONArray) {
result.add(stripTrailingZerosInArrayWith((JSONArray)value, exceptionHeaderName));
} else {
result.add(value);
}
}
return result;
}
}
@@ -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));
}
}
}
@@ -0,0 +1,31 @@
package com.eactive.eai.common.util;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
public class TxSiftContext {
/**
* 거래 시작 시 SIFT discriminator를 MDC에 등록한다.
* SIFT가 비활성화 상태이면 아무 작업도 하지 않는다.
*/
public static boolean begin(String discriminatingValue) {
if (!Logger.isSiftEnabled()) return false;
if(MDC.get(Logger.DISCRIMINATOR) == null) {
MDC.put(Logger.DISCRIMINATOR, discriminatingValue);
return true;
}
return false;
}
/**
* 거래 종료 시 MDC에 등록된 SIFT appender를 닫고 MDC를 정리한다.
* begin() 에서 MDC가 설정되지 않았으면 아무 작업도 하지 않는다.
*/
public static void end() {
String discriminatingValue = MDC.get(Logger.DISCRIMINATOR);
if (StringUtils.isBlank(discriminatingValue)) return;
Logger.closeSiftAppender(discriminatingValue);
MDC.remove(Logger.DISCRIMINATOR);
}
}
@@ -0,0 +1,93 @@
package com.eactive.eai.common.util;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
public class XMLUtils {
private XMLUtils() {
throw new IllegalStateException("Utility class");
}
public static Document convertXmlDocument(Object message) throws DocumentException {
if (message == null)
return DocumentHelper.createDocument();
else if (message instanceof Document)
return (Document) message;
else if (message instanceof String) {
SAXReader builder = new SAXReader();
return builder.read(new StringReader((String)message));
} else
throw new IllegalArgumentException("not support xml type. " + message.getClass());
}
public static List<Element> getChilds(Element parent) {
List<Element> elements = new ArrayList<>();
Iterator<Element> iterator = parent.elementIterator();
while(iterator.hasNext()) {
Element child = iterator.next();
elements.add(child);
elements.addAll(getChilds(child));
}
return elements;
}
public static List<Element> getChildsLevel1(Element parent) {
List<Element> elements = new ArrayList<>();
Iterator<Element> iterator = parent.elementIterator();
while(iterator.hasNext()) {
Element child = iterator.next();
elements.add(child);
}
return elements;
}
@SuppressWarnings("unchecked")
public static Object elementToJSONObject(Element element) {
JSONObject json = new JSONObject();
List<Element> children = element.elements();
if(children.isEmpty()) {
return element.getText();
}
for(Element child : children) {
String childName = child.getName();
if(json.containsKey(childName)) {
Object existing = json.get(childName);
if(existing instanceof JSONArray) {
((JSONArray) existing).add(elementToJSONObject(child));
} else {
JSONArray list = new JSONArray();
list.add(existing);
list.add(elementToJSONObject(child));
json.put(childName, list);
}
} else {
json.put(childName, elementToJSONObject(child));
}
}
return json;
}
public static Element getElement(Element parent, String path) {
String[] keys = path.split("\\.");
Element current = parent;
for(String key : keys) {
current = current.element(key);
}
return (Element) current.elements().get(0);
}
}
@@ -8,6 +8,7 @@ import com.eactive.eai.agent.encryption.EncryptionManager;
import com.eactive.eai.common.dao.BaseDAO; import com.eactive.eai.common.dao.BaseDAO;
import com.eactive.eai.common.dao.DAOException; import com.eactive.eai.common.dao.DAOException;
import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.logger.EAIDBLogControl;
import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.ScpDbAgentUtil; import com.eactive.eai.common.util.ScpDbAgentUtil;
@@ -28,38 +29,6 @@ public class InboundErrorInfoDAO extends BaseDAO {
private InboundErrorInfoMapper inboundErrorInfoMapper; private InboundErrorInfoMapper inboundErrorInfoMapper;
public void addInboundErrorInfo(InboundErrorInfoVO vo) throws DAOException { public void addInboundErrorInfo(InboundErrorInfoVO vo) throws DAOException {
EncryptionManager manager = EncryptionManager.getInstance();
String bzwkDataCtnt = vo.getBwkDataTxt();
// 업무데이터를 암호화할 경우 길이가 늘어나는 문제가 있으므로 double check 함
if ("Y".equalsIgnoreCase(manager.getEncryptYN())) {
if (bzwkDataCtnt.length() > 2000) {
bzwkDataCtnt = StringUtil.chunkString(bzwkDataCtnt, 2000);
}
String encbzwkDataCtnt = null;
try {
encbzwkDataCtnt = ScpDbAgentUtil.ScpEncB64(
PropManager.getInstance().getProperties(ScpDbAgentUtil.EXT_MODULE), bzwkDataCtnt, "EUC-KR");
} catch (Exception e) {
logger.error("addInboundErrorInfo] scpdb_agent encryption Error - " + e.getMessage());
}
// double check
if (encbzwkDataCtnt != null && encbzwkDataCtnt.length() > 4000) {
bzwkDataCtnt = StringUtil.chunkString(bzwkDataCtnt, 500);
try {
encbzwkDataCtnt = ScpDbAgentUtil.ScpEncB64(
PropManager.getInstance().getProperties(ScpDbAgentUtil.EXT_MODULE), bzwkDataCtnt, "EUC-KR");
bzwkDataCtnt = encbzwkDataCtnt;
} catch (Exception e) {
logger.error("addInboundErrorInfo] scpdb_agent encryption Error - " + e.getMessage());
}
}
} else {
bzwkDataCtnt = StringUtil.chunkString(bzwkDataCtnt, 4000);
}
try { try {
vo.setErrTm((vo.getErrTm()).substring(0, 16)); vo.setErrTm((vo.getErrTm()).substring(0, 16));
vo.setErrTxt(StringUtil.chunkString(vo.getErrTxt(), 4000)); vo.setErrTxt(StringUtil.chunkString(vo.getErrTxt(), 4000));
@@ -74,6 +43,17 @@ public class InboundErrorInfoDAO extends BaseDAO {
} }
vo.setBzwkSvcKeyName(bizKey); vo.setBzwkSvcKeyName(bizKey);
String bzwkDataCtnt = vo.getBwkDataTxt();
// db 암호화
bzwkDataCtnt = EncryptionManager.getInstance().encryptDBData(bzwkDataCtnt);
vo.setBwkDataTxt(bzwkDataCtnt);
if (!EAIDBLogControl.isEnable()) {
logger.warn("DB logging is disabled.");
logger.error(vo.toString());
return;
}
InboundErrorInfo entity = inboundErrorInfoMapper.toEntity(vo); InboundErrorInfo entity = inboundErrorInfoMapper.toEntity(vo);
inboundErrorInfoLoader.save(entity); inboundErrorInfoLoader.save(entity);
} catch (Exception e) { } catch (Exception e) {
@@ -129,8 +129,8 @@ public class RequestProcessor extends RequestProcessorSupport {
String testMasterYn = adptGrpVO.getTestMasterYn(); String testMasterYn = adptGrpVO.getTestMasterYn();
// UUID 생성 : UUID에서 - 없는 32자리 // UUID 생성 : UUID에서 - 없는 32자리
String uuid = ""; String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
uuid = UUIDGenerator.getUUID().toString().replaceAll("-", ""); uuid = uuid == null ? UUIDGenerator.getUUID().toString().replaceAll("-", "") : uuid;
// UUID 생성 : UUID = server구분4자리 + UUID // UUID 생성 : UUID = server구분4자리 + UUID
/* /*
String uuid = ""; String uuid = "";
@@ -956,10 +956,7 @@ public class RequestProcessor extends RequestProcessorSupport {
// 거래통제 대상 거래일 경우 '1'로 설정한다. // 거래통제 대상 거래일 경우 '1'로 설정한다.
eaiMsg.setSvcTmEn(EAIMessageKeys.YES_FLAG); eaiMsg.setSvcTmEn(EAIMessageKeys.YES_FLAG);
vo.setRspErrorCode(EAIMessageKeys.EAI_BLOCKED_CODE); vo.setRspErrorCode(EAIMessageKeys.EAI_BLOCKED_CODE);
vo.setRspErrorMsg(ExceptionUtil.make(vo.getRspErrorCode(), new String[]{vo.getEaiSvcCd()})); vo.setRspErrorMsg(buildRestrictErrorMsg(eaiMsg, prop));
// eaiMsg.setRspErrCd(vo.getRspErrorCode());
// eaiMsg.setRspErrMsg(vo.getRspErrorMsg());
eaiMsg.setRspErr(vo.getRspErrorCode(),vo.getRspErrorMsg()); eaiMsg.setRspErr(vo.getRspErrorCode(),vo.getRspErrorMsg());
// 거래통제 로그에 로그생성 // 거래통제 로그에 로그생성
@@ -1033,9 +1030,8 @@ public class RequestProcessor extends RequestProcessorSupport {
Bucket bucket = InflowControlUtil.getBucket(); Bucket bucket = InflowControlUtil.getBucket();
String blockedType = ""; String blockedType = "";
// Client 유량제어 체크 — clientId가 있는 경우에만 String customBlockedType = checkCustomInflow(bucket, eaiMsg, prop);
String clientBlockedType = checkClientInflow(bucket, clientId); if (customBlockedType != null) {
if (clientBlockedType != null) {
blockedType += "C"; blockedType += "C";
} }
// Adapter 유량제어 체크 // Adapter 유량제어 체크
@@ -1066,7 +1062,7 @@ public class RequestProcessor extends RequestProcessorSupport {
InflowTargetVO inflowTargetVO = null; InflowTargetVO inflowTargetVO = null;
InflowGroupVO inflowGroupVO = null; InflowGroupVO inflowGroupVO = null;
if (blockedType.startsWith("C")) { if (blockedType.startsWith("C")) {
inflowTargetVO = resolveClientInflowThreshold(bucket, clientId); inflowTargetVO = resolveCustomInflowThreshold(bucket, eaiMsg, prop);
} else if (blockedType.startsWith("A")) { } else if (blockedType.startsWith("A")) {
inflowTargetVO = bucket.getAdapterInflowThreashold(vo.getAdapterGroupName()); inflowTargetVO = bucket.getAdapterInflowThreashold(vo.getAdapterGroupName());
} else if (blockedType.contains("G")) { } else if (blockedType.contains("G")) {
@@ -1089,7 +1085,7 @@ public class RequestProcessor extends RequestProcessorSupport {
} }
} else if (inflowTargetVO != null) { } else if (inflowTargetVO != null) {
String targetBlockedType; String targetBlockedType;
if (blockedType.startsWith("C")) targetBlockedType = clientBlockedType; if (blockedType.startsWith("C")) targetBlockedType = customBlockedType;
else if (blockedType.startsWith("A")) targetBlockedType = adapterBlockedType; else if (blockedType.startsWith("A")) targetBlockedType = adapterBlockedType;
else targetBlockedType = interfaceBlockedType; else targetBlockedType = interfaceBlockedType;
inflowErrorMsg = buildInflowTargetErrorMsg(inflowTargetVO, targetBlockedType); inflowErrorMsg = buildInflowTargetErrorMsg(inflowTargetVO, targetBlockedType);
@@ -1258,6 +1254,7 @@ public class RequestProcessor extends RequestProcessorSupport {
esbLogger.info(guidLogPrefix + " : FlowControl ROUTING_TYPE [" + prop.getProperty(RouteKeys.ROUTING_TYPE) + "]"); esbLogger.info(guidLogPrefix + " : FlowControl ROUTING_TYPE [" + prop.getProperty(RouteKeys.ROUTING_TYPE) + "]");
} }
beforeRoute(eaiMsg, prop);
retEaiMsg = FlowRouter.process(eaiMsg, prop); retEaiMsg = FlowRouter.process(eaiMsg, prop);
// 응답메시지에 Error Code기 설정되어 있을 경우 // 응답메시지에 Error Code기 설정되어 있을 경우
@@ -1285,6 +1282,7 @@ public class RequestProcessor extends RequestProcessorSupport {
throw new RequestProcessorException(vo.getRspErrorMsg()); throw new RequestProcessorException(vo.getRspErrorMsg());
} }
} }
afterRoute(eaiMsg, retEaiMsg, prop);
// TODO : StandardMessage -> SubMessage // TODO : StandardMessage -> SubMessage
if(Keys.IF_SUBSTANDARD.equals(vo.getStdMsgTypeCode())) { if(Keys.IF_SUBSTANDARD.equals(vo.getStdMsgTypeCode())) {
@@ -1398,12 +1396,11 @@ public class RequestProcessor extends RequestProcessorSupport {
} }
} }
// 클라이언트 유량제어 체크 훅 — 기본 구현은 체크 없음, DJErpRequestProcessor가 오버라이드 protected String checkCustomInflow(Bucket bucket, EAIMessage eaiMsg, Properties prop) {
protected String checkClientInflow(Bucket bucket, String clientId) {
return null; return null;
} }
protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) { protected InflowTargetVO resolveCustomInflowThreshold(Bucket bucket, EAIMessage eaiMsg, Properties prop) {
return null; return null;
} }
@@ -1420,6 +1417,14 @@ public class RequestProcessor extends RequestProcessorSupport {
return String.format("API 호출 한도 초과 [%s]", inflowTargetVO.getName()); return String.format("API 호출 한도 초과 [%s]", inflowTargetVO.getName());
} }
protected String buildRestrictErrorMsg(EAIMessage eaiMsg, Properties prop) {
return ExceptionUtil.make(EAIMessageKeys.EAI_BLOCKED_CODE, new String[]{eaiMsg.getEAISvcCd()});
}
protected void beforeRoute(EAIMessage eaiMsg, Properties prop) {}
protected void afterRoute(EAIMessage reqMsg, EAIMessage retMsg, Properties prop) {}
protected String getInboundErrorResponse(Properties prop, StandardMessage standardMessage, InterfaceMapper mapper protected String getInboundErrorResponse(Properties prop, StandardMessage standardMessage, InterfaceMapper mapper
, String errCode, String errMsg, String charset) { , String errCode, String errMsg, String charset) {
String errorResponse = null; String errorResponse = null;
@@ -51,6 +51,7 @@ public class StandardMessageUtil {
// API 서비스일경우 PathVariable 지원 추가 // API 서비스일경우 PathVariable 지원 추가
// ex) /api/account/{no} // ex) /api/account/{no}
if (StringUtils.equalsAny(vo.getActionName(), REST_URL_ACTION0, REST_URL_ACTION1, REST_URL_ACTION2)) { if (StringUtils.equalsAny(vo.getActionName(), REST_URL_ACTION0, REST_URL_ACTION1, REST_URL_ACTION2)) {
logger.debug("getSTDMessageForPathVariable call");
standardMessage = stdMessageManager.getSTDMessageForPathVariable(vo.getSelectedKey()); standardMessage = stdMessageManager.getSTDMessageForPathVariable(vo.getSelectedKey());
} }
@@ -61,6 +62,7 @@ public class StandardMessageUtil {
standardMessage.setBizData(message, charset); standardMessage.setBizData(message, charset);
standardManager.getMessageCoordinator().coordinateAfterCreateMessageByRule(standardMessage, vo, prop); standardManager.getMessageCoordinator().coordinateAfterCreateMessageByRule(standardMessage, vo, prop);
logger.debug("coordinateAfterCreateMessageByRule called");
return standardMessage; return standardMessage;
} }
@@ -671,6 +671,8 @@ public abstract class DefaultProcess extends Process {
StandardMessage resStandardMessage = this.resEaiMsg.getStandardMessage(); StandardMessage resStandardMessage = this.resEaiMsg.getStandardMessage();
InterfaceMapper mapper = this.resEaiMsg.getMapper(); InterfaceMapper mapper = this.resEaiMsg.getMapper();
String responseType = mapper.getResponseType(resStandardMessage); String responseType = mapper.getResponseType(resStandardMessage);
// apigw내 오류 없이, 표준헤더내 오류설정된 경우 응답 처리
if(!STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(responseType)){ if(!STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(responseType)){
String inboudnAdapterGroupName = this.callProp.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME); String inboudnAdapterGroupName = this.callProp.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME);
String inboudnAdapterName = this.callProp.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME); String inboudnAdapterName = this.callProp.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME);
@@ -682,11 +684,10 @@ public abstract class DefaultProcess extends Process {
AdapterVO inboundAdapterVO = AdapterManager.getInstance().getAdapterVO(inboudnAdapterGroupName, inboudnAdapterName); AdapterVO inboundAdapterVO = AdapterManager.getInstance().getAdapterVO(inboudnAdapterGroupName, inboudnAdapterName);
String errorResponseHandlerClass = AdapterPropManager.getInstance().getProperty(inboundAdapterVO.getPropGroupName(), "ERR_MSG_HANDLER"); String errorResponseHandlerClass = AdapterPropManager.getInstance().getProperty(inboundAdapterVO.getPropGroupName(), "ERR_MSG_HANDLER");
if(StringUtils.isBlank(errorResponseHandlerClass)) { if(StringUtils.isBlank(errorResponseHandlerClass)) {
this.resEaiMsg.setRspErrCd("RECEAIINA001", false);
String errorCode = mapper.getErrorCode(resStandardMessage); String errorCode = mapper.getErrorCode(resStandardMessage);
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage)); String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("Message.Msg_Body.OUTPUT_MSG_DESC")); String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc"));
this.resEaiMsg.setRspErrMsg(String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc)); this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
return; return;
} }
@@ -694,10 +695,12 @@ public abstract class DefaultProcess extends Process {
AdapterErrorMessageHandler adapterErrorMessageHandler = AdapterErrorMessageHandlerFactory.createHandler(errorResponseHandlerClass); AdapterErrorMessageHandler adapterErrorMessageHandler = AdapterErrorMessageHandlerFactory.createHandler(errorResponseHandlerClass);
Object responseObj = adapterErrorMessageHandler.generateNonStandardErrorResponseMessage(inboudnAdapterGroupName, inboudnAdapterName, this.callProp, this.tgtTranObject, resStandardMessage); Object responseObj = adapterErrorMessageHandler.generateNonStandardErrorResponseMessage(inboudnAdapterGroupName, inboudnAdapterName, this.callProp, this.tgtTranObject, this.resEaiMsg);
if(responseObj != null) { if(responseObj != null) {
resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode()); resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode());
} }
this.resEaiMsg.setRspErrCd(EAIMessageKeys.BWK_FAILMSG_CODE, false);
} }
} }
@@ -4,11 +4,17 @@ import java.net.SocketTimeoutException;
import java.util.Properties; import java.util.Properties;
import com.eactive.eai.common.TransactionContextKeys; 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.eactive.eai.util.PropertiesUtil;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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 org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterGroupVO;
@@ -99,7 +105,32 @@ public class HTTPProcess extends DefaultProcess {
this.timeout = iTimeoutValue * 1000; this.timeout = iTimeoutValue * 1000;
this.tempProp.put(INTERFACE_TIME_OUT, "" + this.timeout); this.tempProp.put(INTERFACE_TIME_OUT, "" + this.timeout);
try { try {
// 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.resObject = HttpSender.callService(this.adapterGroupName, this.outboundProp, this.reqObject, this.tempProp);
}
// 응답코드에 대한 처리 // 응답코드에 대한 처리
this.resEaiMsg = setResponseForOutbound(this.resEaiMsg); this.resEaiMsg = setResponseForOutbound(this.resEaiMsg);
if (!com.eactive.eai.adapter.Keys.IF_STANDARD.equals(this.adptrMsgPtrnCd)) { if (!com.eactive.eai.adapter.Keys.IF_STANDARD.equals(this.adptrMsgPtrnCd)) {
@@ -522,6 +522,7 @@ public class RESTProcess extends HTTPProcess {
String url = StringUtils.removeEnd(simAddr, "/") + "/mockapi/" + this.reqEaiMsg.getEAISvcCd(); String url = StringUtils.removeEnd(simAddr, "/") + "/mockapi/" + this.reqEaiMsg.getEAISvcCd();
this.outboundProp.put(HttpClientAdapterServiceKey.URL, url); this.outboundProp.put(HttpClientAdapterServiceKey.URL, url);
this.tempProp.put("tranType", reqEaiMsg.getTranType());
this.tempProp.remove(HttpAdapterServiceKey.INBOUND_REWRITE_PATH); this.tempProp.remove(HttpAdapterServiceKey.INBOUND_REWRITE_PATH);
logger.debug("simUrl=" + url); logger.debug("simUrl=" + url);
} }
@@ -15,26 +15,47 @@ public class JsonPathUtil {
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
// ---------------------------------------------------------------
// JsonNode 기반 단일 파싱 API — 연속 get/set 시 파싱 횟수 절감
// ---------------------------------------------------------------
/** JSON 문자열을 JsonNode 트리로 파싱. */
public static JsonNode toTree(String json) throws Exception {
return objectMapper.readTree(json);
}
/** 이미 파싱된 트리에서 경로의 값을 추출. */
public static String getAt(JsonNode root, String path) {
JsonNode node = getNodeAtPath(root, path);
return node != null ? node.asText() : null;
}
/** 이미 파싱된 트리의 경로에 값을 설정 (in-place). */
public static void setAt(JsonNode root, String path, String value) {
JsonNode parent = getParentNode(root, getParentPath(path));
if (parent == null) return;
String key = getFieldName(path);
if (parent.isArray()) {
((ArrayNode) parent).set(Integer.parseInt(key), objectMapper.convertValue(value, JsonNode.class));
} else if (parent.isObject()) {
((ObjectNode) parent).put(key, value);
}
}
/** JsonNode 트리를 JSON 문자열로 직렬화. */
public static String fromTree(JsonNode root) throws Exception {
return objectMapper.writeValueAsString(root);
}
// ---------------------------------------------------------------
// 문자열 기반 API (내부적으로 위 메서드에 위임)
// ---------------------------------------------------------------
// 특정 경로의 값을 설정하는 함수 // 특정 경로의 값을 설정하는 함수
public static String setValueAtPath(String jsonString, String path, String newValue, boolean isPretty) { public static String setValueAtPath(String jsonString, String path, String newValue, boolean isPretty) {
try { try {
// JSON 문자열을 JsonNode 객체로 파싱
JsonNode rootNode = objectMapper.readTree(jsonString); JsonNode rootNode = objectMapper.readTree(jsonString);
setAt(rootNode, path, newValue);
// 경로에 해당하는 부모 노드를 가져옴
JsonNode parentNode = getParentNode(rootNode, getParentPath(path));
// 경로의 마지막 노드 이름 또는 배열 인덱스 추출
String fieldNameOrIndex = getFieldName(path);
// 배열 또는 객체에서 값을 설정
if (parentNode.isArray()) {
((ArrayNode) parentNode).set(Integer.parseInt(fieldNameOrIndex), objectMapper.convertValue(newValue, JsonNode.class));
} else if (parentNode.isObject()) {
((ObjectNode) parentNode).put(fieldNameOrIndex, newValue);
}
// 수정된 JSON 문자열 반환
if(isPretty) if(isPretty)
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode);
else else
@@ -48,13 +69,7 @@ public class JsonPathUtil {
// 특정 경로의 값을 가져오는 함수 // 특정 경로의 값을 가져오는 함수
public static String getValueAtPath(String jsonString, String path) { public static String getValueAtPath(String jsonString, String path) {
try { try {
// JSON 문자열을 JsonNode 객체로 파싱 return getAt(objectMapper.readTree(jsonString), path);
JsonNode rootNode = objectMapper.readTree(jsonString);
// 경로에 해당하는 노드의 값을 반환
JsonNode resultNode = getNodeAtPath(rootNode, path);
return resultNode != null ? resultNode.asText() : null;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
@@ -101,6 +116,71 @@ public class JsonPathUtil {
return currentNode; return currentNode;
} }
/**
* 이미 파싱된 bodyNode에서 removeFromPath 필드를 제거하고 mergeJson을 병합.
* body의 재파싱 없이 호출 가능하여 파싱 횟수를 1회 절감.
*
* body와 mergeJson이 모두 JSON 객체인 경우에만 병합을 수행한다.
* 어느 한쪽이 JSON 객체가 아니면 mergeJson을 그대로 반환한다 (전체 교체 fallback).
*
* @param bodyNode 이미 파싱된 원본 JsonNode
* @param removeFromPath 제거할 필드의 경로 (예: /encryptedData)
* @param mergeJson 병합할 JSON 문자열
* @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환
*/
public static String mergeAtRoot(JsonNode bodyNode, String removeFromPath, String mergeJson) {
try {
JsonNode mergeNode = objectMapper.readTree(mergeJson);
if (!bodyNode.isObject() || !mergeNode.isObject()) {
return mergeJson;
}
ObjectNode result = (ObjectNode) bodyNode.deepCopy();
// removeFromPath 필드 제거
int lastSlash = removeFromPath.lastIndexOf('/');
String fieldName = removeFromPath.substring(lastSlash + 1);
if (lastSlash == 0) {
result.remove(fieldName);
} else {
JsonNode parent = getNodeAtPath(result, removeFromPath.substring(0, lastSlash));
if (parent instanceof ObjectNode) {
((ObjectNode) parent).remove(fieldName);
}
}
// mergeJson 필드를 body에 병합 (기존 필드 덮어쓰기)
result.setAll((ObjectNode) mergeNode);
return objectMapper.writeValueAsString(result);
} catch (Exception e) {
return mergeJson;
}
}
/**
* body에서 removeFromPath 필드를 제거한 뒤, mergeJson의 필드를 body에 병합하여 반환한다.
* (문자열 기반 overload — 내부적으로 body를 1회 파싱하여 위 메서드에 위임)
*
* 사용 예 (toPath="/" 복호화):
* body = {"encryptedData":"...","requestId":"REQ001"}
* removeFrom = /encryptedData
* mergeJson = {"userId":"U001","name":"test"}
* 결과 = {"requestId":"REQ001","userId":"U001","name":"test"}
*
* @param body 원본 JSON 문자열
* @param removeFromPath 제거할 필드의 경로 (예: /encryptedData)
* @param mergeJson 병합할 JSON 문자열
* @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환
*/
public static String mergeAtRoot(String body, String removeFromPath, String mergeJson) {
try {
return mergeAtRoot(objectMapper.readTree(body), removeFromPath, mergeJson);
} catch (Exception e) {
return mergeJson;
}
}
public static void main(String[] args) { public static void main(String[] args) {
String jsonString = "{ \"person\": { \"name\": \"John\", \"age\": 30, \"groups\": [ { \"id\": 1, \"name\": \"Group A\" }, { \"id\": 2, \"name\": \"Group B\" }, { \"id\": 3, \"name\": \"Group C\" } ] } }"; String jsonString = "{ \"person\": { \"name\": \"John\", \"age\": 30, \"groups\": [ { \"id\": 1, \"name\": \"Group A\" }, { \"id\": 2, \"name\": \"Group B\" }, { \"id\": 3, \"name\": \"Group C\" } ] } }";
@@ -0,0 +1,268 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Properties;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* OutCryptoFilter 단위 테스트.
*
* doPreFilter = 외부로 보내는 요청 암호화 (InCryptoFilter와 반대 방향)
* doPostFilter = 외부에서 받은 응답 복호화
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class OutCryptoFilterTest {
private static final String MODULE = "TEST_MODULE";
private static final String PLAIN_STR = "plain-request-body";
private static final byte[] PLAIN = PLAIN_STR.getBytes(StandardCharsets.UTF_8);
private static final byte[] CIPHER = new byte[]{0x10, 0x20, 0x30, 0x40, 0x50, 0x60};
private static final String CIPHER_B64 = Base64.getEncoder().encodeToString(CIPHER);
private static GenericApplicationContext ctx;
private static CryptoModuleService mockService;
private static OutCryptoFilter filter;
private Properties tempProp;
@BeforeAll
static void setUpClass() throws Exception {
mockService = mock(CryptoModuleService.class);
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
filter = new OutCryptoFilter();
}
@AfterAll
static void tearDownClass() {
if (ctx != null) ctx.close();
}
@BeforeEach
void setUp() throws Exception {
reset(mockService);
tempProp = new Properties();
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(PLAIN);
when(mockService.encrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(CIPHER);
}
// =========================================================================
// 1. buildAad
// =========================================================================
@Test
@DisplayName("1-1. buildAad — CRYPTO_AAD_PROP 미설정 → null")
void testBuildAad_noProp_returnsNull() {
assertNull(filter.buildAad(new Properties(), tempProp));
}
@Test
@DisplayName("1-2. buildAad — CRYPTO_AAD_PROP 설정, tempProp에 값 없음 → null")
void testBuildAad_propSet_noTempValue_returnsNull() {
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "aadKey");
assertNull(filter.buildAad(prop, tempProp));
}
@Test
@DisplayName("1-3. buildAad — CRYPTO_AAD_PROP 설정, tempProp에 값 있음 → UTF-8 바이트 반환")
void testBuildAad_propSet_tempValuePresent_returnsByte() {
String aadVal = "aad-value-xyz";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "aadKey");
tempProp.setProperty("aadKey", aadVal);
assertArrayEquals(aadVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, tempProp));
}
// =========================================================================
// 2. doPreFilter 암호화 (요청을 외부로 보내기 )
// =========================================================================
@Test
@DisplayName("2-1. BODY scope — 평문 암호화 → Base64 반환")
void testPreFilter_body_encrypt() throws Exception {
Object result = filter.doPreFilter("g", "a", bodyEncryptProps(), PLAIN_STR, tempProp);
assertEquals(CIPHER_B64, result);
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(PLAIN));
}
@Test
@DisplayName("2-2. FIELD scope, fromPath='/' — body 전체 암호화 → toPath 필드명으로 래핑")
void testPreFilter_field_encryptFromRoot() throws Exception {
String body = "{\"userId\":\"U001\"}";
Properties prop = fieldEncryptProps(tempProp, "/", "/encryptedData");
Object result = filter.doPreFilter("g", "a", prop, body, tempProp);
assertEquals("{\"encryptedData\":\"" + CIPHER_B64 + "\"}", result);
}
@Test
@DisplayName("2-3. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
void testPreFilter_field_encryptSpecificPath() throws Exception {
String body = "{\"data\":\"secret\",\"enc\":\"\"}";
Properties prop = fieldEncryptProps(tempProp, "/data", "/enc");
Object result = filter.doPreFilter("g", "a", prop, body, tempProp);
assertTrue(result.toString().contains("\"enc\":\"" + CIPHER_B64 + "\""),
"toPath에 Base64 암호문이 반영되어야 한다");
}
@Test
@DisplayName("2-4. FIELD scope — fromPath 값 없음 → body 그대로 반환")
void testPreFilter_field_missingFromPath_passThrough() throws Exception {
String body = "{\"other\":\"value\"}";
Properties prop = fieldEncryptProps(tempProp, "/data", "/enc");
Object result = filter.doPreFilter("g", "a", prop, body, tempProp);
assertEquals(body, result);
verifyNoInteractions(mockService);
}
@Test
@DisplayName("2-5. CRYPTO_MODULE_NAME 누락 — IllegalArgumentException")
void testPreFilter_missingModuleName_throwsException() {
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
assertThrows(IllegalArgumentException.class,
() -> filter.doPreFilter("g", "a", prop, PLAIN_STR, tempProp));
}
// =========================================================================
// 3. doPostFilter 복호화 (외부로부터 응답 받은 )
// =========================================================================
@Test
@DisplayName("3-1. BODY scope — Base64 암호문 복호화 → 평문 반환")
void testPostFilter_body_decrypt() throws Exception {
Object result = filter.doPostFilter("g", "a", bodyDecryptProps(), CIPHER_B64, tempProp);
assertEquals(PLAIN_STR, result);
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
}
@Test
@DisplayName("3-2. FIELD scope — fromPath 복호화 → toPath 반영")
void testPostFilter_field_decryptToPath() throws Exception {
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
Properties prop = fieldDecryptProps(tempProp, "/enc", "/result");
Object result = filter.doPostFilter("g", "a", prop, body, tempProp);
assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
"toPath에 복호화된 값이 반영되어야 한다");
}
@Test
@DisplayName("3-3. FIELD scope, toPath='/' — 복호화된 텍스트로 body 전체 교체")
void testPostFilter_field_decryptToRoot() throws Exception {
String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
Properties prop = fieldDecryptProps(tempProp, "/enc", "/");
Object result = filter.doPostFilter("g", "a", prop, body, tempProp);
assertEquals(PLAIN_STR, result);
}
@Test
@DisplayName("3-4. FIELD scope — fromPath 값 없음 → body 그대로 반환")
void testPostFilter_field_missingFromPath_passThrough() throws Exception {
String body = "{\"other\":\"value\"}";
Properties prop = fieldDecryptProps(tempProp, "/enc", "/result");
Object result = filter.doPostFilter("g", "a", prop, body, tempProp);
assertEquals(body, result);
verifyNoInteractions(mockService);
}
// =========================================================================
// 4. AAD (tempProp 경유)
// =========================================================================
@Test
@DisplayName("4-1. doPreFilter BODY — CRYPTO_AAD_PROP 설정 시 tempProp 값을 AAD로 전달")
void testPreFilter_body_aadFromTempProp() throws Exception {
String aadVal = "out-aad-value";
Properties prop = bodyEncryptProps();
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "myAadKey");
tempProp.setProperty("myAadKey", aadVal);
filter.doPreFilter("g", "a", prop, PLAIN_STR, tempProp);
verify(mockService).encrypt(eq(MODULE), anyMap(),
eq(aadVal.getBytes(StandardCharsets.UTF_8)), any(byte[].class));
}
@Test
@DisplayName("4-2. doPostFilter BODY — CRYPTO_AAD_PROP 미설정 시 aad=null로 전달")
void testPostFilter_body_noAadProp_nullAad() throws Exception {
filter.doPostFilter("g", "a", bodyDecryptProps(), CIPHER_B64, tempProp);
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), any(byte[].class));
}
// =========================================================================
// 헬퍼
// =========================================================================
private Properties bodyEncryptProps() {
Properties p = new Properties();
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
return p;
}
private Properties bodyDecryptProps() {
return bodyEncryptProps();
}
private Properties fieldEncryptProps(Properties tempProp, String fromPath, String toPath) {
Properties p = new Properties();
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
tempProp.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, fromPath);
tempProp.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, toPath);
return p;
}
private Properties fieldDecryptProps(Properties tempProp, String fromPath, String toPath) {
Properties p = new Properties();
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
tempProp.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, fromPath);
tempProp.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, toPath);
return p;
}
}
@@ -0,0 +1,466 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.adapter.http.dynamic.filter.InCryptoFilter;
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.hsm.HsmManager;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.security.CryptoModuleConfigVO;
import com.eactive.eai.common.security.CryptoModuleManager;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
import com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy;
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
/**
* CryptoFilter HSM 연동 통합 테스트 (SoftHSM2 / SunPKCS11)
*
* SoftHSM2에서 MASTER_KEY를 조회하여 키를 도출한
* InCryptoFilter 암복호화 전체 흐름을 검증한다.
*
* 사전 조건:
* C:\SoftHSM2 SoftHSM2 설치 (미설치 자동 건너뜀)
* MASTER_KEY alias는 없으면 자동 생성됨
*
* 검증 전략:
* HSM 도출 CryptoModuleExtension InCryptoFilter doPostFilter(암호화) doPreFilter(복호화) 원문 일치
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
public class CryptoFilterHsmIntegrationTest {
// -------------------------------------------------------------------------
// 상수
// -------------------------------------------------------------------------
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
private static final String TOKEN_LABEL = "eapim-test";
private static final String PIN = "1234";
private static final String MASTER_KEY_ALIAS = "MASTER_KEY";
/** HsmKeyDerivationStrategy: HSM 마스터키를 파생 없이 직접 사용 */
private static final String MOD_HSM_GCM = "HSM_GCM";
/** HsmContextSha256KeyDerivationStrategy: HSM 마스터키 + 컨텍스트값 SHA-256 파생 */
private static final String MOD_CTX_SHA_GCM = "CTX_SHA256_GCM";
private static final String CTX_KEY = "X-Api-Group-Seq";
private static final String CTX_VAL_A = "GROUP-A";
private static final String CTX_VAL_B = "GROUP-B";
// -------------------------------------------------------------------------
// 정적 필드
// -------------------------------------------------------------------------
private static GenericApplicationContext springCtx;
private static HsmManager hsmManager;
private static File tempCfgFile;
// -------------------------------------------------------------------------
// 내부 클래스: runtimeContext를 고정값으로 반환하는 필터
// -------------------------------------------------------------------------
static class ContextAwareCryptoFilter extends InCryptoFilter {
private final String key;
private final String value;
ContextAwareCryptoFilter(String key, String value) {
this.key = key;
this.value = value;
}
@Override
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
Map<String, String> ctx = new HashMap<>();
ctx.put(key, value);
return ctx;
}
}
// -------------------------------------------------------------------------
// 인스턴스 필드
// -------------------------------------------------------------------------
private HttpServletRequest mockReq;
private HttpServletResponse mockRes;
// -------------------------------------------------------------------------
// 전체 설정 / 해제
// -------------------------------------------------------------------------
@BeforeAll
static void setUpAll() throws Exception {
assumeTrue(new File(SOFTHSM2_DLL).exists(),
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
// 1. 토큰 초기화
ensureTokenInitialized();
// 2. PKCS11 설정 문자열
// attributes(*,...): generate/import 모두 CKA_SENSITIVE=false getEncoded() 사용 가능
String cfgContent = "name = SoftHSM\n"
+ "library = " + SOFTHSM2_DLL + "\n"
+ "slotListIndex = 0\n"
+ "attributes(*, CKO_SECRET_KEY, CKK_AES) = {\n"
+ " CKA_SENSITIVE = false\n"
+ " CKA_EXTRACTABLE = true\n"
+ "}\n";
tempCfgFile = File.createTempFile("pkcs11-filter-it-", ".cfg");
Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));
// 3. Mock PropManager (DB 없이 HSM 설정값 제공)
PropManager mockPropManager = mock(PropManager.class);
when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent);
when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN);
when(mockPropManager.getProperty(eq("HSM"), eq("CACHE_RELOAD_YN"), anyString())).thenReturn("N");
// 4. HsmManager (private 생성자 리플렉션)
Constructor<HsmManager> hsmCtor = HsmManager.class.getDeclaredConstructor();
hsmCtor.setAccessible(true);
hsmManager = hsmCtor.newInstance();
// 5. CryptoModuleManager (private 생성자 리플렉션)
CryptoModuleConfigLoader mockLoader = mock(CryptoModuleConfigLoader.class);
when(mockLoader.findAll()).thenReturn(buildModuleConfigs());
Constructor<CryptoModuleManager> managerCtor = CryptoModuleManager.class.getDeclaredConstructor();
managerCtor.setAccessible(true);
CryptoModuleManager manager = managerCtor.newInstance();
CryptoModuleService cryptoService = new CryptoModuleService();
HsmCryptoService hsmCryptoService = new HsmCryptoService();
// 6. Spring ApplicationContext 구성
springCtx = new GenericApplicationContext();
springCtx.getBeanFactory().registerSingleton("propManager", mockPropManager);
springCtx.getBeanFactory().registerSingleton("hsmManager", hsmManager);
springCtx.getBeanFactory().registerSingleton("hsmCryptoService", hsmCryptoService);
springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
springCtx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
springCtx.getBeanFactory().registerSingleton("cryptoModuleService", cryptoService);
springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
springCtx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
springCtx.refresh();
// 7. HsmManager 시작 (SunPKCS11 Provider 초기화 + KeyStore 오픈)
hsmManager.start();
assertTrue(hsmManager.isReady(), "HsmManager 초기화 실패");
System.out.println("[HSM] Provider: " + hsmManager.getPkcs11Provider().getName());
// 8. MASTER_KEY 등록 (없으면 자동 생성)
ensureMasterKey();
// 9. CryptoModuleManager 시작 (모듈 설정 로드)
manager.start();
System.out.println("[Crypto] 모듈 로드 완료");
}
@AfterAll
static void tearDownAll() throws Exception {
if (hsmManager != null && hsmManager.isStarted()) hsmManager.stop();
if (springCtx != null) springCtx.close();
if (tempCfgFile != null) tempCfgFile.delete();
}
@BeforeEach
void setUp() {
mockReq = mock(HttpServletRequest.class);
mockRes = mock(HttpServletResponse.class);
}
// =========================================================================
// 1. HsmKeyDerivationStrategy HSM 마스터키 직접 사용 (InCryptoFilter)
// =========================================================================
@Test
@DisplayName("1-1. FIELD/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
void testHsmGcm_field_roundTrip() throws Exception {
InCryptoFilter filter = new InCryptoFilter();
String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
// SCOPE 기본값 FIELD, 경로 기본값 사용
// 암호화: body 전체 /encrypted_data 필드
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
System.out.println("[1-1] encrypted: " + encrypted);
assertTrue(encrypted.contains("encrypted_data"), "/encrypted_data 필드가 있어야 한다");
// 복호화: /encrypted_data 필드 body 전체 교체
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
System.out.println("[1-1] decrypted: " + decrypted);
assertEquals(original, decrypted);
}
@Test
@DisplayName("1-2. BODY/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
void testHsmGcm_body_roundTrip() throws Exception {
InCryptoFilter filter = new InCryptoFilter();
String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
// 암호화: body 전체 Base64 암호문
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
System.out.println("[1-2] encrypted(Base64): " + encrypted);
assertNotEquals(original, encrypted, "암호문은 평문과 달라야 한다");
// 복호화: Base64 암호문 원문
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
System.out.println("[1-2] decrypted: " + decrypted);
assertEquals(original, decrypted);
}
@Test
@DisplayName("1-3. FIELD/GCM HsmKey + AAD 헤더 — 동일 요청 ID로 암복호화 성공")
void testHsmGcm_field_withAad_success() throws Exception {
InCryptoFilter filter = new InCryptoFilter();
String original = "{\"amount\":\"500000\"}";
String requestId = "REQ-20240101-001";
when(mockReq.getHeader("X-Request-Id")).thenReturn(requestId);
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id");
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
String decrypted = (String) filter.doPreFilter( "G", "A", encrypted, prop, mockReq, mockRes);
System.out.println("[1-3] decrypted: " + decrypted);
assertEquals(original, decrypted);
}
@Test
@DisplayName("1-4. FIELD/GCM HsmKey + AAD 헤더 — 다른 요청 ID로 복호화 시 GCM 인증 실패")
void testHsmGcm_field_withAad_wrongAad_fails() throws Exception {
InCryptoFilter filter = new InCryptoFilter();
String original = "{\"amount\":\"500000\"}";
// 암호화 요청 ID
HttpServletRequest encReq = mock(HttpServletRequest.class);
when(encReq.getHeader("X-Request-Id")).thenReturn("REQ-CORRECT");
// 복호화 다른 요청 ID
HttpServletRequest decReq = mock(HttpServletRequest.class);
when(decReq.getHeader("X-Request-Id")).thenReturn("REQ-WRONG");
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id");
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, encReq, mockRes);
assertThrows(Exception.class,
() -> filter.doPreFilter("G", "A", encrypted, prop, decReq, mockRes),
"잘못된 AAD로 GCM 복호화 시 예외가 발생해야 한다");
System.out.println("[1-4] 다른 AAD 복호화 예외 확인 완료");
}
// =========================================================================
// 2. HsmContextSha256KeyDerivationStrategy 컨텍스트 기반 SHA-256 파생
// =========================================================================
@Test
@DisplayName("2-1. FIELD/GCM ContextSha256 — 동일 컨텍스트로 암복호화 라운드트립")
void testCtxSha256Gcm_field_roundTrip() throws Exception {
InCryptoFilter filter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
String original = "{\"accNo\":\"0011234567890\",\"amount\":\"50000\"}";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
System.out.println("[2-1] encrypted: " + encrypted);
assertTrue(encrypted.contains("encrypted_data"));
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
System.out.println("[2-1] decrypted: " + decrypted);
assertEquals(original, decrypted);
}
@Test
@DisplayName("2-2. FIELD/GCM ContextSha256 — 암호화와 다른 컨텍스트로 복호화 시 GCM 인증 실패")
void testCtxSha256Gcm_differentContext_throwsException() throws Exception {
InCryptoFilter encFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
InCryptoFilter decFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
String original = "{\"data\":\"sensitive\"}";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
String encrypted = (String) encFilter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
assertThrows(Exception.class,
() -> decFilter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes),
"다른 컨텍스트 키로 파생된 키는 복호화에 실패해야 한다");
System.out.println("[2-2] 다른 컨텍스트 복호화 예외 확인 완료");
}
@Test
@DisplayName("2-3. FIELD/GCM ContextSha256 — 컨텍스트별 독립 암복호화 (GROUP-A, GROUP-B 각각 성공)")
void testCtxSha256Gcm_perGroupRoundTrip() throws Exception {
InCryptoFilter filterA = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
InCryptoFilter filterB = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
String plainA = "{\"group\":\"A\",\"amount\":\"1000\"}";
String plainB = "{\"group\":\"B\",\"amount\":\"2000\"}";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
String encA = (String) filterA.doPostFilter("G", "A", plainA, prop, mockReq, mockRes);
String encB = (String) filterB.doPostFilter("G", "B", plainB, prop, mockReq, mockRes);
assertEquals(plainA, (String) filterA.doPreFilter("G", "A", encA, prop, mockReq, mockRes));
assertEquals(plainB, (String) filterB.doPreFilter("G", "B", encB, prop, mockReq, mockRes));
System.out.println("[2-3] GROUP-A, GROUP-B 각각 독립 암복호화 성공");
}
// =========================================================================
// 헬퍼
// =========================================================================
private static List<CryptoModuleConfig> buildModuleConfigs() {
return Arrays.asList(
buildDynamic(MOD_HSM_GCM, "AES", "GCM", "NoPadding",
HsmKeyDerivationStrategy.class.getName(),
"{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\"}"),
buildDynamic(MOD_CTX_SHA_GCM, "AES", "GCM", "NoPadding",
HsmContextSha256KeyDerivationStrategy.class.getName(),
"{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\",\"contextKey\":\"" + CTX_KEY + "\"}")
);
}
private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding,
String strategy, String params) {
CryptoModuleConfig c = new CryptoModuleConfig();
c.setCryptoId(UUID.randomUUID().toString());
c.setCryptoName(name);
c.setAlgType(alg);
c.setCipherMode(mode);
c.setPadding(padding);
c.setKeySourceType("DYNAMIC");
c.setKeyDerivStrategy(strategy);
c.setKeyDerivParams(params);
c.setCacheYn("Y");
c.setCacheTtlSec(60);
c.setUseYn("Y");
// ivHex = null: GCM에서 IV를 AAD 대체값으로 사용하지 않도록 의도적으로 미설정
return c;
}
private static void ensureMasterKey() throws Exception {
java.security.KeyStore ks = hsmManager.getKeyStore();
// 이전 실행 삭제 (sensitive 여부 무관하게 항상 고정키로 재등록)
if (ks.containsAlias(MASTER_KEY_ALIAS)) {
ks.deleteEntry(MASTER_KEY_ALIAS);
System.out.println("[HSM] 기존 MASTER_KEY 삭제");
}
// CryptoModuleServiceTest.KEY_128 동일한 고정 테스트 암호문 비교 가능
// attributes(*, CKO_SECRET_KEY, CKK_AES) 지시어로 임포트 CKA_SENSITIVE=false 적용
byte[] keyBytes = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
SecretKey fixedKey = new SecretKeySpec(keyBytes, "AES");
ks.setKeyEntry(MASTER_KEY_ALIAS, fixedKey, null, null);
// 임포트 getEncoded() 검증
SecretKey stored = (SecretKey) ks.getKey(MASTER_KEY_ALIAS, null);
byte[] encoded = stored.getEncoded();
System.out.println("[HSM] MASTER_KEY 등록 완료: alias=" + MASTER_KEY_ALIAS
+ " / getEncoded()=" + (encoded != null ? encoded.length + "bytes, " + new String(encoded) : "null (추출 불가!)"));
}
private static void ensureTokenInitialized() throws Exception {
ProcessBuilder pb = new ProcessBuilder(
SOFTHSM2_UTIL, "--init-token", "--free",
"--label", TOKEN_LABEL, "--pin", PIN, "--so-pin", PIN);
pb.redirectErrorStream(true);
Process p = pb.start();
String out = readOutput(p);
p.waitFor(10, TimeUnit.SECONDS);
System.out.println("[SoftHSM2] init-token: " + out.trim());
}
private static CryptoModuleConfigMapper testMapper() {
return new CryptoModuleConfigMapper() {
@Override
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
vo.setCryptoId(e.getCryptoId());
vo.setCryptoName(e.getCryptoName());
vo.setCryptoDesc(e.getCryptoDesc());
vo.setAlgType(e.getAlgType());
vo.setCipherMode(e.getCipherMode());
vo.setPadding(e.getPadding());
vo.setIvHex(e.getIvHex());
vo.setKeySourceType(e.getKeySourceType());
vo.setEncKeyHex(e.getEncKeyHex());
vo.setDecKeyHex(e.getDecKeyHex());
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
vo.setKeyDerivParams(e.getKeyDerivParams());
vo.setCacheYn(e.getCacheYn());
vo.setCacheTtlSec(e.getCacheTtlSec());
vo.setUseYn(e.getUseYn());
return vo;
}
@Override
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
return null;
}
};
}
private static String readOutput(Process p) throws Exception {
InputStream is = p.getInputStream();
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int len;
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len, "UTF-8"));
}
return sb.toString();
}
}
@@ -0,0 +1,395 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mockito.ArgumentCaptor;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* InCryptoFilter / AbstractCryptoFilter 단위 테스트.
* CryptoModuleService는 Mock으로 대체하여 필터 로직(Base64, JSON 경로, 프로퍼티 파싱) 검증한다.
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class InCryptoFilterTest {
private static final String MODULE = "TEST_MODULE";
private static final String PLAIN_STR = "decrypted-plain-text";
private static final byte[] PLAIN = PLAIN_STR.getBytes(StandardCharsets.UTF_8);
private static final byte[] CIPHER = new byte[]{0x10, 0x20, 0x30, 0x40, 0x50, 0x60};
private static final String CIPHER_B64 = Base64.getEncoder().encodeToString(CIPHER);
private static GenericApplicationContext ctx;
private static CryptoModuleService mockService;
private static InCryptoFilter filter;
private HttpServletRequest mockRequest;
private HttpServletResponse mockResponse;
@BeforeAll
static void setUpClass() throws Exception {
mockService = mock(CryptoModuleService.class);
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
filter = new InCryptoFilter();
}
@AfterAll
static void tearDownClass() {
if (ctx != null) ctx.close();
}
@BeforeEach
void setUp() throws Exception {
reset(mockService);
mockRequest = mock(HttpServletRequest.class);
mockResponse = mock(HttpServletResponse.class);
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(PLAIN);
when(mockService.encrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(CIPHER);
}
// =========================================================================
// 1. buildRuntimeContext
// =========================================================================
@Test
@DisplayName("1-1. buildRuntimeContext — 항상 빈 Map 반환")
void testBuildRuntimeContext_returnsEmptyMap() {
Map<String, String> result = filter.buildRuntimeContext(new Properties(), mockRequest);
assertNotNull(result);
assertTrue(result.isEmpty());
}
@SuppressWarnings("unchecked")
@Test
@DisplayName("1-2. buildRuntimeContext 오버라이드 시 runtimeContext가 복호화 서비스에 전달된다")
void testBuildRuntimeContext_override_passedToDecrypt() throws Exception {
InCryptoFilter filterWithCtx = new InCryptoFilter() {
@Override
protected Map<String, String> buildRuntimeContext(Properties p, HttpServletRequest req) {
Map<String, String> ctx = new HashMap<>();
ctx.put("groupSeq", "G001");
return ctx;
}
};
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
filterWithCtx.doPreFilter("g", "a", body, fieldDecryptProps("/enc", "/result"), mockRequest, mockResponse);
ArgumentCaptor<Map> ctxCaptor = ArgumentCaptor.forClass(Map.class);
verify(mockService).decrypt(eq(MODULE), ctxCaptor.capture(), isNull(), eq(CIPHER));
assertEquals("G001", ctxCaptor.getValue().get("groupSeq"));
}
@SuppressWarnings("unchecked")
@Test
@DisplayName("1-3. buildRuntimeContext 오버라이드 시 runtimeContext가 암호화 서비스에 전달된다")
void testBuildRuntimeContext_override_passedToEncrypt() throws Exception {
InCryptoFilter filterWithCtx = new InCryptoFilter() {
@Override
protected Map<String, String> buildRuntimeContext(Properties p, HttpServletRequest req) {
Map<String, String> ctx = new HashMap<>();
ctx.put("groupSeq", "G001");
return ctx;
}
};
filterWithCtx.doPostFilter("g", "a", "plain-body", fieldEncryptProps("/", "/encryptedData"), mockRequest, mockResponse);
ArgumentCaptor<Map> ctxCaptor = ArgumentCaptor.forClass(Map.class);
verify(mockService).encrypt(eq(MODULE), ctxCaptor.capture(), isNull(), any(byte[].class));
assertEquals("G001", ctxCaptor.getValue().get("groupSeq"));
}
// =========================================================================
// 2. buildAad
// =========================================================================
@Test
@DisplayName("2-1. buildAad — CRYPTO_AAD_HEADER 미설정 → null")
void testBuildAad_noProp_returnsNull() {
assertNull(filter.buildAad(new Properties(), mockRequest));
}
@Test
@DisplayName("2-2. buildAad — 헤더명 설정, 헤더값 없음 → null")
void testBuildAad_headerNameSet_noValue_returnsNull() {
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(null);
assertNull(filter.buildAad(prop, mockRequest));
}
@Test
@DisplayName("2-3. buildAad — 헤더명 설정, 헤더값 있음 → UTF-8 바이트 반환")
void testBuildAad_headerNameSet_valuePresent_returnsByte() {
String headerVal = "req-id-abc123";
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(headerVal);
assertArrayEquals(headerVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, mockRequest));
}
// =========================================================================
// 3. doPreFilter BODY scope 복호화
// =========================================================================
@Test
@DisplayName("3-1. BODY scope — Base64 암호문 복호화 → 평문 반환")
void testPreFilter_body_decrypt_roundTrip() throws Exception {
Object result = filter.doPreFilter("g", "a", CIPHER_B64, bodyDecryptProps(), mockRequest, mockResponse);
assertEquals(PLAIN_STR, result);
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
}
@Test
@DisplayName("3-2. CRYPTO_MODULE_NAME 누락 — IllegalArgumentException")
void testPreFilter_missingModuleName_throwsException() {
Properties prop = new Properties();
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
assertThrows(IllegalArgumentException.class,
() -> filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse));
}
@Test
@DisplayName("3-3. message가 byte[] — UTF-8 디코딩 후 처리")
void testPreFilter_body_byteArrayMessage() throws Exception {
Object result = filter.doPreFilter("g", "a", CIPHER_B64.getBytes(StandardCharsets.UTF_8),
bodyDecryptProps(), mockRequest, mockResponse);
assertEquals(PLAIN_STR, result);
}
@Test
@DisplayName("3-4. message가 String/byte[] 외 타입 — toString() 변환 후 처리")
void testPreFilter_body_nonStringMessage_usesToString() throws Exception {
Object nonStringMsg = new Object() {
@Override
public String toString() { return CIPHER_B64; }
};
Object result = filter.doPreFilter("g", "a", nonStringMsg, bodyDecryptProps(), mockRequest, mockResponse);
assertEquals(PLAIN_STR, result);
}
// =========================================================================
// 4. doPreFilter FIELD scope 복호화
// =========================================================================
@Test
@DisplayName("4-1. FIELD scope — fromPath 복호화 → toPath 반영")
void testPreFilter_field_decryptToPath() throws Exception {
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
Properties prop = fieldDecryptProps("/enc", "/result");
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
"toPath에 복호화된 값이 반영되어야 한다");
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
}
@Test
@DisplayName("4-2. FIELD scope, toPath='/' — 복호화 결과가 비-JSON이면 텍스트 그대로 반환 (fallback)")
void testPreFilter_field_decryptToRoot_nonJsonFallback() throws Exception {
String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
Properties prop = fieldDecryptProps("/enc", "/");
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
assertEquals(PLAIN_STR, result, "복호화 결과가 JSON 객체가 아니면 텍스트 그대로 반환");
}
@Test
@DisplayName("4-4. FIELD scope, toPath='/' — 복호화된 JSON을 body에 병합 (fromPath 필드 제거)")
void testPreFilter_field_decryptToRoot_mergesJson() throws Exception {
String decryptedJson = "{\"userId\":\"U001\",\"name\":\"test\"}";
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class)))
.thenReturn(decryptedJson.getBytes(StandardCharsets.UTF_8));
String body = "{\"encryptedData\":\"" + CIPHER_B64 + "\",\"requestId\":\"REQ001\"}";
Properties prop = fieldDecryptProps("/encryptedData", "/");
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
String r = result.toString();
assertFalse(r.contains("encryptedData"), "암호화 필드(encryptedData)는 제거되어야 한다");
assertTrue(r.contains("\"requestId\":\"REQ001\""), "원본 body의 다른 필드는 유지되어야 한다");
assertTrue(r.contains("\"userId\":\"U001\""), "복호화된 JSON 필드가 병합되어야 한다");
assertTrue(r.contains("\"name\":\"test\""), "복호화된 JSON 필드가 병합되어야 한다");
}
@Test
@DisplayName("4-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
void testPreFilter_field_missingFromPathValue_passThrough() throws Exception {
String body = "{\"other\":\"value\"}";
Properties prop = fieldDecryptProps("/enc", "/other");
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
assertEquals(body, result);
verifyNoInteractions(mockService);
}
// =========================================================================
// 5. doPostFilter BODY scope 암호화
// =========================================================================
@Test
@DisplayName("5-1. BODY scope — 평문 암호화 → Base64 반환")
void testPostFilter_body_encrypt() throws Exception {
String body = "plain-response-body";
Object result = filter.doPostFilter("g", "a", body, bodyEncryptProps(), mockRequest, mockResponse);
assertEquals(CIPHER_B64, result);
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(body.getBytes(StandardCharsets.UTF_8)));
}
// =========================================================================
// 6. doPostFilter FIELD scope 암호화
// =========================================================================
@Test
@DisplayName("6-1. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
void testPostFilter_field_encryptToPath() throws Exception {
String body = "{\"plain\":\"hello\",\"enc\":\"\"}";
Properties prop = fieldEncryptProps("/plain", "/enc");
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
assertTrue(result.toString().contains("\"enc\":\"" + CIPHER_B64 + "\""),
"toPath에 Base64 암호문이 반영되어야 한다");
}
@Test
@DisplayName("6-2. FIELD scope, fromPath='/' — body 전체 암호화 → toPath 필드명으로 래핑")
void testPostFilter_field_encryptFromRoot() throws Exception {
String body = "{\"plain\":\"hello\"}";
Properties prop = fieldEncryptProps("/", "/encrypted");
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
assertEquals("{\"encrypted\":\"" + CIPHER_B64 + "\"}", result);
}
@Test
@DisplayName("6-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
void testPostFilter_field_missingFromPathValue_passThrough() throws Exception {
String body = "{\"other\":\"value\"}";
Properties prop = fieldEncryptProps("/plain", "/enc");
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
assertEquals(body, result);
verifyNoInteractions(mockService);
}
// =========================================================================
// 7. AAD 헤더 연동
// =========================================================================
@Test
@DisplayName("7-1. doPreFilter BODY — CRYPTO_AAD_HEADER 설정 시 헤더값을 AAD로 전달")
void testPreFilter_body_aadFromHeader_passedToService() throws Exception {
String aadVal = "request-id-xyz";
Properties prop = bodyDecryptProps();
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(aadVal);
filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse);
verify(mockService).decrypt(eq(MODULE), anyMap(),
eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
}
@Test
@DisplayName("7-2. doPostFilter BODY — CRYPTO_AAD_HEADER 미설정 시 aad=null로 전달")
void testPostFilter_body_noAadHeader_nullAadPassedToService() throws Exception {
filter.doPostFilter("g", "a", "plain", bodyEncryptProps(), mockRequest, mockResponse);
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), any(byte[].class));
}
@Test
@DisplayName("7-3. doPreFilter FIELD — CRYPTO_AAD_HEADER 설정 시 헤더값을 AAD로 전달")
void testPreFilter_field_aadFromHeader_passedToService() throws Exception {
String aadVal = "field-aad-value";
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
Properties prop = fieldDecryptProps("/enc", "/result");
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Api-Aad");
when(mockRequest.getHeader("X-Api-Aad")).thenReturn(aadVal);
filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
verify(mockService).decrypt(eq(MODULE), anyMap(),
eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
}
// =========================================================================
// 헬퍼
// =========================================================================
private Properties bodyDecryptProps() {
Properties p = new Properties();
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
return p;
}
private Properties bodyEncryptProps() {
return bodyDecryptProps();
}
private Properties fieldDecryptProps(String fromPath, String toPath) {
Properties p = new Properties();
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
p.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, fromPath);
p.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, toPath);
return p;
}
private Properties fieldEncryptProps(String fromPath, String toPath) {
Properties p = new Properties();
p.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MODULE);
p.setProperty(AbstractCryptoFilter.PROP_SCOPE, "FIELD");
p.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, fromPath);
p.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, toPath);
return p;
}
}
@@ -1,103 +0,0 @@
package com.eactive.eai.agent.inflow;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.agent.command.CommandException;
import com.eactive.eai.common.inflow.InflowControlManager;
import com.eactive.eai.common.inflow.InflowControlUtil;
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
/**
* ReloadInflowClientControlCommand 단위 테스트.
*
* <p>InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여
* PropManager/ApplicationContext 없이 동작을 검증한다.
*/
class ReloadInflowClientControlCommandTest {
private ClientDualInflowControlManager mockDualManager;
@BeforeEach
void setUp() {
mockDualManager = mock(ClientDualInflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
@AfterEach
void tearDown() {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
private ReloadInflowClientControlCommand commandWith(Object args) {
ReloadInflowClientControlCommand cmd = new ReloadInflowClientControlCommand();
cmd.setArgs(args);
return cmd;
}
// =========================================================================
// args 타입 검증 실패
// =========================================================================
@Test
void execute_argsIsInteger_throwsCommandException() {
// args 검증이 getInflowControlManager() 호출 이전에 발생하므로 캐시 설정 불필요
assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute);
}
// =========================================================================
// ClientDualInflowControlManager 비활성
// =========================================================================
@Test
void execute_notClientDualManager_throwsCommandException() {
InflowControlManager baseMgr = mock(InflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
assertThrows(CommandException.class, commandWith("CLIENT_A")::execute);
}
// =========================================================================
// 전체 재로드 (ALL)
// =========================================================================
@Test
void execute_argsIsALL_callsReloadClientNoArgs() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
Object result = commandWith("ALL").execute();
assertEquals("success", result);
verify(mockDualManager).reloadClient();
verify(mockDualManager, never()).reloadClient(anyString());
}
// =========================================================================
// 개별 재로드 (ClientId)
// =========================================================================
@Test
void execute_argsIsClientId_callsReloadClientWithId() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
Object result = commandWith("CLIENT_A").execute();
assertEquals("success", result);
verify(mockDualManager).reloadClient("CLIENT_A");
verify(mockDualManager, never()).reloadClient();
}
@Test
void execute_differentClientId_callsReloadClientWithThatId() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
commandWith("CLIENT_B").execute();
verify(mockDualManager).reloadClient("CLIENT_B");
}
}
@@ -1,96 +0,0 @@
package com.eactive.eai.agent.inflow;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.agent.command.CommandException;
import com.eactive.eai.common.inflow.InflowControlManager;
import com.eactive.eai.common.inflow.InflowControlUtil;
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
/**
* RemoveInflowClientControlCommand 단위 테스트.
*
* <p>InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여
* PropManager/ApplicationContext 없이 동작을 검증한다.
*/
class RemoveInflowClientControlCommandTest {
private ClientDualInflowControlManager mockDualManager;
@BeforeEach
void setUp() {
mockDualManager = mock(ClientDualInflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
@AfterEach
void tearDown() {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
private RemoveInflowClientControlCommand commandWith(Object args) {
RemoveInflowClientControlCommand cmd = new RemoveInflowClientControlCommand();
cmd.setArgs(args);
return cmd;
}
// =========================================================================
// args 타입 검증 실패
// =========================================================================
@Test
void execute_argsIsInteger_throwsCommandException() {
assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute);
}
// =========================================================================
// ClientDualInflowControlManager 비활성
// =========================================================================
@Test
void execute_notClientDualManager_throwsCommandException() {
InflowControlManager baseMgr = mock(InflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
assertThrows(CommandException.class, commandWith("CLIENT_A")::execute);
}
// =========================================================================
// 정상 제거
// =========================================================================
@Test
void execute_validClientId_callsRemoveClient() throws CommandException {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
Object result = commandWith("CLIENT_A").execute();
assertEquals("success", result);
verify(mockDualManager).removeClient("CLIENT_A");
}
@Test
void execute_differentClientId_callsRemoveClientWithThatId() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
commandWith("CLIENT_B").execute();
verify(mockDualManager).removeClient("CLIENT_B");
}
@Test
void execute_validClientId_doesNotCallReload() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
commandWith("CLIENT_A").execute();
verify(mockDualManager, never()).reloadClient();
verify(mockDualManager, never()).reloadClient(anyString());
}
}
@@ -0,0 +1,260 @@
package com.eactive.eai.authserver.jwt;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Base64;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
/**
* RsaPssSigner / RsaPssVerifier 단위 테스트.
* Spring 컨텍스트 불필요 KeyPairGenerator로 테스트용 RSA 2048 키쌍을 생성하여 검증한다.
*/
@SuppressWarnings("deprecation")
@TestMethodOrder(MethodOrderer.DisplayName.class)
class RsaPssSignerVerifierTest {
private static RSAPrivateKey privateKey;
private static RSAPublicKey publicKey;
private static RsaPssSigner signer;
private static RsaPssVerifier verifier;
private static final byte[] CONTENT = "header.payload".getBytes();
@BeforeAll
static void setUpClass() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KeyPair keyPair = gen.generateKeyPair();
privateKey = (RSAPrivateKey) keyPair.getPrivate();
publicKey = (RSAPublicKey) keyPair.getPublic();
signer = new RsaPssSigner(privateKey);
verifier = new RsaPssVerifier(publicKey);
}
// =========================================================================
// 1. RsaPssSigner
// =========================================================================
@Test
@DisplayName("1-1. algorithm() — PS256 반환")
void testSignerAlgorithm() {
assertEquals("PS256", signer.algorithm());
}
@Test
@DisplayName("1-2. sign() — non-null, 256바이트 (RSA-2048 서명 크기)")
void testSignReturnsExpectedLength() {
byte[] sig = signer.sign(CONTENT);
assertNotNull(sig);
assertEquals(256, sig.length);
}
@Test
@DisplayName("1-3. sign() — 비결정론적 (같은 입력이라도 랜덤 salt로 인해 서명이 달라야 한다)")
void testSignIsNonDeterministic() {
byte[] sig1 = signer.sign(CONTENT);
byte[] sig2 = signer.sign(CONTENT);
assertFalse(Arrays.equals(sig1, sig2), "PSS는 매번 다른 salt를 사용하므로 서명이 달라야 한다");
}
// =========================================================================
// 2. RsaPssVerifier
// =========================================================================
@Test
@DisplayName("2-1. algorithm() — PS256 반환")
void testVerifierAlgorithm() {
assertEquals("PS256", verifier.algorithm());
}
@Test
@DisplayName("2-2. 정상 서명 → 검증 성공 (예외 없음)")
void testVerifyValidSignature() {
byte[] sig = signer.sign(CONTENT);
assertDoesNotThrow(() -> verifier.verify(CONTENT, sig));
}
@Test
@DisplayName("2-3. 내용 변조 → InvalidSignatureException")
void testVerifyTamperedContent() {
byte[] sig = signer.sign(CONTENT);
byte[] tampered = "tampered.payload".getBytes();
assertThrows(InvalidSignatureException.class, () -> verifier.verify(tampered, sig));
}
@Test
@DisplayName("2-4. 서명 변조 (첫 바이트 반전) → InvalidSignatureException")
void testVerifyTamperedSignature() {
byte[] sig = signer.sign(CONTENT);
sig[0] ^= 0xFF;
assertThrows(InvalidSignatureException.class, () -> verifier.verify(CONTENT, sig));
}
@Test
@DisplayName("2-5. 다른 키쌍의 공개키로 검증 → InvalidSignatureException")
void testVerifyWithWrongPublicKey() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
RSAPublicKey otherPublicKey = (RSAPublicKey) gen.generateKeyPair().getPublic();
RsaPssVerifier wrongVerifier = new RsaPssVerifier(otherPublicKey);
byte[] sig = signer.sign(CONTENT);
assertThrows(InvalidSignatureException.class, () -> wrongVerifier.verify(CONTENT, sig));
}
// =========================================================================
// 3. Signer + Verifier 통합
// =========================================================================
@Test
@DisplayName("3-1. sign → verify 라운드트립 — 성공")
void testRoundTrip() {
byte[] sig = signer.sign(CONTENT);
assertDoesNotThrow(() -> verifier.verify(CONTENT, sig));
}
@Test
@DisplayName("3-2. 다른 키쌍의 서명을 원래 verifier로 검증 → 실패")
void testRoundTripCrossKeyFails() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KeyPair other = gen.generateKeyPair();
RsaPssSigner otherSigner = new RsaPssSigner((RSAPrivateKey) other.getPrivate());
byte[] sigByOther = otherSigner.sign(CONTENT);
assertThrows(InvalidSignatureException.class, () -> verifier.verify(CONTENT, sigByOther));
}
@Test
@DisplayName("3-3. 원래 서명을 다른 키쌍의 verifier로 검증 → 실패")
void testRoundTripCrossVerifierFails() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KeyPair other = gen.generateKeyPair();
RsaPssVerifier otherVerifier = new RsaPssVerifier((RSAPublicKey) other.getPublic());
byte[] sig = signer.sign(CONTENT);
assertThrows(InvalidSignatureException.class, () -> otherVerifier.verify(CONTENT, sig));
}
// =========================================================================
// 4. 시각 확인 콘솔 출력으로 RS256(결정론적) vs PS256(비결정론적) 비교
// =========================================================================
@Test
@DisplayName("4-1. [시각 확인] RS256은 결정론적, PS256은 비결정론적 — 콘솔 hex 출력")
void testVisualComparison() {
RsaSigner rs256Signer = new RsaSigner(privateKey);
byte[] rs256Sig1 = rs256Signer.sign(CONTENT);
byte[] rs256Sig2 = rs256Signer.sign(CONTENT);
byte[] ps256Sig1 = signer.sign(CONTENT);
byte[] ps256Sig2 = signer.sign(CONTENT);
System.out.println();
System.out.println("━━━ 서명 알고리즘 시각 비교 (동일 데이터: \"" + new String(CONTENT) + "\") ━━━");
System.out.println();
System.out.printf("[RS256 서명 1] %s%n", toHex(rs256Sig1));
System.out.printf("[RS256 서명 2] %s%n", toHex(rs256Sig2));
System.out.printf(" → 두 서명 일치 여부: %b (결정론적 — 항상 같음)%n", Arrays.equals(rs256Sig1, rs256Sig2));
System.out.println();
System.out.printf("[PS256 서명 1] %s%n", toHex(ps256Sig1));
System.out.printf("[PS256 서명 2] %s%n", toHex(ps256Sig2));
System.out.printf(" → 두 서명 일치 여부: %b (비결정론적 — 매번 다름, PSS 랜덤 salt 때문)%n", Arrays.equals(ps256Sig1, ps256Sig2));
System.out.println();
assertTrue(Arrays.equals(rs256Sig1, rs256Sig2), "RS256은 결정론적이어야 한다");
assertFalse(Arrays.equals(ps256Sig1, ps256Sig2), "PS256은 비결정론적이어야 한다");
}
// =========================================================================
// 4-2. JWT 토큰 시각 확인 헤더 alg 필드 + 서명 파트 비교
// =========================================================================
@Test
@DisplayName("4-2. [JWT 시각 확인] alg 헤더(RS256/PS256)와 서명 파트 변화를 JWT 문자열로 확인")
void testJwtVisualComparison() {
String claims = "{\"sub\":\"test-client\",\"scope\":\"read\"}";
RsaSigner rs256Signer = new RsaSigner(privateKey);
PssJwtAccessTokenConverter pssConverter = new PssJwtAccessTokenConverter(privateKey, publicKey);
String rs256Jwt1 = JwtHelper.encode(claims, rs256Signer).getEncoded();
String rs256Jwt2 = JwtHelper.encode(claims, rs256Signer).getEncoded();
String ps256Jwt1 = pssConverter.buildJwt(claims);
String ps256Jwt2 = pssConverter.buildJwt(claims);
System.out.println();
System.out.println("━━━ JWT 토큰 시각 비교 ━━━");
System.out.println();
printJwt("RS256 JWT 1", rs256Jwt1);
printJwt("RS256 JWT 2", rs256Jwt2);
System.out.printf(" → 두 JWT 완전 일치: %b (헤더·페이로드·서명 모두 동일)%n",
rs256Jwt1.equals(rs256Jwt2));
System.out.println();
printJwt("PS256 JWT 1", ps256Jwt1);
printJwt("PS256 JWT 2", ps256Jwt2);
System.out.printf(" → 두 JWT 완전 일치: %b (헤더·페이로드 동일, 서명만 다름)%n",
ps256Jwt1.equals(ps256Jwt2));
System.out.println();
System.out.println(" [서명 파트만 비교]");
System.out.printf(" RS256 서명1: %s%n", signaturePart(rs256Jwt1));
System.out.printf(" RS256 서명2: %s%n", signaturePart(rs256Jwt2));
System.out.printf(" PS256 서명1: %s%n", signaturePart(ps256Jwt1));
System.out.printf(" PS256 서명2: %s%n", signaturePart(ps256Jwt2));
assertEquals(rs256Jwt1, rs256Jwt2, "RS256 JWT는 동일해야 한다");
assertNotEquals(ps256Jwt1, ps256Jwt2, "PS256 JWT는 서명 파트가 달라야 한다");
}
private static void printJwt(String label, String jwt) {
String[] parts = jwt.split("\\.");
String header = new String(Base64.getUrlDecoder().decode(padBase64Url(parts[0])));
String payload = new String(Base64.getUrlDecoder().decode(padBase64Url(parts[1])));
System.out.printf("[%s]%n", label);
System.out.printf(" 전체 : %s%n", jwt);
System.out.printf(" 헤더 : %s%n", header); // {"alg":"RS256","typ":"JWT"} 또는 PS256
System.out.printf(" 페이로드: %s%n", payload);
System.out.printf(" 서명 : %s%n", parts[2]);
System.out.println();
}
private static String signaturePart(String jwt) {
return jwt.split("\\.")[2];
}
/** Base64URL 패딩 보정 (Java 표준 디코더는 패딩 필수) */
private static String padBase64Url(String base64url) {
int mod = base64url.length() % 4;
if (mod == 2) return base64url + "==";
if (mod == 3) return base64url + "=";
return base64url;
}
private static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
if (i > 0 && i % 32 == 0) sb.append("\n ");
sb.append(String.format("%02X", bytes[i]));
}
return sb.toString();
}
}
@@ -0,0 +1,158 @@
package com.eactive.eai.common.hsm;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.File;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
/**
* SoftHSM2 마스터키 등록 유틸리티 테스트.
*
* 용도: CryptoModuleConfig.keyDerivParams hsmKeyAlias 대응하는
* AES-256 마스터키를 SoftHSM2 토큰에 등록한다.
*
* 사전 조건:
* C:\SoftHSM2 SoftHSM2 설치
* (토큰이 없으면 자동 초기화, 있으면 기존 토큰 재사용)
*
* 실행 확인:
* softhsm2-util.exe --show-slots
* 또는 pkcs11-tool --list-objects (OpenSC 설치 )
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class HsmKeyRegisterUtil {
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
private static final String TOKEN_LABEL = "eapim-test";
private static final String PIN = "1234";
/** DB CryptoModuleConfig.keyDerivParams.hsmKeyAlias 와 반드시 일치해야 한다. */
private static final String MASTER_KEY_ALIAS = "MASTER_KEY";
private static Provider provider;
private static KeyStore keyStore;
@BeforeAll
static void setUp() throws Exception {
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
assumeTrue(new File(SOFTHSM2_DLL).exists(),
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
ensureTokenInitialized();
String cfgContent = "name = SoftHSM\n"
+ "library = " + SOFTHSM2_DLL + "\n"
+ "slotListIndex = 0\n";
provider = HsmManager.createProvider(cfgContent);
Provider existing = Security.getProvider(provider.getName());
if (existing != null) Security.removeProvider(existing.getName());
Security.addProvider(provider);
keyStore = KeyStore.getInstance("PKCS11", provider);
keyStore.load(null, PIN.toCharArray());
}
// ------------------------------------------------------------------
// 1. 현재 등록된 목록 출력
// ------------------------------------------------------------------
@Test
@Order(1)
void listKeys() throws Exception {
System.out.println("=== HSM 키 목록 ===");
Enumeration<String> aliases = keyStore.aliases();
if (!aliases.hasMoreElements()) {
System.out.println(" (등록된 키 없음)");
}
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
String type = keyStore.isKeyEntry(alias) ? "KEY" : "CERT";
System.out.println(" [" + type + "] " + alias);
}
System.out.println("==================");
}
// ------------------------------------------------------------------
// 2. 마스터키 등록 (없는 경우에만)
// ------------------------------------------------------------------
@Test
@Order(2)
void registerMasterKey() throws Exception {
if (keyStore.containsAlias(MASTER_KEY_ALIAS)) {
System.out.println("[SKIP] 이미 등록됨: " + MASTER_KEY_ALIAS);
return;
}
KeyGenerator kg = KeyGenerator.getInstance("AES", provider);
kg.init(256);
SecretKey sk = kg.generateKey();
keyStore.setKeyEntry(MASTER_KEY_ALIAS, sk, null, null);
System.out.println("[OK] AES-256 마스터키 등록 완료: alias=" + MASTER_KEY_ALIAS);
}
// ------------------------------------------------------------------
// 3. 등록 조회 확인
// ------------------------------------------------------------------
@Test
@Order(3)
void verifyMasterKey() throws Exception {
keyStore.load(null, PIN.toCharArray()); // 갱신
assert keyStore.containsAlias(MASTER_KEY_ALIAS)
: "마스터키 등록 실패: " + MASTER_KEY_ALIAS;
SecretKey sk = (SecretKey) keyStore.getKey(MASTER_KEY_ALIAS, null);
assert sk != null : "키 조회 실패";
assert "AES".equals(sk.getAlgorithm()) : "알고리즘 불일치: " + sk.getAlgorithm();
System.out.println("[OK] 마스터키 조회 성공: alias=" + MASTER_KEY_ALIAS
+ " / algorithm=" + sk.getAlgorithm());
}
// ------------------------------------------------------------------
// 헬퍼
// ------------------------------------------------------------------
private static void ensureTokenInitialized() throws Exception {
ProcessBuilder pb = new ProcessBuilder(
SOFTHSM2_UTIL, "--init-token", "--free",
"--label", TOKEN_LABEL,
"--pin", PIN,
"--so-pin", PIN);
pb.redirectErrorStream(true);
Process p = pb.start();
String out = readOutput(p);
p.waitFor(10, TimeUnit.SECONDS);
System.out.println("[SoftHSM2] init-token: " + out.trim());
}
private static String readOutput(Process p) throws Exception {
InputStream is = p.getInputStream();
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int len;
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len, "UTF-8"));
}
return sb.toString();
}
}
@@ -1,190 +0,0 @@
package com.eactive.eai.common.inflow.dual;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.common.inflow.InflowTargetVO;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.local.LocalBucket;
/**
* ClientDualInflowControlManager 클라이언트 메트릭 단위 테스트.
*/
class ClientDualInflowControlManagerMetricsTest {
private ClientDualInflowControlManager manager;
@BeforeEach
void setUp() {
manager = new ClientDualInflowControlManager();
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
private InflowTargetVO makeTargetVO(String name) {
InflowTargetVO vo = new InflowTargetVO();
vo.setName(name);
vo.setThresholdPerSecond(100);
vo.setThreshold(10000);
vo.setThresholdTimeUnit("DAY");
vo.setActivate(true);
return vo;
}
private LocalBucket stableBucket(long capacity) {
return Bucket4j.builder()
.addLimit(Bandwidth.simple(capacity, Duration.ofHours(1)))
.build();
}
private LocalBucket exhaustedBucket(long capacity) {
LocalBucket b = stableBucket(capacity);
b.tryConsume(capacity);
return b;
}
private DualCustomBucket passDualBucket(String name) {
return new DualCustomBucket(stableBucket(100), null, makeTargetVO(name));
}
private DualCustomBucket blockedPerSecondDualBucket(String name) {
return new DualCustomBucket(exhaustedBucket(1), null, makeTargetVO(name));
}
private DualCustomBucket blockedThresholdDualBucket(String name) {
return new DualCustomBucket(stableBucket(100), exhaustedBucket(1), makeTargetVO(name));
}
private void injectClientMap(Map<String, DualCustomBucket> map) {
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
}
// =========================================================================
// isClientPassDetail() 카운터 증가
// =========================================================================
@Test
void isClientPassDetail_bucketNotRegistered_noMetricsCreated() {
manager.isClientPassDetail("UNKNOWN");
assertNull(manager.getClientMetrics("UNKNOWN"));
}
@Test
void isClientPassDetail_pass_incrementsAllowed() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").allowed.get());
}
@Test
void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", blockedThresholdDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
}
@Test
void isClientPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", blockedPerSecondDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").rejectedPerSecond.get());
}
// =========================================================================
// getAllClientMetrics 조회 불변성
// =========================================================================
@Test
void getAllClientMetrics_empty_returnsEmptyMap() {
assertTrue(manager.getAllClientMetrics().isEmpty());
}
@Test
void getAllClientMetrics_afterCalls_containsEntries() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
map.put("C2", passDualBucket("C2"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.isClientPassDetail("C2");
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllClientMetrics();
assertEquals(2, all.size());
assertTrue(all.containsKey("C1"));
assertTrue(all.containsKey("C2"));
}
// =========================================================================
// resetClientMetrics / resetAllClientMetrics
// =========================================================================
@Test
void resetClientMetrics_resetsTargetClient() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.resetClientMetrics("C1");
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
}
@Test
void resetAllClientMetrics_resetsAll() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
map.put("C2", passDualBucket("C2"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.isClientPassDetail("C2");
manager.resetAllClientMetrics();
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
assertEquals(0, manager.getClientMetrics("C2").allowed.get());
}
// =========================================================================
// removeClient 메트릭 자동 삭제
// =========================================================================
@Test
void removeClient_cleansUpMetrics() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertNotNull(manager.getClientMetrics("C1"));
manager.removeClient("C1");
assertNull(manager.getClientMetrics("C1"));
}
}
@@ -6,6 +6,7 @@ import static org.mockito.Mockito.*;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -52,13 +53,16 @@ class CryptoModuleServiceTest {
mockHsm = mock(HsmCryptoService.class); mockHsm = mock(HsmCryptoService.class);
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES"); SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
System.out.println("masterKey:"+new String(KEY_128));
when(mockHsm.getSecretKey(anyString())).thenReturn(masterKey); when(mockHsm.getSecretKey(anyString())).thenReturn(masterKey);
CryptoModuleConfig aes = buildStatic("AES_SVC", "AES", "CBC", "PKCS5Padding"); CryptoModuleConfig aes = buildStatic("AES_SVC", "AES", "CBC", "PKCS5Padding");
CryptoModuleConfig aria = buildStatic("ARIA_SVC", "ARIA", "CBC", "PKCS5Padding"); CryptoModuleConfig aria = buildStatic("ARIA_SVC", "ARIA", "CBC", "PKCS5Padding");
CryptoModuleConfig ecb = buildStaticNoIv("AES_ECB_SVC", "AES", "ECB", "NoPadding"); CryptoModuleConfig ecb = buildStaticNoIv("AES_ECB_SVC", "AES", "ECB", "NoPadding");
CryptoModuleConfig dyn = buildDynamic("DYN_SVC", "AES", "CBC", "PKCS5Padding"); CryptoModuleConfig dyn = buildDynamic("DYN_SVC", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(aes, aria, ecb, dyn)); CryptoModuleConfig gcm = buildStatic("AES_GCM_SVC", "AES", "GCM", "NoPadding");
CryptoModuleConfig gcmDyn = buildDynamic("DYN_GCM_SVC", "AES", "GCM", "NoPadding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(aes, aria, ecb, dyn, gcm, gcmDyn));
Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor(); Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
ctor.setAccessible(true); ctor.setAccessible(true);
@@ -139,17 +143,26 @@ class CryptoModuleServiceTest {
Map<String, String> runtimeCtx = new HashMap<>(); Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001"); runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
byte[] plain = "DYNAMIC 키 서비스 테스트".getBytes(StandardCharsets.UTF_8); byte[] plain = "{\"test\": \"DYNAMIC AES/CBC 테스트\"}".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, plain); byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, plain);
byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, encrypted); byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, encrypted);
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
System.out.println("DYNAMIC AES/CBC");
System.out.println("plain:"+new String(plain));
System.out.println("encBase64:"+encBase64);
System.out.println("decBase64:"+decBase64);
System.out.println("plain:"+new String(decrypted));
assertArrayEquals(plain, decrypted); assertArrayEquals(plain, decrypted);
} }
@Test @Test
@DisplayName("2-2. DYNAMIC — 다른 runtimeContext 값으로 복호화 시 실패") @DisplayName("2-2. DYNAMIC (HsmKeyDerivationStrategy) — runtimeContext는 키 도출에 미사용, 다른 컨텍스트로도 복호화 성공")
void testDynamic_differentContext_decryptFails() throws Exception { void testDynamic_differentContext_decryptSucceeds() throws Exception {
Map<String, String> encCtx = new HashMap<>(); Map<String, String> encCtx = new HashMap<>();
encCtx.put("X-Api-Enc-Key", "encryptKeyValue0"); encCtx.put("X-Api-Enc-Key", "encryptKeyValue0");
@@ -158,10 +171,10 @@ class CryptoModuleServiceTest {
byte[] plain = "키불일치 테스트".getBytes(StandardCharsets.UTF_8); byte[] plain = "키불일치 테스트".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("DYN_SVC", encCtx, plain); byte[] encrypted = service.encrypt("DYN_SVC", encCtx, plain);
byte[] decrypted = service.decrypt("DYN_SVC", decCtx, encrypted);
assertThrows(Exception.class, assertArrayEquals(plain, decrypted,
() -> service.decrypt("DYN_SVC", decCtx, encrypted), "HsmKeyDerivationStrategy는 runtimeContext를 사용하지 않으므로 컨텍스트가 달라도 복호화 성공");
"다른 컨텍스트 키로 복호화 시 예외가 발생해야 한다");
} }
// ========================================================================= // =========================================================================
@@ -200,6 +213,155 @@ class CryptoModuleServiceTest {
"NoPadding 모드에서 블록 크기 미정렬 평문은 예외가 발생해야 한다"); "NoPadding 모드에서 블록 크기 미정렬 평문은 예외가 발생해야 한다");
} }
// =========================================================================
// 4. STATIC AAD encrypt(name, aad, plain) / decrypt(name, aad, cipher)
// =========================================================================
@Test
@DisplayName("4-1. STATIC AES/GCM — AAD 명시 encrypt → decrypt 라운드트립")
void testStaticGcm_explicitAad_roundTrip() throws Exception {
byte[] aad = "request-header-ctx".getBytes(StandardCharsets.UTF_8);
byte[] plain = "GCM AAD 서비스 테스트".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
byte[] decrypted = service.decrypt("AES_GCM_SVC", aad, encrypted);
assertArrayEquals(plain, decrypted);
}
@Test
@DisplayName("4-2. STATIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
void testStaticGcm_nullAad_usesIvAsAad() throws Exception {
byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("AES_GCM_SVC", (byte[]) null, plain);
byte[] decrypted = service.decrypt("AES_GCM_SVC", (byte[]) null, encrypted);
assertArrayEquals(plain, decrypted);
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
System.out.println("STATIC AES/GCM");
System.out.println("plain:"+new String(plain));
System.out.println("encBase64:"+encBase64);
System.out.println("decBase64:"+decBase64);
System.out.println("plain:"+new String(decrypted));
}
@Test
@DisplayName("4-3. STATIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
void testStaticGcm_wrongAad_throwsAuthException() throws Exception {
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
byte[] plain = "GCM 인증 실패 테스트".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
assertThrows(Exception.class,
() -> service.decrypt("AES_GCM_SVC", wrongAad, encrypted),
"잘못된 AAD로 GCM 복호화 시 인증 실패 예외가 발생해야 한다");
}
@Test
@DisplayName("4-4. STATIC CBC — AAD 파라미터는 무시되고 정상 복호화")
void testStaticCbc_aadIgnored_normalDecrypt() throws Exception {
byte[] aad = "ignored-aad".getBytes(StandardCharsets.UTF_8);
byte[] plain = "CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("AES_SVC", aad, plain);
byte[] decrypted = service.decrypt("AES_SVC", aad, encrypted);
assertArrayEquals(plain, decrypted, "CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
}
// =========================================================================
// 5. DYNAMIC AAD encrypt(name, ctx, aad, plain) / decrypt(name, ctx, aad, cipher)
// =========================================================================
@Test
@DisplayName("5-1. DYNAMIC AES/GCM — runtimeContext + AAD 명시 encrypt → decrypt 라운드트립")
void testDynamicGcm_explicitAad_roundTrip() throws Exception {
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
byte[] aad = "dynamic-gcm-aad-ctx".getBytes(StandardCharsets.UTF_8);
byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, aad, encrypted);
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
System.out.println("DYNAMIC AES/GCM");
System.out.println("aad:"+new String(aad));
System.out.println("plain:"+new String(plain));
System.out.println("encBase64:"+encBase64);
System.out.println("decBase64:"+decBase64);
System.out.println("plain:"+new String(decrypted));
assertArrayEquals(plain, decrypted);
}
@Test
@DisplayName("5-2. DYNAMIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
void testDynamicGcm_nullAad_usesIvAsAad() throws Exception {
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
byte[] plain = "{\"test\": \"테스트\"}".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, null, plain);
byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, null, encrypted);
String encBase64 = Base64.getEncoder().encodeToString(encrypted);
String decBase64 = Base64.getEncoder().encodeToString(decrypted);
System.out.println("DYNAMIC AES/GCM");
System.out.println("aad:null");
System.out.println("plain:"+new String(plain));
System.out.println("encBase64:"+encBase64);
System.out.println("decBase64:"+decBase64);
System.out.println("plain:"+new String(decrypted));
assertArrayEquals(plain, decrypted);
}
@Test
@DisplayName("5-3. DYNAMIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
void testDynamicGcm_wrongAad_throwsAuthException() throws Exception {
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
byte[] plain = "DYNAMIC GCM 인증 실패".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
assertThrows(Exception.class,
() -> service.decrypt("DYN_GCM_SVC", runtimeCtx, wrongAad, encrypted),
"잘못된 AAD로 DYNAMIC GCM 복호화 시 인증 실패 예외가 발생해야 한다");
}
@Test
@DisplayName("5-4. DYNAMIC CBC — AAD 파라미터는 무시되고 정상 복호화")
void testDynamicCbc_aadIgnored_normalDecrypt() throws Exception {
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
byte[] aad = "ignored-aad".getBytes(StandardCharsets.UTF_8);
byte[] plain = "DYNAMIC CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);
byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, aad, plain);
byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, aad, encrypted);
assertArrayEquals(plain, decrypted, "DYNAMIC CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
}
// ========================================================================= // =========================================================================
// 헬퍼 // 헬퍼
// ========================================================================= // =========================================================================
@@ -246,8 +408,8 @@ class CryptoModuleServiceTest {
c.setPadding(padding); c.setPadding(padding);
c.setIvHex(IV_HEX); c.setIvHex(IV_HEX);
c.setKeySourceType("DYNAMIC"); c.setKeySourceType("DYNAMIC");
c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy"); c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy");
c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\",\"contextKey\":\"X-Api-Enc-Key\",\"offset\":\"0\",\"length\":\"16\"}"); c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\"}");
c.setCacheYn("Y"); c.setCacheYn("Y");
c.setCacheTtlSec(300); c.setCacheTtlSec(300);
c.setUseYn("Y"); c.setUseYn("Y");
@@ -36,4 +36,29 @@ class DerivedKeyTest {
assertSame(dk.getEncKey(), dk.getDecKey()); assertSame(dk.getEncKey(), dk.getDecKey());
} }
@Test
@DisplayName("1-4. 2-arg 생성자 — iv는 null")
void testTwoArgConstructor_ivIsNull() {
DerivedKey dk = new DerivedKey(new byte[]{1}, new byte[]{2});
assertNull(dk.getIv(), "2-arg 생성자로 생성 시 iv는 null이어야 한다");
}
@Test
@DisplayName("1-5. 3-arg 생성자 — iv 반환")
void testThreeArgConstructor_ivReturned() {
byte[] iv = {0x01, 0x02, 0x03};
DerivedKey dk = new DerivedKey(new byte[]{1}, new byte[]{2}, iv);
assertArrayEquals(iv, dk.getIv());
}
@Test
@DisplayName("1-6. 3-arg 생성자 — iv null 허용")
void testThreeArgConstructor_nullIv() {
DerivedKey dk = new DerivedKey(new byte[]{1}, new byte[]{2}, null);
assertNull(dk.getIv());
}
} }
@@ -0,0 +1,179 @@
package com.eactive.eai.common.security.keyderiv;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HsmContextSha256KeyDerivationStrategy 단위 테스트
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class HsmContextSha256KeyDerivationStrategyTest {
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
private static final byte[] MASTER_KEY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
private static GenericApplicationContext ctx;
private static HsmCryptoService mockHsmCryptoService;
private static HsmContextSha256KeyDerivationStrategy strategy;
private Map<String, String> params;
private Map<String, String> runtimeContext;
@BeforeAll
static void setUpClass() throws Exception {
mockHsmCryptoService = mock(HsmCryptoService.class);
SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
strategy = new HsmContextSha256KeyDerivationStrategy();
}
@BeforeEach
void setUpParams() {
params = new HashMap<>();
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
params.put("contextKey", "X-Api-Group-Seq");
runtimeContext = new HashMap<>();
runtimeContext.put("X-Api-Group-Seq", "GROUP_001");
}
// =========================================================================
// 1. deriveKey
// =========================================================================
@Test
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, 길이 32바이트(SHA-256 고정 출력)")
void testDeriveKey_validParams_returns32ByteKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertNotNull(dk);
assertNotNull(dk.getEncKey());
assertNotNull(dk.getDecKey());
assertEquals(32, dk.getEncKey().length, "SHA-256 출력은 항상 32바이트여야 한다");
}
@Test
@DisplayName("1-2. SHA-256(masterKey || contextValue) 계산 결과 검증")
void testDeriveKey_sha256ResultIsCorrect() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
byte[] contextBytes = "GROUP_001".getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[MASTER_KEY.length + contextBytes.length];
System.arraycopy(MASTER_KEY, 0, combined, 0, MASTER_KEY.length);
System.arraycopy(contextBytes, 0, combined, MASTER_KEY.length, contextBytes.length);
byte[] expected = MessageDigest.getInstance("SHA-256").digest(combined);
assertArrayEquals(expected, dk.getEncKey(), "SHA-256 해싱 결과가 일치해야 한다");
}
@Test
@DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
}
@Test
@DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리")
void testDeriveKey_missingContextValue_emptyStringUsed() throws Exception {
runtimeContext.remove("X-Api-Group-Seq");
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
byte[] expected = MessageDigest.getInstance("SHA-256").digest(MASTER_KEY);
assertArrayEquals(expected, dk.getEncKey(), "컨텍스트 값 없으면 masterKey만 해싱해야 한다");
}
@Test
@DisplayName("1-5. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
void testDeriveKey_differentContextValue_differentKey() throws Exception {
DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);
runtimeContext.put("X-Api-Group-Seq", "GROUP_002");
DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);
assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
"컨텍스트 값이 다르면 도출된 키도 달라야 한다");
}
@Test
@DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
void testDeriveKey_missingHsmKeyAlias_throwsException() {
params.remove("hsmKeyAlias");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext),
"hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
}
@Test
@DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
void testDeriveKey_missingContextKey_throwsException() {
params.remove("contextKey");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext),
"contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
}
// =========================================================================
// 2. buildCacheKey
// =========================================================================
@Test
@DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
void testBuildCacheKey_format() {
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
assertEquals("CRYPTO_A:GROUP_001", cacheKey);
}
@Test
@DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
void testBuildCacheKey_missingContextValue_emptyString() {
runtimeContext.remove("X-Api-Group-Seq");
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
assertEquals("CRYPTO_A:", cacheKey);
}
@Test
@DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
void testBuildCacheKey_differentCryptoName_differentKey() {
String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
assertNotEquals(key1, key2);
}
}
@@ -0,0 +1,149 @@
package com.eactive.eai.common.security.keyderiv;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.strategy.HsmKeyAndIvDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HsmKeyAndIvDerivationStrategy 단위 테스트
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class HsmKeyAndIvDerivationStrategyTest {
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
private static final String HSM_IV_ALIAS = "MASTER_IV_AES";
private static final byte[] MASTER_KEY = new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,
0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10};
private static final byte[] MASTER_IV = new byte[]{0x10,0x20,0x30,0x40,0x50,0x60,0x70,(byte)0x80,
0x11,0x21,0x31,0x41,0x51,0x61,0x71,(byte)0x81};
private static GenericApplicationContext ctx;
private static HsmCryptoService mockHsmCryptoService;
private static HsmKeyAndIvDerivationStrategy strategy;
private Map<String, String> params;
private Map<String, String> runtimeContext;
@BeforeAll
static void setUpClass() throws Exception {
mockHsmCryptoService = mock(HsmCryptoService.class);
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS))
.thenReturn(new SecretKeySpec(MASTER_KEY, "AES"));
when(mockHsmCryptoService.getSecretKey(HSM_IV_ALIAS))
.thenReturn(new SecretKeySpec(MASTER_IV, "AES"));
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
strategy = new HsmKeyAndIvDerivationStrategy();
}
@BeforeEach
void setUpParams() {
params = new HashMap<>();
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
params.put("hsmIvAlias", HSM_IV_ALIAS);
runtimeContext = new HashMap<>();
}
// =========================================================================
// 1. deriveKey
// =========================================================================
@Test
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, key와 iv 모두 non-null")
void testDeriveKey_validParams_returnsKeyAndIv() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertNotNull(dk);
assertNotNull(dk.getEncKey(), "encKey는 null이 아니어야 한다");
assertNotNull(dk.getDecKey(), "decKey는 null이 아니어야 한다");
assertNotNull(dk.getIv(), "iv는 null이 아니어야 한다");
}
@Test
@DisplayName("1-2. encKey == decKey (대칭 방식)")
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(dk.getEncKey(), dk.getDecKey());
}
@Test
@DisplayName("1-3. encKey는 hsmKeyAlias에 해당하는 HSM 키")
void testDeriveKey_encKeyMatchesMasterKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(MASTER_KEY, dk.getEncKey(), "encKey는 hsmKeyAlias로 가져온 마스터키여야 한다");
}
@Test
@DisplayName("1-4. iv는 hsmIvAlias에 해당하는 HSM 키")
void testDeriveKey_ivMatchesMasterIv() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(MASTER_IV, dk.getIv(), "iv는 hsmIvAlias로 가져온 HSM 값이어야 한다");
}
@Test
@DisplayName("1-5. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
void testDeriveKey_missingHsmKeyAlias_throwsException() {
params.remove("hsmKeyAlias");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext));
}
@Test
@DisplayName("1-6. 필수 파라미터 누락(hsmIvAlias) — IllegalArgumentException")
void testDeriveKey_missingHsmIvAlias_throwsException() {
params.remove("hsmIvAlias");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext));
}
// =========================================================================
// 2. buildCacheKey
// =========================================================================
@Test
@DisplayName("2-1. buildCacheKey — cryptoName 단독 반환")
void testBuildCacheKey_returnsCryptoName() {
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
assertEquals("CRYPTO_A", cacheKey);
}
@Test
@DisplayName("2-2. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
void testBuildCacheKey_differentCryptoName_differentKey() {
String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
assertNotEquals(key1, key2);
}
}