Compare commits

..

110 Commits

Author SHA1 Message Date
Rinjae bc9a4caf4a generatedJavaDir 중복 등록 방지 코드 추가 - APT 재생성 오류 해결 2026-08-27 16:32:41 +09:00
curry772 82b7c779f1 feat : template 기반 AdapterErrorMessageHandler 내부오류코드별 템플릿 지정 추가
- {in 어댑터그룹명}.sys.{rspErrCd}.template 키 사용
2026-08-26 13:48:05 +09:00
curry772 65bd1ff005 거래로그의 오류 출력 부분 수정
- 수신시스템 오류응답 수신 시(RECEAIINA001) 성공건으로 변경
- 유량제어(RECEAIIRP072), 거래차단(RECEAIIRP070) 건은 오류건으로 변경
2026-08-26 11:04:33 +09:00
curry772 e9fbffa87e 거래로그의 오류 출력 부분 수정
- 오류 발생 시 AdapterErrorMessageHandler가 처리하여도 400 오류로 출력되도록 수정
- 업무 오류 발생 시 AdapterErrorMessageHandler가 처리하여도 에러 RECEAIINA001 출력
2026-08-26 09:47:02 +09:00
curry772 585a9e4520 응답 변환 시 한글 깨지는 문제
- 원인은 HTTPProcess에서 어댑터 문자셋에 따라 무조건 charset 마추어 bytes로 변경하는 문제
2026-08-18 15:36:22 +09:00
curry772 e3419e4d43 가변 url 표준전문헤더값 추출 기능 추가
- stdHeaderGroup. prefix 사용
2026-08-18 10:06:55 +09:00
curry772 f11d27e05e damo-manager.jar 배포본 내 WEB-INF/lib로 이동 2026-08-15 11:17:10 +09:00
curry772 2865c6bd03 헤더내 환경구분코드 운영 system mode 시 'P' 설정하도록 수정 2026-08-14 15:20:26 +09:00
curry772 cc016d9d81 ReflectHeaderFilter 필터 추가 2026-08-14 15:19:46 +09:00
curry772 23bdca4777 printStackTrace() 주석 처리 2026-08-13 15:07:21 +09:00
curry772 398c26537f 비동기 로그 배치 커밋 적용 2026-08-12 13:15:58 +09:00
curry772 3f75bd9b49 QueryString(parameter, formdata) 관련 Util 클래스 추가 2026-08-10 15:25:30 +09:00
curry772 51d95d8824 QueryString(parameter, formdata) 관련 Util 클래스 추가 2026-08-10 15:25:30 +09:00
curry772 5c3de18cc9 HttpAdapterServiceVirtualAccount online -> common 이동 2026-08-06 14:23:09 +09:00
curry772 a5a072fef4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/elink-online-common.git 2026-08-06 09:30:17 +09:00
curry772 078331b3b8 In 필터 개발 : 데이터부에 상태코드 값을 응답 HTTP 상태코드에 설정하는 필터 2026-08-06 09:29:54 +09:00
Rinjae 428ab1103a damo-manager.jar 라이브러리 업데이트
- eapim-admin, eapim-online, elink-portal 공통 반영
2026-08-05 17:46:54 +09:00
curry772 4813a2ef0e 거래고유번호 맨앞에 instid 추가 2026-08-03 13:29:17 +09:00
curry772 752156b86f 거래고유번호 맨앞에 instid 추가 2026-08-03 11:38:21 +09:00
curry772 5732ddf962 checkTokenRetry 메소드 refactoring 2026-07-31 14:26:45 +09:00
curry772 5ffc48c039 REST 전송 body log의 길이를 줄이는 문제 처리 2026-07-31 13:48:39 +09:00
curry772 cbb6f33ce5 HttpClient5AdapterServiceRest.checkTokenRetry private -> protected 2026-07-31 13:48:08 +09:00
curry772 44093681b7 AdapterErrorMessageHandler 프러퍼티 그룹에 default template 기능 추가
- default.template
- default.in.template
- default.sys.template
2026-07-31 09:35:48 +09:00
curry772 a24b437f5c 수신에서 표준오류응답을 수신하였는데 Inbound 어댑터에 AdapterErrorHandler 설정하였으나, 처리 후 null를
리턴하는 경우, 처리 로직 추가
2026-07-30 14:08:25 +09:00
curry772 67dba6ca4a R(응답)거래가 없어져서 소스 원복 2026-07-30 11:25:44 +09:00
curry772 f4d7ac2fad 타발응답거래(R2) 거래시 로그 출력 및 응답 안주는 문제 처리
- R2거래이어도 동기 거래이기 때문에 응답을 전달해야 함.
- 로그도 100 ~ 400을 출력해야 함.
2026-07-30 08:57:09 +09:00
curry772 6ca653aa1e oauth token scope 검증 중 통과 시킬 scope 배열 리스트 설정으로 변경
- oAuthServer pass.scope.list에 ',' 구분자로 통과시킬 scope 리스트
  설정(ex:public,oob)
2026-07-27 14:56:20 +09:00
curry772 90941bff54 DAMO 암호화 복호화 실패 시 원본 리턴되도록 수정 2026-07-27 10:43:33 +09:00
curry772 9e1f7391a0 ToolsController 개발
- base64 인코딩, damo 암복호화, version 정보
2026-07-23 11:43:59 +09:00
curry772 cb0a588a9c ToolsController(/manage/tools) Postman 컬렉션 추가
base64/hex 변환, DAMO 및 EncryptionManager 암복호화, 배포 버전 조회 API를
기존 CryptoModuleManage/SessionCacheManage 컬렉션과 동일한 스타일로 샘플링.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:10:27 +09:00
curry772 afe2040b6e SessionManager 캐시 조회 관리 API 추가 (/manage/session)
SessionManagerForIgnite/SessionManagerForEhcache가 관리하는 login,
http-login, evict-master, terminal, convert-user-id, socket,
outbound-access-token 캐시를 조회할 수 있는 관리용 API를 추가한다.
outbound-access-token 캐시는 Ignite 백엔드에서만 지원하며, Ehcache
백엔드에서는 success=false로 응답하고 500을 반환하지 않는다.
2026-07-21 15:25:27 +09:00
curry772 d13278c374 OAuth2 토큰 타입이 없을 때 Authorization 헤더에 Bearer 접두어 미부착
accessToken.getTokenType()이 비어있으면 Bearer 접두어 없이 토큰 값만
헤더에 설정하도록 setAuthHeaders를 수정하고, OAuth2AccessTokenVO의
tokenType은 BEARER_TYPE과 대소문자 무관하게 일치할 때만 상수로 정규화한다.
2026-07-21 15:01:54 +09:00
curry772 98d815e659 CryptoModuleManager 기반 암호화모듈 진단/암복호화 테스트 관리 API 추가
/manage/crypto 하위에 등록된 암호화모듈 목록/단건 조회, DYNAMIC 키
전략(KeyDerivationStrategy) 로드 상태 확인, 동적 키 캐시 키 목록 조회,
Base64 기반 암복호화 라운드트립 테스트 엔드포인트를 제공한다.
원본 암호키(encKeyHex/decKeyHex/ivHex)는 응답에 포함하지 않는다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:59:09 +09:00
curry772 c118c88dce 어댑터별 apikey 헤더명 동적 적용 2026-07-20 11:12:13 +09:00
curry772 28a4becf1c threshhold 및 threshholdpersecond 값 null 체크 추가 2026-07-20 11:10:56 +09:00
curry772 8339efc752 상태코드 출력포맷 변경 2026-07-20 11:10:21 +09:00
curry772 61c9aa9864 REST, HTTP 어댑터 재처리 기능 보완 2026-07-13 13:25:38 +09:00
curry772 a515d0f8b0 디버그 로그 필터 추가 2026-07-10 09:45:15 +09:00
curry772 b389ef2962 REST Header 로그에 송신 Body 출력할 수 있는 기능 추가 2026-07-09 16:24:42 +09:00
curry772 c15e52d205 UnkownMessageLogUtil 클래스명 오타 수정 2026-07-09 14:55:04 +09:00
curry772 2829d4362c 파라미터 메소드 생성 assignGetParam, assignRequestHeaders 파라미터 추가 2026-07-09 14:14:27 +09:00
curry772 5a9225e93e 로그 항목 수정 2026-07-09 14:08:11 +09:00
curry772 815a064cd9 제주은행 EAI와 응답 시 데이터부가 없을 경우(API GW 오류 발생), 500으로 리턴되도록 수정 2026-07-09 11:08:58 +09:00
curry772 5a943e3399 인코딩 오류 수정 2026-07-08 12:26:20 +09:00
curry772 3090f745ba weblogic 재배포 대응 2026-07-07 11:24:07 +09:00
curry772 af6b84f57d HSM 장애 대응 기능 개발 2026-07-06 09:22:38 +09:00
curry772 90633b3d26 JsonNode 사용방식으로 변경 2026-07-03 09:00:32 +09:00
curry772 822072147e 수신 시스템이 비표준일 경우, 오류 메시지를 표준메시지헤더에서 가져오지 않도록 수정 2026-07-03 08:59:06 +09:00
curry772 3527884e31 내부 ObjectMapper 추가 2026-07-03 08:57:42 +09:00
curry772 8df8a73175 hsm keystore reload 주기 설정 추가 2026-07-03 08:57:09 +09:00
curry772 5d9676b504 수신 시각이 프러퍼티에 있을 경우 그 값을 사용하도록 수정 2026-07-03 08:55:37 +09:00
curry772 40f496cfba 암복호화 기능 수정 2026-06-30 16:24:46 +09:00
curry772 efc06e6023 hsm 키 추가 2026-06-30 09:43:10 +09:00
curry772 a923ff3f9f HSM, 암호화 관련 테스트 수행 중 수정 2026-06-29 13:16:39 +09:00
curry772 ab9b86e7cf DAMO 라이브러리 업데이트 2026-06-25 10:00:33 +09:00
curry772 d78e422368 PreFilter에서 헤더복사하도록 수정 2026-06-25 09:56:23 +09:00
curry772 3cc66fcaa8 JacksonUtil 개발 2026-06-25 09:56:01 +09:00
curry772 73ed49372a ERP용 token 발급 개발 2026-06-25 09:55:27 +09:00
curry772 3574ad9e0c damo 테스트 추가 2026-06-23 09:55:50 +09:00
curry772 49eb8fbe85 API에 scope이 적용되어 있지 않은 경우, Client에 API 적용여부를 확인하도록 수정 2026-06-23 09:55:20 +09:00
curry772 253716f4b2 암복호화 적용 2026-06-23 09:54:07 +09:00
curry772 02e282c7ba Bypass REST Outbound Adapter 이동 2026-06-23 09:46:54 +09:00
curry772 180ea8cac1 거래차단 거래 503 Service unavailable throw 하도록 수정 2026-06-23 09:33:14 +09:00
curry772 2c21c691df UnkownMessageLogUtils.logUnkownMessage 로그 위치 변경 2026-06-23 09:23:17 +09:00
curry772 0da31a8de2 OAuthServer 프러퍼티 그룹 암호화 적용 2026-06-23 08:54:00 +09:00
curry772 46d88d77f2 필터 에러코드 추가 2026-06-19 18:00:31 +09:00
curry772 51967284ba api 찾지 못할 경우 FilterException throw로 변경 2026-06-19 18:00:13 +09:00
curry772 a218291748 rest outbound adapter에서 통신오류가 아닌 경우에만, adaptererrorhandler를 호출하도록 수정 2026-06-19 11:21:22 +09:00
curry772 96fae0c53e DJB 표준전문 및 오류 응답 관련 수정 2026-06-17 19:23:27 +09:00
curry772 30b04e6e19 KJB 관련 소스 이동 2026-06-17 19:23:06 +09:00
curry772 fef16d1b99 오류응답 처리 수정 2026-06-17 13:54:08 +09:00
curry772 cf8756053e getTobeDesc 메소드 추가 2026-06-17 13:52:38 +09:00
curry772 3fcc112113 kjb 필터 이동 2026-06-17 09:27:13 +09:00
curry772 b85301763f kjb 필터 이동 2026-06-17 09:27:00 +09:00
curry772 3ebe90d595 표준전문헤더부 desc에 설정할 TobeDesc 메소드 추가 2026-06-17 09:26:38 +09:00
curry772 bf03a97fa3 오류코드 메시지를 프로퍼티 그룹에 등록하여 매핑 치환하는 AdapterErrorMsgHandler 개발
- AdapterErrorMessageHandler{CODE_CONVERT} 에 등록
- TemplateCodeConvertAdapterErrorMsgHandler json만 지원하는 handler 개발
2026-06-15 14:45:18 +09:00
curry772 756523c5c3 HSM 저장 키 디버그 로그 추가 2026-06-14 12:00:05 +09:00
curry772 ec48226671 BIZ_DATA 배열내에 있는 경우 못찾는 문제 처리 2026-06-14 11:56:05 +09:00
curry772 44e3f686a4 outbound target url - path variable 지원
- {var1}/aaa/{httpHeaderGroup.var2} depth 있는 경우도 지원도되록 변경
2026-06-14 11:55:41 +09:00
curry772 5551affe3e 오류 처리 로직 추가
- 역방향(당발) 거래 시 circuitbreaker 오류에 대해 유량제어 오류가 발생하도록 수정
- Adapter Error handler에 Outbound Process(RestProcess)에서 호출 할 수 있는 메소드
추가
2026-06-12 09:18:24 +09:00
curry772 a616cd6334 불필요한 로직 제거 및 json 처리 메소드 이동 2026-06-12 09:14:28 +09:00
curry772 05a456b461 afterFoundEaiMessage 함수 추가 2026-06-12 09:03:56 +09:00
curry772 056583b542 필요없는 로직 제거(중복 호출)
- EAIMessage setRspErr 호출되면 coordinateSetStandardMessageError 호출됨
2026-06-12 09:03:33 +09:00
curry772 2ed893e70d 표준 HTTP inbound adapter 응답 content type을 요청과 동일하게 리턴되도록 수정 2026-06-12 08:57:10 +09:00
curry772 23eaf725b6 표준 전문의 BIZ_DATA(개별부) path를 mapping config properties 파일 설정으로 변경
- standard-message-mapping-config-djb.properties 참고
- 배열 형태의 경우, path를 정상적으로 적용 못하는 문제 우회
2026-06-12 08:55:42 +09:00
curry772 2354af516c eaiSvcCd를 표준헤더 생성시 StandardMessage 객체내에 저장하도록 변경
- 비표준은 이 값을 이용(표준은 전문에서 추출)
2026-06-12 08:52:24 +09:00
curry772 209c1ba15d 파라미터 타입 변경 2026-06-12 08:48:32 +09:00
curry772 895d346bb6 표준전문이 JSON MS949인 경우, 한글 깨짐 처리 2026-06-10 15:10:33 +09:00
curry772 940e5bb90c EAIMessage에 callProp를 추가하고, 동기 거래에 대한 Outbound에서 세팅한 callProp가
RequestProcessor, EAIMessage를 통해 접근 가능하도록 변경
2026-06-10 08:53:39 +09:00
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
163 changed files with 16055 additions and 4046 deletions
+15 -3
View File
@@ -25,7 +25,8 @@ java {
compileJava { compileJava {
options.encoding = 'UTF-8' options.encoding = 'UTF-8'
sourceSets.main.java { srcDir generatedJavaDir } // generatedJavaDir 는 아래 generatedSourceOutputDirectory 로 APT 가 이미 컴파일함.
// srcDir 로도 등록하면 낡은 생성물이 입력소스가 돼 APT 재생성 시 duplicate class 발생 → 등록 금지.
options.generatedSourceOutputDirectory = project.file(generatedJavaDir) options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
aptOptions { aptOptions {
@@ -50,8 +51,16 @@ dependencies {
//implementation project(':elink-online-transformer') //implementation project(':elink-online-transformer')
api project(':elink-online-transformer') api project(':elink-online-transformer')
compileOnly fileTree(dir: 'libs', include: ['*.jar']) // damo-manager.jar 를 제외한 나머지 libs 는 기존대로 컴파일 시점에만 사용한다.
// (WAS lib 또는 다른 경로에서 런타임에 제공됨)
compileOnly fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
// damo-manager.jar: 기존에는 tomcat/lib 에 직접 넣어 런타임에 제공했으나,
// 배포 WAR(eapim-online.war)의 WEB-INF/lib 에 포함시키기 위해 런타임 의존성으로 전환한다.
// implementation 이므로 이 모듈의 compile/test classpath 와, 이 모듈을 project 의존성으로
// 참조하는 eapim-online 루트의 runtimeClasspath(= war 패키징 대상)에 함께 포함된다.
implementation files('libs/damo-manager.jar')
api (group: 'org.apache.activemq', name: 'activemq-console', version: '5.14.5'){ api (group: 'org.apache.activemq', name: 'activemq-console', version: '5.14.5'){
exclude group: 'com.fasterxml.jackson.core' exclude group: 'com.fasterxml.jackson.core'
} }
@@ -135,11 +144,14 @@ 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'
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15' testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
testImplementation 'junit:junit:4.4' testImplementation 'junit:junit:4.4'
testImplementation files('libs/kjb-safedb.jar')
} }
test { test {
Binary file not shown.
@@ -0,0 +1,391 @@
{
"info": {
"_postman_id": "b7e2e1d0-3c1a-4b0a-9d2e-crypto-manage-0001",
"name": "CryptoModuleManage API (/manage/crypto)",
"description": "CryptoModuleManager 기반 암호화모듈 진단 및 암복호화 테스트 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. cryptoName / dynamicCryptoName 을 DB(crypto_module_config)에 실제 등록된 모듈명으로 변경\n3. DYNAMIC 모듈은 keyDerivParams 의 contextKey 값과 dynamicContextKey 변수가 일치해야 함 (예: X-Api-Enc-Key)\n\n주의: 이 API는 원본 암호키(encKeyHex/decKeyHex/ivHex)를 절대 응답에 포함하지 않는다. 진단 응답은 메타데이터와 키 도출 전략(KeyDerivationStrategy) 로드 상태만 노출한다.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
{ "key": "cryptoName", "value": "SAMPLE_STATIC", "type": "string", "description": "STATIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
{ "key": "dynamicCryptoName", "value": "SAMPLE_DYNAMIC", "type": "string", "description": "DYNAMIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
{ "key": "dynamicContextKey", "value": "X-Api-Enc-Key", "type": "string", "description": "DYNAMIC 모듈의 key_deriv_params.contextKey 값과 일치해야 함" },
{ "key": "dynamicContextValue", "value": "clientKeyValue01", "type": "string" },
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
{ "key": "plainTextBase64", "value": "", "type": "string" },
{ "key": "staticCipherTextBase64", "value": "", "type": "string" },
{ "key": "dynamicCipherTextBase64", "value": "", "type": "string" }
],
"item": [
{
"name": "1. 진단 (Diagnostics)",
"item": [
{
"name": "전체 암호화모듈 목록 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/crypto/list",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "list"]
},
"description": "등록된 전체 암호화모듈의 진단정보(메타데이터)를 반환한다. 원본 키는 포함되지 않는다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));",
"pm.test('원본 키 필드가 응답에 없어야 함', () => {",
" const body = pm.response.text();",
" pm.expect(body).to.not.include('encKeyHex');",
" pm.expect(body).to.not.include('decKeyHex');",
"});"
]
}
}
]
},
{
"name": "단일 암호화모듈 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{cryptoName}}"]
},
"description": "cryptoName 하나의 진단정보를 조회한다. STATIC 모듈은 keyDerivStrategy가 null, strategyLoaded가 false로 내려온다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('cryptoName 일치', () => pm.expect(json.data.cryptoName).to.eql(pm.collectionVariables.get('cryptoName')));"
]
}
}
]
},
{
"name": "DYNAMIC 모듈 조회 (전략 로드 확인)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{dynamicCryptoName}}"]
},
"description": "DYNAMIC 모듈은 resolveStrategy를 통해 키 도출 전략(KeyDerivationStrategy) 클래스가 정상 로드되는지 strategyLoaded/strategyClassName으로 확인할 수 있다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('keySourceType = DYNAMIC', () => pm.expect(json.data.keySourceType).to.eql('DYNAMIC'));",
"pm.test('strategyLoaded = true', () => pm.expect(json.data.strategyLoaded).to.eql(true));",
"pm.test('strategyClassName 존재', () => pm.expect(json.data.strategyClassName).to.be.a('string'));"
]
}
}
]
},
{
"name": "동적 키 캐시 목록 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/crypto/cache/dynamic-keys",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "cache", "dynamic-keys"]
},
"description": "DYNAMIC 키 방식 모듈이 캐시한 키의 캐시-키 목록(예: 'cryptoName:contextValue')을 반환한다. 원본 키 값이 아니다. 아래 'DYNAMIC 키' 폴더의 암호화 요청을 먼저 실행한 뒤 호출하면 목록이 채워진다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));"
]
}
}
]
}
]
},
{
"name": "2. STATIC 키 암복호화 테스트",
"item": [
{
"name": "암호화 (encrypt)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/encrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{cryptoName}}", "test", "encrypt"]
},
"description": "STATIC 키 방식 모듈로 평문(Base64)을 암호화한다. runtimeContext 없이 호출한다."
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const plainText = pm.collectionVariables.get('plainText');",
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
"pm.test('평문 base64와 달라야 함', () => pm.expect(json.data.cipherTextBase64).to.not.eql(pm.collectionVariables.get('plainTextBase64')));",
"pm.collectionVariables.set('staticCipherTextBase64', json.data.cipherTextBase64);"
]
}
}
]
},
{
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"cipherTextBase64\": \"{{staticCipherTextBase64}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
},
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 staticCipherTextBase64를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "3. DYNAMIC 키 암복호화 테스트",
"item": [
{
"name": "암호화 (encrypt, runtimeContext 포함)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/encrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "encrypt"]
},
"description": "DYNAMIC 키 방식 모듈로 runtimeContext(예: 헤더값)를 전달하여 키를 도출한 뒤 암호화한다. dynamicContextKey는 DB에 등록된 key_deriv_params.contextKey와 일치해야 한다."
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const plainText = pm.collectionVariables.get('plainText');",
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
"pm.collectionVariables.set('dynamicCipherTextBase64', json.data.cipherTextBase64);"
]
}
}
]
},
{
"name": "복호화 (decrypt, 동일 runtimeContext 필요)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
},
"description": "암호화 시 사용한 것과 동일한 runtimeContext를 전달해야 동일한 키가 도출되어 복호화에 성공한다. dynamicContextValue를 바꿔서 보내면 키가 달라져 복호화가 실패(예외)하는 것도 확인해볼 수 있다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "4. 에러 케이스",
"item": [
{
"name": "미등록 cryptoName 조회 → success=false",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/crypto/NOT_REGISTERED_MODULE",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "NOT_REGISTERED_MODULE"]
},
"description": "존재하지 않는 cryptoName 조회 시 HTTP 200 + success=false + message에 사유가 담겨 반환되는지 확인한다 (예외가 그대로 500으로 노출되지 않음)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
"pm.test('message에 모듈명 포함', () => pm.expect(json.message).to.include('NOT_REGISTERED_MODULE'));"
]
}
}
]
},
{
"name": "잘못된 Base64로 복호화 → success=false",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"cipherTextBase64\": \"not-valid-base64!!\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
},
"description": "Base64 형식이 아닌 문자열을 전달하면 success=false로 처리되고 서버가 500으로 죽지 않는지 확인한다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
"pm.test('message 존재', () => pm.expect(json.message).to.be.a('string'));"
]
}
}
]
},
{
"name": "다른 runtimeContext로 DYNAMIC 복호화 시도 → 실패 확인",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"differentContextValue\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
},
"description": "'3. DYNAMIC 키' 폴더의 암호화를 먼저 실행해 dynamicCipherTextBase64를 채운 뒤, 암호화 때와 다른 runtimeContext 값으로 복호화를 시도한다. 키가 달라 padding/복호화 오류로 success=false가 되는 것을 확인한다 (패딩 모드에 따라 우연히 성공할 수도 있으니 참고용)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"console.log('다른 컨텍스트 복호화 결과:', JSON.stringify(json));"
]
}
}
]
}
]
}
]
}
@@ -0,0 +1,312 @@
{
"info": {
"_postman_id": "c3f4a1e0-9b2d-4a11-8e5a-session-cache-0001",
"name": "SessionCacheManage API (/manage/session)",
"description": "SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. 서버가 Ignite 캐시 백엔드로 기동된 경우에만 outbound-access-token 캐시가 채워짐 (Ehcache 백엔드는 success=false로 응답, 정상 동작임)\n3. 모든 요청은 조회 전용(GET)이며 실제 세션/캐시 데이터를 변경하지 않는다.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" }
],
"item": [
{
"name": "1. 캐시 요약 / 마스터 인스턴스",
"item": [
{
"name": "전체 캐시 요약 (구현체별 XML)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/status",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "status"]
},
"description": "Ignite/Ehcache 구현체가 각자 형식으로 만든 전체 캐시 이름/크기 요약 텍스트(XML)를 반환한다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열(XML)', () => pm.expect(json.data).to.be.a('string'));",
"pm.test('Caches 루트 태그 포함', () => pm.expect(json.data).to.include('<Caches'));"
]
}
}
]
},
{
"name": "현재 evict master 인스턴스 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/master-inst-id",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "master-inst-id"]
},
"description": "싱글 어댑터 처리를 담당하는 현재 master 서버 인스턴스명을 반환한다. 아직 선출되지 않았으면 data가 null일 수 있다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"console.log('master instance id:', json.data);"
]
}
}
]
}
]
},
{
"name": "2. 개별 캐시 조회",
"item": [
{
"name": "로그인(WebSocket) 캐시",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/login",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "login"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('name = loginSession', () => pm.expect(json.data.name).to.eql('loginSession'));",
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));",
"pm.test('size는 keys 개수와 일치', () => pm.expect(json.data.size).to.eql(json.data.keys.length));"
]
}
}
]
},
{
"name": "HTTP 로그인 캐시",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/http-login",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "http-login"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('name = httpLogin', () => pm.expect(json.data.name).to.eql('httpLogin'));",
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
]
}
}
]
},
{
"name": "싱글 어댑터 evict-master 캐시",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/evict-master",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "evict-master"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('name = evictMasterCache', () => pm.expect(json.data.name).to.eql('evictMasterCache'));",
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
]
}
}
]
},
{
"name": "단말(ATM 등) 캐시",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/terminal",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "terminal"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('name = terminalSession', () => pm.expect(json.data.name).to.eql('terminalSession'));",
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
]
}
}
]
},
{
"name": "단말ID→사용자ID 변환 캐시",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/convert-user-id",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "convert-user-id"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('name = convertUserIdSession', () => pm.expect(json.data.name).to.eql('convertUserIdSession'));",
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
]
}
}
]
},
{
"name": "소켓 세션 캐시",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/socket",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "socket"]
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('name = adapterSession', () => pm.expect(json.data.name).to.eql('adapterSession'));",
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
]
}
}
]
},
{
"name": "아웃바운드 AccessToken 캐시 (Ignite 전용)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/outbound-access-token",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "outbound-access-token"]
},
"description": "Ignite 백엔드에서만 지원한다. Ehcache 백엔드로 기동된 서버라면 success=false + message로 응답하며, 이는 정상 동작이다(서버 500 아님)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"if (json.success) {",
" pm.test('data.name 존재', () => pm.expect(json.data.name).to.be.a('string'));",
" pm.test('data.keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));",
"} else {",
" pm.test('미지원 백엔드는 message 포함', () => pm.expect(json.message).to.be.a('string'));",
" console.log('OutboundAccessToken 캐시 미지원 백엔드:', json.message);",
"}"
]
}
}
]
}
]
},
{
"name": "3. 전체 캐시 목록",
"item": [
{
"name": "모든 캐시 한번에 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/session/cache/all",
"host": ["{{baseUrl}}"],
"path": ["manage", "session", "cache", "all"]
},
"description": "login/http-login/evict-master/terminal/convert-user-id/socket 6종은 항상 포함되고, outbound-access-token은 Ignite 백엔드일 때만 추가되어 count가 6 또는 7이 된다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));",
"pm.test('count는 6 또는 7 (백엔드에 따라 outbound-access-token 포함 여부 다름)', () => {",
" pm.expect(json.count).to.be.oneOf([6, 7]);",
"});",
"pm.test('count와 data.length 일치', () => pm.expect(json.count).to.eql(json.data.length));",
"json.data.forEach((cache) => {",
" pm.test(`캐시 [${cache.name}] 는 keys 배열을 가짐`, () => pm.expect(cache.keys).to.be.an('array'));",
"});"
]
}
}
]
}
]
}
]
}
+353
View File
@@ -0,0 +1,353 @@
{
"info": {
"_postman_id": "d4a8b2f0-5e3c-4d21-9f0a-tools-manage-0001",
"name": "ToolsManage API (/manage/tools)",
"description": "base64/hex 변환, DAMO 및 EncryptionManager 암복호화 테스트, 배포 버전 확인용 유틸리티 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. DAMO/EncryptionManager 암복호화는 각 실행 순서(암호화 → 복호화)를 지켜야 체이닝 변수가 채워짐\n3. EncryptionManager 는 encryptYN=N 환경에서는 encrypt/decrypt가 원문을 그대로 반환한다(정상 동작). 먼저 '상태 조회'로 확인할 것\n4. /manage/tools/version 은 eapim-online(WAR 루트) build.gradle의 generateVersionInfo 태스크가 생성한 version.info 리소스가 있어야 success=true로 응답한다. 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등에서는 success=false + message로 응답하며, 이는 정상 동작이다.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
{ "key": "charset", "value": "UTF-8", "type": "string" },
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
{ "key": "base64EncodedText", "value": "", "type": "string" },
{ "key": "hexEncodedText", "value": "", "type": "string" },
{ "key": "damoCipherText", "value": "", "type": "string" },
{ "key": "encManagerCipherText", "value": "", "type": "string" }
],
"item": [
{
"name": "1. Base64 / Hex 변환",
"item": [
{
"name": "문자열 → Base64 인코딩",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/base64/encode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "base64", "encode"]
},
"description": "charset(생략 시 UTF-8) 기준으로 문자열을 바이트로 변환한 뒤 Base64로 인코딩한다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.collectionVariables.set('base64EncodedText', json.data);"
]
}
}
]
},
{
"name": "Base64 → 문자열 디코딩 (위 인코딩 결과 사용)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{base64EncodedText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/base64/decode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "base64", "decode"]
},
"description": "바로 위 '문자열 → Base64 인코딩' 요청에서 저장한 base64EncodedText를 디코딩하여 원문과 일치하는지 확인한다. 반드시 인코딩 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
},
{
"name": "문자열 → Hex 인코딩",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/hex/encode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "hex", "encode"]
},
"description": "charset(생략 시 UTF-8) 기준으로 문자열을 바이트로 변환한 뒤 대문자 Hex 문자열로 인코딩한다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.collectionVariables.set('hexEncodedText', json.data);"
]
}
}
]
},
{
"name": "Hex → 문자열 디코딩 (위 인코딩 결과 사용)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{hexEncodedText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/hex/decode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "hex", "decode"]
},
"description": "바로 위 '문자열 → Hex 인코딩' 요청에서 저장한 hexEncodedText를 디코딩하여 원문과 일치하는지 확인한다. 반드시 인코딩 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "2. DAMO 암복호화 (DamoManager 직접 호출)",
"item": [
{
"name": "암호화 (encrypt)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/damo/encrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "damo", "encrypt"]
},
"description": "com.eactive.ext.djb.DamoManager를 직접 호출한다. EncryptionManager의 encryptYN 설정과 무관하게 항상 실제 암호화가 수행된다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.test('평문과 달라야 함', () => pm.expect(json.data).to.not.eql(pm.collectionVariables.get('plainText')));",
"pm.collectionVariables.set('damoCipherText', json.data);"
]
}
}
]
},
{
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{damoCipherText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/damo/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "damo", "decrypt"]
},
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 damoCipherText를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "3. EncryptionManager 암복호화 (운영 설정 반영)",
"item": [
{
"name": "현재 설정 상태 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/tools/encryption-manager/status",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "encryption-manager", "status"]
},
"description": "encryptYN/dbEncryptSolutionName/encryptEnabled를 조회한다. encryptYN=N이면 아래 encrypt/decrypt가 원문을 그대로 반환하는 것이 정상 동작이니 먼저 이 값을 확인할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('encryptYN 존재', () => pm.expect(json.data.encryptYN).to.exist);",
"pm.test('encryptEnabled는 boolean', () => pm.expect(json.data.encryptEnabled).to.be.a('boolean'));",
"console.log('EncryptionManager 상태:', JSON.stringify(json.data));"
]
}
}
]
},
{
"name": "암호화 (encrypt)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/encryption-manager/encrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "encryption-manager", "encrypt"]
},
"description": "실제 운영 코드가 사용하는 EncryptionManager.encryptDBData를 그대로 호출한다. encryptYN=N 환경에서는 원문이 그대로 반환된다(정상 동작)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.collectionVariables.set('encManagerCipherText', json.data);"
]
}
}
]
},
{
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{encManagerCipherText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/encryption-manager/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "encryption-manager", "decrypt"]
},
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 encManagerCipherText를 복호화한다. encryptYN 설정값과 무관하게 encrypt→decrypt 라운드트립 결과는 항상 원문과 같아야 한다(활성 시 실제 복호화, 비활성 시 원문 그대로 통과). 반드시 암호화 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "4. 버전 정보",
"item": [
{
"name": "배포 버전 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/tools/version",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "version"]
},
"description": "eapim-online(WAR를 만드는 루트 프로젝트) build.gradle의 generateVersionInfo 태스크가 생성한 version.info(git describe 결과, buildTime)를 반환한다. 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등 파일이 없으면 success=false + message로 응답하며, 이는 정상 동작이다(서버 500 아님)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"if (json.success) {",
" pm.test('version 존재', () => pm.expect(json.data.version).to.be.a('string'));",
" pm.test('buildTime 존재', () => pm.expect(json.data.buildTime).to.be.a('string'));",
"} else {",
" pm.test('미생성 환경은 message 포함', () => pm.expect(json.message).to.be.a('string'));",
" console.log('version.info 없음:', json.message);",
"}"
]
}
}
]
}
]
}
]
}
-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,50 @@ 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; /**
* 비표준 -> 표준 거래
* 아웃바운드 DefaultProcess에서 응답 표준헤더에 에러응답이 세팅되어 있는 경우, 비표준 응답 메시지 생성용
*/
public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception;
/**
* 비표준 -> 표준 거래
* GW 내부 오류가 발생한 경우, RequestPrcessor에서 에러 응답을 생성하는 경우, ExceptionHandler에 의해 호출, 비표준 응답 메시지 생성용
*/
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName,
String inboudnAdapterName, Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg)
throws Exception;
/**
* 비표준 -> 표준 거래
* GW RequestProcess 호출 전에 Adapter(Filter)에서 오류가 발생한 경우, ApiAdapterController에 Exception에 맞추어, 비표준 응답 메시지 생성용
*/
public Object generateNonStandardInboundErrorResponseMessage(String inboudnAdapterGroupName,
String inboudnAdapterName, Properties callProp, Object inboundRequestData, Object inboundResponseData,
Throwable e) throws Exception;
/**
* 표준 -> 비표준 거래
* 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound 어댑터에 의해서 호출하여 응답 메시지 생성
* 수신한 응답 메시지가 있을 수도 있고 없을 수도 있다.
*/
default public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
String outboundadapterName, Properties callProp, Object outboundRequestData, Object outboundResponseData,
Throwable e) throws Exception {
return null;
}
/**
* 표준 -> 비표준 거래
* 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound Process 응답 메시지 생성
*/
default public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
String outboundadapterName, Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg)
throws Exception {
return null;
}
} }
@@ -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;
} }
@@ -1,5 +1,7 @@
package com.eactive.eai.adapter.handler; package com.eactive.eai.adapter.handler;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -8,13 +10,23 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils; 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.FilterCryptoException;
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.property.PropManager;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.eactive.eai.common.util.Logger; 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.StandardItem;
import com.eactive.eai.message.StandardMessage; import com.eactive.eai.message.StandardMessage;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/** /**
* 템플릿 기반 어댑터 에러 메시지 핸들러. * 템플릿 기반 어댑터 에러 메시지 핸들러.
@@ -78,7 +90,7 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
/** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */ /** PropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
static final String PROP_GROUP = "AdapterErrorMessageHandler"; static final String PROP_GROUP = "AdapterErrorMessageHandler";
/** callProp 참조 변수 접두어: ${callprop.키} */ /** callProp 참조 변수 접두어: ${callprop.키} */
@@ -87,14 +99,22 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
/** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */ /** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */
static final String CALLPROP_INDIRECT_PREFIX = "callprop["; static final String CALLPROP_INDIRECT_PREFIX = "callprop[";
/** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */ /** 예외 객체 필드 참조 접두어: ${exception.필드명} */
static final String EXCEPTION_PREFIX = "exception.";
/** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 (renderRow 내부에서 사용) */
private static final Pattern VAR_PATTERN = private static final Pattern VAR_PATTERN =
Pattern.compile("\\$\\{([^}]+)\\}"); Pattern.compile("\\$\\{([^}]+)\\}");
/** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */ /**
private static final Pattern FOREACH_PATTERN = * foreach 블록과 스칼라 변수를 순서대로 단일 패스로 처리하는 통합 패턴.
Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}", * group(1) != null → foreach 블록: group(1)=경로, group(2)=블록 본문
Pattern.DOTALL); * group(3) != null → 스칼라 변수: group(3)=표현식
*/
private static final Pattern COMBINED_PATTERN =
Pattern.compile(
"\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}|\\$\\{([^}]+)\\}",
Pattern.DOTALL);
@Override @Override
public Object generateNonStandardErrorResponseMessage( public Object generateNonStandardErrorResponseMessage(
@@ -102,10 +122,14 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
String inboundAdapterName, String inboundAdapterName,
Properties callProp, Properties callProp,
Object outboundRequestData, Object outboundRequestData,
StandardMessage resStandardMessage) throws Exception { EAIMessage resEaiMsg) throws Exception {
String templateKey = inboundAdapterGroupName + ".template"; StandardMessage resStandardMessage = resEaiMsg.getStandardMessage();
String templateKey = inboundAdapterGroupName + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey); String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template))
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
if (StringUtils.isBlank(template)) { if (StringUtils.isBlank(template)) {
if (logger.isWarn()) { if (logger.isWarn()) {
@@ -118,55 +142,73 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
return render(template, resStandardMessage, callProp); return render(template, resStandardMessage, callProp);
} }
/** 3-인자 위임: exception 없이 호출하는 기존 경로 유지 */
String render(String template, StandardMessage msg, Properties callProp) {
return render(template, msg, callProp, null);
}
/** /**
* 템플릿에 StandardMessage callProp 값을 치환해 최종 문자열을 반환한다. * 템플릿에 StandardMessage, callProp, exception 값을 치환해 최종 문자열을 반환한다.
* foreach 블록과 스칼라 변수를 좌→우 순서로 단일 패스 처리한다.
* package-private: 단위 테스트에서 직접 호출 가능 * package-private: 단위 테스트에서 직접 호출 가능
*/ */
String render(String template, StandardMessage msg, Properties callProp) { String render(String template, StandardMessage msg, Properties callProp, Throwable exception) {
// 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리 // 관리 UI 등에서 HTML 인코딩된 엔티티를 복원 (예: &#35; → #)
StringBuffer sb = new StringBuffer(); template = template.replace("&#35;", "#")
Matcher foreachMatcher = FOREACH_PATTERN.matcher(template); .replace("&amp;", "&")
while (foreachMatcher.find()) { .replace("&lt;", "<")
String arrayPath = foreachMatcher.group(1).trim(); .replace("&gt;", ">")
String blockContent = foreachMatcher.group(2); .replace("&quot;", "\"");
String rendered = renderForeach(arrayPath, blockContent, msg);
foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered));
}
foreachMatcher.appendTail(sb);
// 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환
String intermediate = sb.toString();
StringBuffer result = new StringBuffer(); StringBuffer result = new StringBuffer();
Matcher varMatcher = VAR_PATTERN.matcher(intermediate); Matcher matcher = COMBINED_PATTERN.matcher(template);
while (varMatcher.find()) { while (matcher.find()) {
String expr = varMatcher.group(1).trim(); String replacement;
String value = resolveScalar(expr, msg, callProp); if (matcher.group(1) != null) {
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value)); // {{#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));
} }
varMatcher.appendTail(result); matcher.appendTail(result);
return result.toString(); return result.toString();
} }
/** /**
* 변수 표현식을 해석해 값을 반환한다. * 변수 표현식을 해석해 값을 반환한다.
* 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열. * 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
* - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회 * - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회
* - "callprop.키[:기본값]" → callProp 직접/중첩 조회 * - "callprop.키[:기본값]" → callProp 직접/중첩 조회
* - "경로[:기본값]" → StandardMessage.findItemValue(경로) * - "exception.필드[:기본값]" → 리플렉션으로 예외 객체 getter/필드 조회
* - "경로[:기본값]" → StandardMessage.findItemValue(경로)
*/ */
private String resolveScalar(String expr, StandardMessage msg, Properties callProp) { private String resolveScalar(String expr, StandardMessage msg, Properties callProp, Throwable exception) {
if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) { if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) {
return resolveIndirectCallProp(expr, msg, callProp); return resolveIndirectCallProp(expr, msg, callProp);
} }
if (expr.startsWith(CALLPROP_PREFIX)) { if (expr.startsWith(CALLPROP_PREFIX)) {
String rest = expr.substring(CALLPROP_PREFIX.length()); String rest = expr.substring(CALLPROP_PREFIX.length());
int colonIdx = rest.indexOf(':'); int colonIdx = rest.indexOf(':');
String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest; String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : ""; String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
if (callProp == null) return defaultVal; if (callProp == null) return defaultVal;
String value = resolveCallProp(callProp, keyPath); String value = resolveCallProp(callProp, keyPath);
return value != null ? value : defaultVal; 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(':'); int colonIdx = expr.indexOf(':');
String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr; String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : ""; String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : "";
@@ -176,13 +218,22 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
/** /**
* StandardMessage 경로를 해석한다. * StandardMessage 경로를 해석한다.
* 1. findItemValue(path) 로 직접 조회한다. * 1. 경로에 '[n]' 구문이 있으면 배열 인덱스 접근으로 처리한다.
* 2. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우 * 예) MSG.MSG_LIST[0].outp_msg_cd → MSG_LIST 첫 번째 행의 outp_msg_cd 값
* 2. findItemValue(path) 로 직접 조회한다.
* 3. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우
* 나머지 경로(subPath)를 키로 JSON 필드를 추출한다. * 나머지 경로(subPath)를 키로 JSON 필드를 추출한다.
* 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo * 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo
* DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city * DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city
*/ */
private String resolveMessagePath(StandardMessage msg, String path) { 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); String value = msg.findItemValue(path);
if (value != null) return value; if (value != null) return value;
@@ -200,6 +251,44 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
return null; 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 문자열에서 dot 구분 경로로 필드 값을 추출한다.
* 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로, * 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로,
@@ -234,6 +323,8 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
? expr.substring(closeIdx + 2) ? expr.substring(closeIdx + 2)
: ""; : "";
if (msg == null) return defaultVal;
String key = msg.findItemValue(msgPath); String key = msg.findItemValue(msgPath);
if (StringUtils.isBlank(key)) return defaultVal; if (StringUtils.isBlank(key)) return defaultVal;
if (callProp == null) return defaultVal; if (callProp == null) return defaultVal;
@@ -276,7 +367,7 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
* 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원) * 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
*/ */
private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) { private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
StandardItem arrayItem = msg.findItem(arrayPath); StandardItem arrayItem = msg.findItem(arrayPath);
if (arrayItem == null) { if (arrayItem == null) {
if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath); if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
return ""; return "";
@@ -316,4 +407,158 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
varMatcher.appendTail(result); varMatcher.appendTail(result);
return result.toString().trim(); return result.toString().trim();
} }
@Override
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception {
String template = "";
String templateKey = "";
if (resEaiMsg != null && StringUtils.isNotEmpty(resEaiMsg.getRspErrCd())) {
templateKey = inboudnAdapterGroupName + ".sys." + resEaiMsg.getRspErrCd() + ".template";
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
}
if (StringUtils.isBlank(template)) {
templateKey = inboudnAdapterGroupName + ".sys.template";
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
}
if (StringUtils.isBlank(template))
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.sys.template");
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 FilterCryptoException) {
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e, 550);
} else 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());
}
}
/**
* 표준 -> 비표준 거래
* 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound Process 응답 메시지 생성
*/
@Override
public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
String outboundadapterName, Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg)
throws Exception {
String templateKey = outboundadapterGroupName + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template))
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
if (StringUtils.isBlank(template)) {
return null;
}
return render(template, resEaiMsg.getStandardMessage(), callProp);
}
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 = "default.in." + httpCode + ".template";
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)) {
templateKey = "default.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;
}
} }
@@ -0,0 +1,126 @@
package com.eactive.eai.adapter.handler;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class TemplateCodeConvertAdapterErrorMsgHandler extends TemplateAdapterErrorMsgHandler {
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
/** PropManager 에서 코드 변환 설정을 조회할 프로퍼티 그룹 이름 */
static final String PROP_GROUP = "AdapterErrorMessageHandler{CODE_CONVERT}";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public Object generateNonStandardErrorResponseMessage(
String inboundAdapterGroupName,
String inboundAdapterName,
Properties callProp,
Object outboundRequestData,
EAIMessage resEaiMsg) throws Exception {
Object responseMsessage = super.generateNonStandardErrorResponseMessage(
inboundAdapterGroupName,
inboundAdapterName,
callProp,
outboundRequestData,
resEaiMsg);
logger.debug("generateNonStandardErrorResponseMessage - {}", responseMsessage);
if (!(responseMsessage instanceof String)) {
return responseMsessage;
}
String jsonStr = (String) responseMsessage;
if (StringUtils.isBlank(jsonStr)) {
return responseMsessage;
}
Properties props = PropManager.getInstance().getProperties(PROP_GROUP);
if (props == null) {
return responseMsessage;
}
String fieldsKey = inboundAdapterGroupName + ".convert.fields";
String fieldsValue = props.getProperty(fieldsKey);
if (StringUtils.isBlank(fieldsValue)) {
return responseMsessage;
}
JsonNode rootNode = OBJECT_MAPPER.readTree(jsonStr);
boolean modified = false;
for (String rawField : fieldsValue.split(",")) {
String fieldPath = rawField.trim();
if (StringUtils.isBlank(fieldPath)) continue;
String currentValue = getJsonValue(rootNode, fieldPath);
if (currentValue == null) continue;
// key: inboundAdapterGroupName.{fieldPath}.{현재값} → 폴백: ...{fieldPath}.default
String mappingKey = inboundAdapterGroupName + "." + fieldPath + "." + currentValue;
String newValue = props.getProperty(mappingKey);
if (newValue == null) {
String defaultKey = inboundAdapterGroupName + "." + fieldPath + ".default";
newValue = props.getProperty(defaultKey);
}
if (newValue == null) continue;
setJsonValue(rootNode, fieldPath, newValue);
modified = true;
if (logger.isDebug()) {
logger.debug("code convert: field={}, {} -> {}", fieldPath, currentValue, newValue);
}
}
if (modified) {
responseMsessage = OBJECT_MAPPER.writeValueAsString(rootNode);
}
return responseMsessage;
}
/**
* dot 구분 경로로 JsonNode 에서 값을 꺼낸다.
* 최종 노드가 값 노드(primitive)가 아니면 null 반환.
*/
private String getJsonValue(JsonNode node, String fieldPath) {
JsonNode current = node;
for (String part : fieldPath.split("\\.", -1)) {
if (current == null || !current.isObject()) return null;
current = current.get(part);
}
if (current == null || current.isNull() || !current.isValueNode()) return null;
return current.asText();
}
/**
* dot 구분 경로로 ObjectNode 의 최종 필드 값을 newValue 로 교체한다.
* 중간 경로가 ObjectNode 가 아니면 무시한다.
*/
private void setJsonValue(JsonNode rootNode, String fieldPath, String newValue) {
String[] parts = fieldPath.split("\\.", -1);
JsonNode current = rootNode;
for (int i = 0; i < parts.length - 1; i++) {
if (current == null || !current.isObject()) return;
current = current.get(parts[i]);
}
if (current instanceof ObjectNode) {
((ObjectNode) current).put(parts[parts.length - 1], newValue);
}
}
}
@@ -41,6 +41,12 @@ public class HttpStatusException extends Exception {
this.status = status; this.status = status;
} }
public HttpStatusException(String msg, String code, int status, Throwable cause) {
super(msg, cause);
this.setCode(code);
this.status = status;
}
public int getStatus() { public int getStatus() {
return status; return status;
} }
@@ -51,7 +57,7 @@ public class HttpStatusException extends Exception {
public String getCode() { public String getCode() {
if (StringUtils.isBlank(code)) { if (StringUtils.isBlank(code)) {
return String.format("Http Status: %d", status); return String.format("HttCd:%d", status);
} else { } else {
return code; return code;
} }
@@ -11,7 +11,9 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException; import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
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.regex.Matcher; import java.util.regex.Matcher;
@@ -30,6 +32,7 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.TrustAllStrategy; import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.HttpResponseInterceptor; import org.apache.hc.core5.http.HttpResponseInterceptor;
@@ -40,6 +43,7 @@ import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.Keys; import com.eactive.eai.adapter.Keys;
import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory; import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory;
import com.eactive.eai.common.TransactionContextKeys; import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.logger.HttpAdapterExtraHeaderVo;
import com.eactive.eai.common.message.EAIMessageKeys; import com.eactive.eai.common.message.EAIMessageKeys;
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;
@@ -416,8 +420,24 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
logProcessNo = 200; logProcessNo = 200;
} }
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo, Header[] headers = request.getHeaders();
contextAdapterGroupName, contextAdapterName, request.getHeaders(), url, request.getMethod()); List<HttpAdapterExtraHeaderVo> headerVoList = null;
if ( headers != null && headers.length != 0 ) {
headerVoList = HttpAdapterExtraLogUtil.convertHeaderToListOfHttpAdapterExtraHeaderVo(headers);
} else {
headerVoList = new ArrayList<HttpAdapterExtraHeaderVo>();
}
String trimedPostBody = (String) context.getAttribute(HttpAdapterExtraLogUtil.BODY_FIELD_NAME);
if(StringUtils.isNotEmpty(trimedPostBody)) {
HttpAdapterExtraHeaderVo vo = new HttpAdapterExtraHeaderVo();
vo.setName(HttpAdapterExtraLogUtil.BODY_FIELD_NAME);
vo.setValue(trimedPostBody);
headerVoList.add(vo);
}
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo,
contextAdapterGroupName, contextAdapterName, headerVoList, url, request.getMethod(), 0);
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -443,7 +463,7 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
}else{ }else{
logProcessNo += 100; logProcessNo += 100;
} }
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo, HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo,
contextAdapterGroupName, contextAdapterName, response.getHeaders(), url, request.getMethod(), response.getCode()); contextAdapterGroupName, contextAdapterName, response.getHeaders(), url, request.getMethod(), response.getCode());
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
@@ -570,8 +590,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);
}
}
@@ -88,10 +88,17 @@ public class DeadlineAwareRetryExecutor {
if (!policy.shouldRetryOnStatus(request, status)) { if (!policy.shouldRetryOnStatus(request, status)) {
// 성공 또는 재시도 불필요 → 호출부에서 close 책임 // 성공 또는 재시도 불필요 → 호출부에서 close 책임
log.debug("non-retryable response status={}, attempt={}", status, attempt + 1); log.info("non-retryable response status={}, attempt={}", status, attempt + 1);
return response; return response;
} }
// ④ 마지막 시도였으면 종료
if (attempt >= policy.getMaxRetries()) {
// 최대 재처리에 도달하여, 재시도하지 않고, 응답 객체를 리턴한다.
log.info("최대 재시도 횟수 도달 response status={}, attempt={}", status, attempt + 1);
return response;
}
// 재시도 대상 // 재시도 대상
log.warn("retryable status={}, attempt={}/{}", log.warn("retryable status={}, attempt={}/{}",
status, attempt + 1, policy.getMaxRetries()); status, attempt + 1, policy.getMaxRetries());
@@ -127,9 +134,6 @@ public class DeadlineAwareRetryExecutor {
} }
} }
// ④ 마지막 시도였으면 바로 종료
if (attempt >= policy.getMaxRetries()) break;
// ⑤ 백오프 대기 전 남은 시간 재확인 // ⑤ 백오프 대기 전 남은 시간 재확인
long remainingBeforeWait = totalDeadlineMs - (System.currentTimeMillis() - startTime); long remainingBeforeWait = totalDeadlineMs - (System.currentTimeMillis() - startTime);
if (remainingBeforeWait <= policy.getBackoffMs()) { if (remainingBeforeWait <= policy.getBackoffMs()) {
@@ -171,10 +175,6 @@ public class DeadlineAwareRetryExecutor {
log.info("RequestConfig = {}", config); log.info("RequestConfig = {}", config);
} }
private boolean isSuccess(int status) {
return status >= 200 && status < 300;
}
private void closeQuietly(CloseableHttpResponse response) { private void closeQuietly(CloseableHttpResponse response) {
try { try {
response.close(); response.close();
@@ -14,6 +14,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch; import org.apache.commons.lang3.time.StopWatch;
import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpHost;
@@ -135,7 +136,15 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
byte[] responseMessage = null; byte[] responseMessage = null;
String responseString = ""; String responseString = "";
try (CloseableHttpResponse response = (CloseableHttpResponse) this.client.execute(method, context)) {
DeadlineAwareRetryExecutor executor =
new DeadlineAwareRetryExecutor((CloseableHttpClient)this.client,
vo.getConnectionTimeout(),
vo.getTimeout());
// Properties에서 RetryPolicy 로드
RetryPolicy policy = RetryPolicy.from(prop); // 설정 로드 방식에 맞게
try (CloseableHttpResponse response = (CloseableHttpResponse) executor.execute(method, context, policy)) {
status = response.getCode(); status = response.getCode();
responseMessage = EntityUtils.toByteArray(response.getEntity()); responseMessage = EntityUtils.toByteArray(response.getEntity());
responseString = new String(responseMessage, vo.getEncode()); responseString = new String(responseMessage, vo.getEncode());
@@ -177,7 +186,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) {
@@ -0,0 +1,679 @@
package com.eactive.eai.adapter.http.client.impl;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Formatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.protocol.BasicHttpContext;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import com.eactive.eai.adapter.http.HttpMemoryLogger;
import com.eactive.eai.adapter.http.HttpMethodType;
import com.eactive.eai.adapter.http.client.HttpClient5AdapterServiceSupport;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.impl.HttpAdapterServiceBypass;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.util.CommonLib;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.openbanking.eai.common.token.AccessTokenManager;
import com.openbanking.eai.common.token.AccessTokenVO;
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
public class HttpClient5AdapterServiceBypass extends HttpClient5AdapterServiceSupport
implements HttpClientAdapterServiceKey {
public static final String TYPE_BYPASS_REQUEST = "bypassRequest";
public static final String REST_OPTION = "REST_OPTION";
protected boolean doSendUrlFragment = true;
protected boolean doHandleCompression = false;
protected boolean doForwardIP = false;
@SuppressWarnings({ "unchecked" })
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
HttpClientAdapterVO vo = super.setting(prop, tempProp);
boolean useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
// ex) $.dataHeader.GW_RSLT_CD
String tokenErrorCodeKey = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_KEY");
String tokenErrorCodeValues = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_VALUES");
String tokenErrorHttpStatusCode = prop.getProperty("ADAPTER_TOKEN_ERROR_HTTP_STATUS_CODE"); // 200 or 400번대
/**
* @formatter:off
* TSEAIHE02.RESTOPTION 정보 => JSON 형태로 구성
* - type : simpleRequest, variableUrlRequest
* - extraPath : 어댑터 프로퍼티 URL에 추가될 HTTP REST URL
* - method : get, delete, post, put
* - contentType : application/json, application/x-www-form-urlencoded
* - adapterTokenUseYn: Y, N(default)
* ex)
* {"type":"variableUrlRequest","extraPath":"/aaa/{userId}","uriVariables":["userId"]}
* @formatter:on
*/
String restOptionData = tempProp.getProperty(REST_OPTION, "{}");
JSONObject restOptionObject = parseJson(restOptionData);
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
}
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
String inboundRewritePath = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_REWRITE_PATH, "");
String inboundQueryString = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_QUERY_STRING, "");
Properties inboundHeaders = (Properties) tempProp.get(HttpAdapterServiceKey.INBOUND_HEADER);
Map<String, String> inboundPathVariables = (Map<String, String>) tempProp
.get(HttpAdapterServiceKey.INBOUND_PATH_VARIABLES);
String restMethod = (String) restOptionObject.get("method");
if (StringUtils.isBlank(restMethod)) {
restMethod = inboundMethod;
}
String url = getRewriteUrl(vo, inboundRewritePath, inboundQueryString, restOptionObject, inboundPathVariables);
if (logger.isDebug()) {
logger.debug("HttpClient5AdapterServiceBypass] url = [" + url + "]");
}
HttpClient mclient = this.client;
HttpUriRequestBase method = generateMethod(restMethod, url);
assignRequestHeaders(method, inboundHeaders, tempProp);
if (hasBody(data, method)) {
byte[] bodyBytes;
if (data instanceof byte[]) {
bodyBytes = (byte[]) data;
} else if (data instanceof String) {
bodyBytes = ((String) data).getBytes(vo.getEncode());
} else {
bodyBytes = new byte[0];
}
method.setEntity(new ByteArrayEntity(bodyBytes, null));
}
String contentType = (String) restOptionObject.get("contentType");
if (StringUtils.isBlank(contentType) && inboundHeaders != null) {
contentType = getIgnoreCaseProp(inboundHeaders, HttpHeaders.CONTENT_TYPE);
contentType = StringUtils.substringBefore(contentType, ";");
}
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
configureHttpClient(requestConfigBuilder, vo, method);
OAuth2AccessTokenVO accessToken = null;
if (useAdapterToken) {
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
accessToken = (OAuth2AccessTokenVO) tokenManager.getAccessTokenVO(vo.getAdapterGroupName());
// 토큰이 없거나 만료 됐으면 재발급
if (accessToken == null || accessToken.isExpired()) {
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
accessToken = (OAuth2AccessTokenVO) tokenManager.retryAccessTokenVO(vo.getAdapterGroupName(), prop,
oldToken);
}
if (logger.isDebug()) {
logger.debug("HttpClient5AdapterServiceBypass] SEND (" + vo.getAdapterGroupName() + ") TOKEN = ["
+ accessToken + "]");
}
setAuthHeaders(method, accessToken);
if (logger.isDebug()) {
logger.debug("HttpClient5AdapterServiceBypass] SEND (" + vo.getAdapterGroupName()
+ ") RequestHeader [Authorization=" + method.getFirstHeader("Authorization") + "]");
}
}
// 로깅 인터셉터에 전달할 컨텍스트 정보 설정
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
HttpContext context = new BasicHttpContext();
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME, vo.getAdapterGroupName());
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_NAME, vo.getAdapterName());
Integer logProcessNo = (Integer) tempProp.get(HttpClientAdapterServiceKey.LOG_PROCESS_NO);
if (logProcessNo != null && logProcessNo > 0) {
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
}
int status = -1;
try {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
byte[] responseMessage = null;
Header[] responseHeaders = null;
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
"SEND [Bypass Request..]" + CommonLib.getDumpMessage(data));
}
try {
if (logger.isDebug()) {
logger.debug("[method getName]" + method.getMethod());
logger.debug("[method getRequestHeaders]" + java.util.Arrays.toString(method.getHeaders()));
logger.debug("[method getURI]" + method.getPath());
}
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
status = response.getCode();
responseHeaders = response.getHeaders();
responseMessage = EntityUtils.toByteArray(response.getEntity());
if (logger.isDebug()) {
logger.debug("[received status]" + status);
}
}
} catch (ConnectException e) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClient5AdapterServiceBypass] responseType =" + vo.getResponseType());
logger.debug("HttpClient5AdapterServiceBypass] RECV (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(responseMessage));
}
throw new Exception("excuteMethod java.net.ConnectException = " + e.getMessage());
}
stopWatch.stop();
/**
* @formatter:off
* OAuth 토큰 응답 체크(Adapter properties로 설정)
* 응답코드에 따라 유효한 토큰 확인(토큰 재발급 여부 확인)
* API 서비스 마다 정책이 다름(보통 400대에서 체크하나 200에서도 체크할 수 있음)
*
* TOKEN_ERROR_CODE_KEY: 응답 json error code key
* __ex) $.dataHeader.resultCode
* TOKEN_ERROR_CODE_VALUES: 토큰 재발급이 필요한 응답 error codes(ex: O0001,O0002,O0003)
* TOKEN_ERROR_HTTP_STATUS_CODE: 보통 400대에서 체크하나 API 제공자에 따라 200에서도 체클할수 있음
* ex) TOKEN_ERROR_HTTP_STATUS_CODE = 200
* @formatter:on
*/
boolean needReissue = false;
if (status == 200) {
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
}
} else if (status >= 400 && status < 500) {
if (useAdapterToken) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
}
if (!needReissue) {
// body 값이 없을때만 exception 처리하고, body가 있으면 bypass 한다.
if (responseMessage == null || responseMessage.length == 0) {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), url, restMethod, responseMessage);
throw new Exception(errMsg);
}
}
} else if (status != 200) {
responseMessage = responseMessage != null ? responseMessage : new byte[0];
// body 값이 없을때만 exception 처리하고, body가 있으면 bypass 한다.
if (responseMessage.length == 0) {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), url, restMethod, responseMessage);
throw new Exception(errMsg);
}
}
if (useAdapterToken) {
if (responseMessage == null) {
throw new Exception("responseMessage is NULL, HttpStatus=" + status);
}
// OAuth 토큰 재요청 코드 확인
if (needReissue) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClient5AdapterServiceBypass] retry access token response code = ["
+ vo.getResponseType() + "] message = [" + new String(responseMessage, vo.getEncode())
+ "]");
}
// 토큰 재발급
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
OAuth2AccessTokenVO newAccessToken = (OAuth2AccessTokenVO) tokenManager
.retryAccessTokenVO(vo.getAdapterGroupName(), prop, oldToken);
method.setHeader("Authorization", newAccessToken.getAuthorization());
setAuthHeaders(method, newAccessToken);
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClient5AdapterServiceBypass] SEND (" + vo.getAdapterGroupName()
+ ") RETRY TOKEN = [" + newAccessToken + "]");
}
try (CloseableHttpResponse retryResponse = (CloseableHttpResponse) mclient.execute(method, context)) {
status = retryResponse.getCode();
responseHeaders = retryResponse.getHeaders();
responseMessage = EntityUtils.toByteArray(retryResponse.getEntity());
} catch (IOException e) {
throw new Exception("retry excuteMethod Exception = " + e.getMessage());
}
if (status != 200) {
responseMessage = responseMessage != null ? responseMessage : new byte[0];
if (responseMessage.length == 0) {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), url, restMethod,
responseMessage);
throw new Exception(errMsg);
}
}
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClient5AdapterServiceBypass] RETRY responseType =" + vo.getResponseType());
logger.debug("HttpClient5AdapterServiceBypass] RETRY RECV (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(responseMessage));
}
}
}
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
"RECV [Bypass Request..]" + CommonLib.getDumpMessage(responseMessage));
}
// 응답 Content-Type charset 기반 인코딩 결정, 없으면 어댑터 encoding 사용
String responseEncode = vo.getEncode();
if (responseHeaders != null) {
for (Header header : responseHeaders) {
if (StringUtils.equalsIgnoreCase(header.getName(), HttpHeaders.CONTENT_TYPE)) {
try {
MediaType mediaType = MediaType.parseMediaType(header.getValue());
if (mediaType.getCharset() != null) {
responseEncode = mediaType.getCharset().name();
}
} catch (Exception ignored) {}
break;
}
}
}
assignRelayDataToInbound(tempProp, responseHeaders, status);
return responseMessage != null ? new String(responseMessage, responseEncode) : "";
} catch (SocketTimeoutException ste) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
"HttpClient5AdapterServiceBypass] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
+ ste.toString(),
ste);
}
logger.error("HttpClient5AdapterServiceBypass] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
+ ste.toString(), ste);
throw ste;
} catch (ConnectException ce) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
"HttpClient5AdapterServiceBypass] Connection Exception (" + vo.getAdapterGroupName() + ") : "
+ ce.toString(),
ce);
}
logger.error("HttpClient5AdapterServiceBypass] Connection Exception (" + vo.getAdapterGroupName() + ") : "
+ ce.toString(), ce);
throw ce;
} catch (Exception e) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(), e.toString(), e);
}
logger.error(
"HttpClient5AdapterServiceBypass] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
e);
throw e;
} finally {
if (status != HttpStatus.SC_OK) {
String[] msgArgs = new String[1];
msgArgs[0] = String.valueOf(status);
String resMsg = ExceptionUtil.make("RDCEAIAHA013", msgArgs);
if (logger.isDebug()) {
logger.debug(resMsg);
}
}
}
}
private boolean hasBody(Object data, HttpUriRequestBase method) {
if (data == null) {
return false;
}
// GET, DELETE는 표준 HTTP에서 body를 지원하지 않음
return method instanceof HttpPost || method instanceof HttpPut;
}
private HttpUriRequestBase generateMethod(String restMethod, String url) {
switch (HttpMethodType.getValue(restMethod)) {
case GET:
return new HttpGet(url);
case DELETE:
return new HttpDelete(url);
case POST:
return new HttpPost(url);
case PUT:
return new HttpPut(url);
default:
return new HttpPost(url);
}
}
private String getRewriteUrl(HttpClientAdapterVO vo, String inboundRewritePath, String queryString,
JSONObject restOptionObject, Map<String, String> inboundPathVariables) {
String fragment = null;
if (StringUtils.isNotBlank(queryString)) {
int fragIdx = queryString.indexOf('#');
if (fragIdx >= 0) {
fragment = queryString.substring(fragIdx + 1);
queryString = queryString.substring(0, fragIdx);
}
}
String restExtraPath = (String) restOptionObject.get("extraPath");
if (StringUtils.isBlank(restExtraPath)) {
StringBuilder uri = new StringBuilder(500);
String baseUrl = StringUtils.removeEnd(vo.getUrl(), "/");
String rewritePath = inboundRewritePath != null && !inboundRewritePath.startsWith("/")
? "/" + inboundRewritePath : inboundRewritePath;
uri.append(baseUrl).append(StringUtils.defaultString(rewritePath));
if (StringUtils.isNotBlank(queryString)) {
uri.append('?');
uri.append(encodeUriQuery(queryString, false));
}
if (doSendUrlFragment && fragment != null) {
uri.append('#');
uri.append(encodeUriQuery(fragment, false));
}
return uri.toString();
} else {
String type = (String) restOptionObject.get("type");
String url = getUrl(vo.getUrl(), restExtraPath);
if (StringUtils.equalsIgnoreCase(type, HttpClientAdapterServiceRest.TYPE_VARIABLE_URL_REQUEST)) {
inboundPathVariables = mergeInboundPathVariables(inboundPathVariables, queryString);
List<String> urlVariableValueList = new ArrayList<>();
JSONArray uriVariables = (JSONArray) restOptionObject.get("uriVariables");
if (uriVariables != null && !uriVariables.isEmpty() && inboundPathVariables != null
&& !inboundPathVariables.isEmpty()) {
for (Object tempObject : uriVariables) {
String urlVaribleId = (String) tempObject;
String uriVariableValue = inboundPathVariables.get(urlVaribleId);
urlVariableValueList.add(uriVariableValue);
}
if (!urlVariableValueList.isEmpty()) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(url).build();
Object[] urls = urlVariableValueList.toArray();
url = uriComponents.expand(urls).toUriString();
}
}
} else {
if (StringUtils.isNotBlank(queryString)) {
url += "?" + encodeUriQuery(queryString, false);
}
if (doSendUrlFragment && fragment != null) {
url += "#" + encodeUriQuery(fragment, false);
}
}
return url;
}
}
private Map<String, String> mergeInboundPathVariables(Map<String, String> inboundPathVariables,
String queryString) {
if (StringUtils.isBlank(queryString)) {
return inboundPathVariables;
}
if (inboundPathVariables == null) {
inboundPathVariables = new LinkedHashMap<>();
}
String[] pairs = queryString.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
inboundPathVariables.put(pair.substring(0, idx), pair.substring(idx + 1));
}
return inboundPathVariables;
}
private String getUrl(String baseUrl, String extraPath) {
if (StringUtils.isBlank(extraPath)) {
return baseUrl;
}
if (StringUtils.contains(extraPath, "http://") || StringUtils.contains(extraPath, "https://")) {
return extraPath;
}
String targetUrl = baseUrl;
if (!targetUrl.endsWith("/")) {
targetUrl += "/";
}
if (extraPath.startsWith("/")) {
targetUrl += extraPath.substring(1);
} else {
targetUrl += extraPath;
}
return targetUrl;
}
protected CharSequence encodeUriQuery(CharSequence in, boolean encodePercent) {
StringBuilder outBuf = null;
Formatter formatter = null;
for (int i = 0; i < in.length(); i++) {
char c = in.charAt(i);
boolean escape = true;
if (c < 128) {
if (asciiQueryChars.get(c) && !(encodePercent && c == '%')) {
escape = false;
}
} else if (!Character.isISOControl(c) && !Character.isSpaceChar(c)) {
escape = false;
}
if (!escape) {
if (outBuf != null)
outBuf.append(c);
} else {
if (outBuf == null) {
outBuf = new StringBuilder(in.length() + 5 * 3);
outBuf.append(in, 0, i);
formatter = new Formatter(outBuf);
}
formatter.format("%%%02X", (int) c);
}
}
return outBuf != null ? outBuf : in;
}
protected static final BitSet asciiQueryChars;
static {
char[] c_unreserved = "_-!.~'()*".toCharArray();
char[] c_punct = ",;:$&+=".toCharArray();
char[] c_reserved = "/@".toCharArray();
asciiQueryChars = new BitSet(128);
for (char c = 'a'; c <= 'z'; c++)
asciiQueryChars.set(c);
for (char c = 'A'; c <= 'Z'; c++)
asciiQueryChars.set(c);
for (char c = '0'; c <= '9'; c++)
asciiQueryChars.set(c);
for (char c : c_unreserved)
asciiQueryChars.set(c);
for (char c : c_punct)
asciiQueryChars.set(c);
for (char c : c_reserved)
asciiQueryChars.set(c);
asciiQueryChars.set('%');
}
private void assignRelayDataToInbound(Properties prop, Header[] responseHeaders, int status) {
Properties headerProp = new Properties();
if (responseHeaders != null) {
for (Header header : responseHeaders) {
String name = header.getName();
String value = header.getValue();
String existing = headerProp.getProperty(name);
if (existing != null) {
// 동일 이름의 헤더가 여러 개인 경우 콤마로 합침 (RFC 7230)
headerProp.put(name, existing + ", " + value);
} else {
headerProp.put(name, value);
}
}
}
headerProp.put(HttpAdapterServiceBypass.HTTP_STATUS, String.valueOf(status));
Map<String, Object> map = new HashMap<String, Object>();
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, headerProp);
prop.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map);
}
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
method.setHeader("Authorization",
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
}
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
String encode) {
if (responseMessage == null) {
return false;
}
if (StringUtils.isBlank(tokenErrorCodeKey)) {
return false;
}
if (StringUtils.isBlank(tokenErrorCodeValues)) {
return false;
}
try {
DocumentContext jsonContext = JsonPath.parse(new String(responseMessage, encode));
String responseCode = jsonContext.read(tokenErrorCodeKey);
if (StringUtils.isBlank(responseCode)) {
return false;
}
String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenErrorCodeValues, ",");
return ArrayUtils.contains(arr, responseCode);
} catch (Exception e) {
logger.error("HttpClient5AdapterServiceBypass] checkTokenRetry error=" + e.getMessage());
}
return false;
}
private void assignRequestHeaders(HttpUriRequestBase method, Properties headerProp, Properties inProp) {
if (headerProp == null) {
return;
}
for (Entry<Object, Object> e : headerProp.entrySet()) {
String key = (String) e.getKey();
String value = (String) e.getValue();
if (StringUtils.equalsAnyIgnoreCase(key, HttpAdapterServiceBypass.HOP_BY_HOP_HEADERS)
|| StringUtils.equalsIgnoreCase(key, HttpHeaders.CONTENT_LENGTH)) {
continue;
}
if (doHandleCompression && StringUtils.equalsIgnoreCase(key, HttpHeaders.ACCEPT_ENCODING)) {
continue;
}
method.addHeader(key, value);
if (logger.isDebugEnabled()) {
logger.debug("Request Header :" + key + "=[" + value + "]");
}
}
if (doForwardIP) {
String forHeaderName = "X-Forwarded-For";
String forHeader = inProp.getProperty(HttpAdapterServiceKey.INBOUND_REMOTE_ADDR);
if (StringUtils.isNotBlank(forHeader)) {
String existingForHeader = headerProp.getProperty(forHeaderName);
if (existingForHeader != null) {
forHeader = existingForHeader + ", " + forHeader;
}
method.addHeader(forHeaderName, forHeader);
}
String protoHeaderName = "X-Forwarded-Proto";
String protoHeader = inProp.getProperty(HttpAdapterServiceKey.INBOUND_SCHEME);
if (StringUtils.isNotBlank(protoHeader)) {
method.addHeader(protoHeaderName, protoHeader);
}
}
}
@SuppressWarnings("deprecation")
private JSONObject parseJson(String message) throws Exception {
if (message == null) {
return null;
}
return (JSONObject) JSONValue.parse(message);
}
public static final String getIgnoreCaseProp(Properties prop, String key) {
if (prop == null) {
return null;
}
for (Entry<Object, Object> e : prop.entrySet()) {
if (StringUtils.equalsIgnoreCase(key, (String) e.getKey())) {
return (String) e.getValue();
}
}
return null;
}
}
@@ -9,6 +9,9 @@ import org.json.simple.JSONObject;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey; 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.HttpClient5AdapterFilterFactory;
import com.eactive.eai.adapter.http.client.impl.filter.HttpClientAdapterFilter; import com.eactive.eai.adapter.http.client.impl.filter.HttpClientAdapterFilter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
/** /**
* 1. 기능 : HttpClient5AdapterServiceRest 호출 전후 Filter 적용할 수 있는 기능을 제공한다.<br> * 1. 기능 : HttpClient5AdapterServiceRest 호출 전후 Filter 적용할 수 있는 기능을 제공한다.<br>
@@ -34,6 +37,8 @@ public class HttpClient5AdapterServiceRestAddFilter extends HttpClient5AdapterSe
// // Adapter에서 Exception이 발생한 경우, 처리할 필터 // // Adapter에서 Exception이 발생한 경우, 처리할 필터
// static final String EXCEPTION_FILTER = "EXCEPTION_FILTER"; // static final String EXCEPTION_FILTER = "EXCEPTION_FILTER";
private ObjectMapper mapper = new ObjectMapper();
/** /**
* 1. 기능 : HttpClient 호출 전후 Filter 적용용 2. 처리 개요 : <br> * 1. 기능 : HttpClient 호출 전후 Filter 적용용 2. 처리 개요 : <br>
* 3. 주의사항 <br> * 3. 주의사항 <br>
@@ -54,8 +59,9 @@ public class HttpClient5AdapterServiceRestAddFilter extends HttpClient5AdapterSe
adapterResponse = doPostFilters(adptGrpName, adptName, prop, adapterResponse, tempProp); adapterResponse = doPostFilters(adptGrpName, adptName, prop, adapterResponse, tempProp);
if (adapterResponse instanceof JSONObject) if (adapterResponse instanceof JSONObject)
adapterResponse = ((JSONObject) adapterResponse).toJSONString(); adapterResponse = ((JSONObject) adapterResponse).toJSONString();
else if (adapterResponse instanceof ObjectNode)
if (adapterResponse instanceof Node) adapterResponse = mapper.writeValueAsString((ObjectNode) adapterResponse);
else if (adapterResponse instanceof Node)
adapterResponse = ((Node) adapterResponse).asXML(); adapterResponse = ((Node) adapterResponse).asXML();
return adapterResponse; return adapterResponse;
@@ -65,7 +65,7 @@ public class RetryPolicy {
return new RetryPolicy(getInt(props, RETRY_MAX_RETRIES, 0), getLong(props, RETRY_BACKOFF_MS, 1000L), codes, return new RetryPolicy(getInt(props, RETRY_MAX_RETRIES, 0), getLong(props, RETRY_BACKOFF_MS, 1000L), codes,
getBoolean(props, RETRY_ON_CONNECT_FAIL, false), getBoolean(props, RETRY_ON_TIMEOUT, false), getBoolean(props, RETRY_ON_CONNECT_FAIL, false), getBoolean(props, RETRY_ON_TIMEOUT, false),
getBoolean(props, RETRY_IDEMPOTENT_ONLY, true)); getBoolean(props, RETRY_IDEMPOTENT_ONLY, false));
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -0,0 +1,33 @@
package com.eactive.eai.adapter.http.client.impl.filter;
import java.util.Properties;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.util.Logger;
public class DebugLoggerOutFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
@Override
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
throws Exception {
logger.debug("Outbound Filter SND adptGrpName : {}", adptGrpName);
logger.debug("Outbound Filter SND adptName : {}", adptName);
logger.debug("Outbound Filter SND prop : {}", prop);
logger.debug("Outbound Filter SND message : {}", message);
logger.debug("Outbound Filter SND tempProp : {}", tempProp);
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
Properties tempProp) throws Exception {
logger.debug("Outbound Filter RCV adptGrpName : {}", adptGrpName);
logger.debug("Outbound Filter RCV adptName : {}", adptName);
logger.debug("Outbound Filter RCV prop : {}", prop);
logger.debug("Outbound Filter RCV message : {}", message);
logger.debug("Outbound Filter RCV tempProp : {}", tempProp);
return message;
}
}
@@ -6,7 +6,7 @@ import com.eactive.eai.common.util.Logger;
public class HttpClient5AdapterFilterFactory { public class HttpClient5AdapterFilterFactory {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
static String customBasePackage = "com.eactive.eai.custom.adapter.http.client.filter"; static String customBasePackage = "com.eactive.eai.custom.adapter.http.client.impl.filter";
static String basePackage = "com.eactive.eai.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 static ConcurrentHashMap<String, HttpClientAdapterFilter> h = new ConcurrentHashMap<>();
@@ -19,21 +19,27 @@ import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요. * STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
* *
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조. * 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
* AAD 관련 추가 프로퍼티: * AAD 관련 프로퍼티:
* CRYPTO_AAD_PROP tempProp에서 AAD 값을 조회할 키 이름. * PROP_AAD_HEADER tempProp에서 AAD 값을 조회할 키 이름.
* 미설정 또는 해당 키 없으면 aad=null → IV를 AAD 대체값으로 사용. * 미설정 또는 해당 키 없으면 aad=null → IV를 AAD 대체값으로 사용.
*/ */
public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientAdapterFilter { public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientAdapterFilter {
public static final String PROP_AAD_PROP = "CRYPTO_AAD_PROP";
/** /**
* DYNAMIC 키 도출용 컨텍스트를 구성한다. * DYNAMIC 키 도출용 컨텍스트를 구성한다.
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다. * STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
* 서브클래스에서 오버라이드하여 tempProp으로부터 값을 추출할 수 있다. * 서브클래스에서 오버라이드하여 tempProp으로부터 값을 추출할 수 있다.
*/ */
protected Map<String, String> buildRuntimeContext(Properties prop, Properties tempProp) { protected Map<String, String> buildRuntimeContext(Properties prop, Properties tempProp) {
return new HashMap<>(); Map<String, String> context = new HashMap<>();
for (String key : prop.stringPropertyNames()) {
context.put(key, prop.getProperty(key));
}
for (String key : tempProp.stringPropertyNames()) {
context.put(key, tempProp.getProperty(key));
}
return context;
} }
/** /**
@@ -41,11 +47,10 @@ public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientA
* 미설정 또는 값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다. * 미설정 또는 값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
*/ */
protected byte[] buildAad(Properties prop, Properties tempProp) { protected byte[] buildAad(Properties prop, Properties tempProp) {
String propKey = prop.getProperty(PROP_AAD_PROP); if (tempProp == null) {
if (StringUtils.isBlank(propKey) || tempProp == null) { return null;
return null; }
} String value = tempProp.getProperty(PROP_AAD_HEADER);
String value = tempProp.getProperty(propKey);
return StringUtils.isBlank(value) ? null : value.getBytes(StandardCharsets.UTF_8); return StringUtils.isBlank(value) ? null : value.getBytes(StandardCharsets.UTF_8);
} }
@@ -61,8 +66,8 @@ public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientA
if (SCOPE_FIELD.equals(scope)) { if (SCOPE_FIELD.equals(scope)) {
return encryptField(body, moduleName, runtimeCtx, aad, return encryptField(body, moduleName, runtimeCtx, aad,
getProp(prop, PROP_ENC_FROM_PATH, PATH_ROOT), getProp(tempProp, PROP_ENC_FROM_PATH, PATH_ROOT),
getProp(prop, PROP_ENC_TO_PATH, PATH_ENCDATA)); getProp(tempProp, PROP_ENC_TO_PATH, PATH_ENCDATA));
} }
return encryptBody(body, moduleName, runtimeCtx, aad); return encryptBody(body, moduleName, runtimeCtx, aad);
} }
@@ -79,8 +84,8 @@ public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientA
if (SCOPE_FIELD.equals(scope)) { if (SCOPE_FIELD.equals(scope)) {
return decryptField(body, moduleName, runtimeCtx, aad, return decryptField(body, moduleName, runtimeCtx, aad,
getProp(prop, PROP_DEC_FROM_PATH, PATH_ENCDATA), getProp(tempProp, PROP_DEC_FROM_PATH, PATH_ENCDATA),
getProp(prop, PROP_DEC_TO_PATH, PATH_ROOT)); getProp(tempProp, PROP_DEC_TO_PATH, PATH_ROOT));
} }
return decryptBody(body, moduleName, runtimeCtx, aad); return decryptBody(body, moduleName, runtimeCtx, aad);
} }
@@ -0,0 +1,897 @@
package com.eactive.eai.adapter.http.client.impl.kjb;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.http.protocol.BasicHttpContext;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URIBuilder;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import com.eactive.eai.adapter.http.HttpMemoryLogger;
import com.eactive.eai.adapter.http.HttpMethodType;
import com.eactive.eai.adapter.http.client.HttpClient5AdapterServiceSupport;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.util.CommonLib;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.openbanking.eai.common.token.AccessTokenManager;
import com.openbanking.eai.common.token.AccessTokenVO;
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
import com.eactive.eai.common.property.PropManager;
public class HttpClient5AdapterServiceBase64 extends HttpClient5AdapterServiceSupport
implements HttpClientAdapterServiceKey {
public static final String TYPE_VARIABLE_URL_REQUEST = "variableUrlRequest";
public static final String TYPE_SIMPLE_REQUEST = "simpleRequest";
public static final String REST_OPTION = "REST_OPTION";
//public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_GROUP = "HEADER_GROUP";
public static final String HTTP_STATUS = "HTTP_STATUS";
public static final String HEADER_KEYS = "HEADER_KEYS";
public static final String AUTH_TOKEN = "AUTH_TOKEN";
/**
* 1. 기능 : REST API 통신에 사용 <br>
* 2. 처리 개요 : - 속성 정보를 설정 하고 수동 시스템 서비스를 호출 한다. <br>
* 3. 주의사항 <br>
*
* @param prop Http Adapter 속성 정보
* @return 반환 된 Object
* @exception Exception 수동 시스템 간 통신 중 발생
**/
@SuppressWarnings({ "unchecked" })
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
HttpClientAdapterVO vo = super.setting(prop, tempProp);
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
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 relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, "");
boolean useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
// ex) $.dataHeader.GW_RSLT_CD
String tokenErrorCodeKey = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_KEY");
String tokenErrorCodeValues = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_VALUES");
String tokenErrorHttpStatusCode = prop.getProperty("ADAPTER_TOKEN_ERROR_HTTP_STATUS_CODE"); // 200 or 400번대
// 레이아웃 메시지 타입 REST URL 추출 시 활용. Default JSON
String messageType = prop.getProperty("MESSAGE_TYPE", MessageType.JSON);
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
/**
* @formatter:off
* TSEAIHE02.RESTOPTION 정보 => JSON 형태로 구성
* - type : simpleRequest, variableUrlRequest
* - extraPath : 어댑터 프로퍼티 URL에 추가될 HTTP REST URL
* - method : get, delete, post, put
* - contentType: application/json(default), application/x-www-form-urlencoded
* - adapterTokenUseYn: Y, N(default)
* ex)
* {"type":"simpleRequest","extraPath":"v2.0/accout/balance","method":"post"}
* @formatter:on
*/
String restOptionData = tempProp.getProperty(REST_OPTION);
JSONObject restOptionObject = parseJson(restOptionData);
if (restOptionObject == null) {
throw new Exception("OptionData parsing result is NULL");
}
String rmethod = (String) restOptionObject.getOrDefault("method", inboundMethod);
String contentType = (String) restOptionObject.getOrDefault("contentType", "application/json");
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
}
HttpClient mclient = this.client;
String sendData = "";
if (data instanceof String) {
sendData = (String) data;
} else if (data instanceof byte[]) {
sendData = new String((byte[]) data, vo.getEncode());
}
////mclient.getParams().setContentCharset(vo.getEncode());
JSONObject dataObject = null;
Object httpHeader = null;
Object dataContent = null;
if (MessageType.JSON.equals(messageType)) {
Object parsed = parseJsonGeneric(sendData);
if(parsed instanceof JSONObject) {
dataObject = parseJson(sendData);
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) {
httpHeader = dataObject.get(headerGroupName);
dataObject.remove(headerGroupName);
}
dataContent = dataObject;
if (dataObject.containsKey("innerList")) {
JSONArray innerList = (JSONArray) dataObject.get("innerList");
dataContent = innerList;
}
}
}
logger.debug("HttpClientAdapterServiceRest] dataObject1 = [" + dataObject + "]");
logger.debug("HttpClientAdapterServiceRest] dataContent = [" + dataContent + "]");
// interface 에 설정된게 우선한다.
String uri = null;
if (dataObject == null) {
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData);
} else {
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject);
}
// ////if (vo.getConnectionTimeout() != 0) {
// mclient.getHttpConnectionManager().getParams().setConnectionTimeout(vo.getConnectionTimeout());
// }
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL=[" + vo.getUrl() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") PARAMETER_NAME=[" + vo.getParameterName() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") ENCODE=[" + vo.getEncode() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") RESPONSE_TYPE=[" + vo.getResponseType() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL_ENCODE_YN=[" + vo.getUrlEncodeYn() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") CONNECTION_TIMEOUT=[" + vo.getConnectionTimeout() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") REST_OPTION=[" + restOptionData + "] ");
}
HttpUriRequestBase method = null;
// 메소드 타입에 따라 HttpRequestBase 인스턴스 생성
switch (HttpMethodType.getValue(rmethod)) {
case GET:
method = new HttpGet(uri);
break;
case DELETE:
method = new HttpDelete(uri);
break;
case POST:
method = new HttpPost(uri);
break;
case PUT:
method = new HttpPut(uri);
break;
default:
method = new HttpGet(uri);
break;
}
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
if(useForwardProxy) {
java.net.URL url = new java.net.URL(forwardProxyUrl);
// 프로토콜, 호스트, 포트 추출
String protocol = url.getProtocol();
String host = url.getHost();
int port = url.getPort();
HttpHost proxy = new HttpHost(protocol,host, port);
requestConfigBuilder.setProxy(proxy);
}
configureHttpClient(requestConfigBuilder, vo, method);
switch (HttpMethodType.getValue(rmethod)) {
case GET:
case DELETE:
HashMap<String, String> h = getParameters(dataObject);
URIBuilder uriBuilder = new URIBuilder(uri);
for (Map.Entry<String, String> entry : h.entrySet()) {
uriBuilder.addParameter(entry.getKey(), entry.getValue());
}
URI fullUri = uriBuilder.build();
if (method instanceof HttpGet) {
((HttpGet) method).setUri(fullUri);
} else if (method instanceof HttpDelete) {
((HttpDelete) method).setUri(fullUri);
}
if (logger.isDebug()) {
logger.debug("HttpClientAdapterServiceRest] (get Method) QueryString = [" + fullUri.getQuery() + "]");
}
break;
case PUT:
case POST:
//assignPostBody(dataObject, contentType, vo.getEncode(), method);
assignPostBody(dataContent, contentType, vo.getEncode(), method);
break;
default:
break;
}
if (logger.isDebugEnabled()) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") method.getParams()=["
+ method.getEntity() + "] ");
}
OAuth2AccessTokenVO accessToken = null;
if (useAdapterToken) {
AccessTokenManagerByDB tokenManager = AccessTokenManagerByDB.getInstance();
accessToken = (OAuth2AccessTokenVO) tokenManager.getAccessTokenVO(vo.getAdapterGroupName());
// 토큰이 없거나 만료 됐으면 재발급
if (accessToken == null || accessToken.isExpired()) {
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
accessToken = (OAuth2AccessTokenVO) tokenManager.retryAccessTokenVO(vo.getAdapterGroupName(), prop,
oldToken);
}
if (logger.isDebug()) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") TOKEN = ["
+ accessToken + "]");
}
setAuthHeaders(method, accessToken);
if (logger.isDebug()) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
+ ") RequestHeader [Authorization=" + method.getHeader("Authorization") + "]");
}
}
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
if(!StringUtils.isBlank(authToken)) {
setAuthHeaders(method, authToken);
}
// 전달 header 셋팅
// Adapter 레벨 header 보다 data로 넘어온 httpHeader를 우선 적용(header명이 같다면 덮어씀)
assignRequestHeaders(method, httpHeader);
int status = -1;
try {
byte[] responseMessage = null;
Header[] responseHeaders;
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
if (dataContent != null) { //dataObject
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = ["
+ dataContent.toString() + "]"); //dataObject.toJSONString()
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(dataContent.toString())); //dataObject.toJSONString()
} else {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" + sendData
+ "]");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(sendData));
}
}
StopWatch stopWatch = new StopWatch();
stopWatch.start();
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
}
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
HttpContext context = new BasicHttpContext();
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME, vo.getAdapterGroupName());
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_NAME, vo.getAdapterName());
Integer logProcessNo = (Integer) tempProp.get(HttpClientAdapterServiceKey.LOG_PROCESS_NO);
if (logProcessNo != null && logProcessNo > 0) {
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
}
try {
if (logger.isDebug()) {
logger.debug("[method getName]" + method.getMethod());
logger.debug("[method getEntity]" + method.getEntity());
logger.debug("[method getRequestHeaders]" + method.getHeaders().toString());
logger.debug("[method getURI]" + method.getPath());
}
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
status = response.getCode();
if (status >= 200 && status <= 207) {
status = 200;
}
// if (status == 404) {
// status = 200;
// }
// responseMessage = EntityUtils.toByteArray(response.getEntity());
responseHeaders = response.getHeaders();
// 2025/08/06 Response body를 Base64로 Encode 해서 Json으로 만들어서 전달하도록 수정
HttpEntity entity = response.getEntity();
if (entity != null) {
byte[] entityBytes = EntityUtils.toByteArray(response.getEntity());
String b64 = Base64.encodeBase64String(entityBytes);
JSONObject resultJson = new JSONObject();
resultJson.put("base64_file_data", b64);
responseMessage = resultJson.toString().getBytes();
} else {
responseMessage = EntityUtils.toByteArray(response.getEntity());
}
if (logger.isDebug()) {
logger.debug("[received status]" + status);
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]");
}
}
} catch (ConnectException e) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
}
throw new Exception("excuteMethod java.net.ConnectException = " + e.getMessage());
}
stopWatch.stop();
/**
* @formatter:off
* OAuth 토큰 응답 체크(Adapter properties로 설정)
* 응답코드에 따라 유효한 토큰 확인(토큰 재발급 여부 확인)
* API 서비스 마다 정책이 다름(보통 400대에서 체크하나 200에서도 체크할 수 있음)
*
* TOKEN_ERROR_CODE_KEY: 응답 json error code key
* __ex) $.dataHeader.resultCode
* TOKEN_ERROR_CODE_VALUES: 토큰 재발급이 필요한 응답 error codes(ex: O0001,O0002,O0003)
* TOKEN_ERROR_HTTP_STATUS_CODE: 보통 400대에서 체크하나 API 제공자에 따라 200에서도 체클할수 있음
* ex) TOKEN_ERROR_HTTP_STATUS_CODE = 200
* @formatter:on
*/
boolean needReissue = false;
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders);
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 (status >= 200 && status <= 207) {
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
}
} else if (status == 302) {
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
relayResponseHeaderKeys += ",Location";
}
} else {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseString);
// logger.error(errMsg);
if (status >= 400 && status < 500) {
if (useAdapterToken) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
}
// if (!needReissue) {
// throw new Exception(errMsg);
// }
} else {
throw new Exception(errMsg);
}
}
// ----------------------------------------------------------
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
logger.debug("HttpClientAdapterServiceRest] RECV " + vo.getEncode() + "(" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
// logger.debug("HttpClientAdapterServiceRest] RECV MS949 (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "MS949") + "]");
// logger.debug("HttpClientAdapterServiceRest] RECV euc-kr (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "euc-kr") + "]");
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = " + CommonLib.getDumpMessage(responseMessage));
}
if (useAdapterToken) {
if (responseMessage == null) {
throw new Exception("responseMessage is NULL, HttpStatus=" + status);
}
// OAuth 토큰 재요청 코드 확인
if (needReissue) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] retry access token response code = ["
+ vo.getResponseType() + "] message = [" + new String(responseMessage, vo.getEncode())
+ "]");
}
// 토큰 재발급
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
OAuth2AccessTokenVO newaccessToken = (OAuth2AccessTokenVO) tokenManager
.retryAccessTokenVO(vo.getAdapterGroupName(), prop, oldToken);
method.setHeader("Authorization", newaccessToken.getAuthorization());
setAuthHeaders(method, newaccessToken);
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
+ ") RETRY TOKEN = [" + newaccessToken + "]");
}
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method)) {
status = response.getCode();
responseMessage = EntityUtils.toByteArray(response.getEntity());
if (logger.isDebug()) {
logger.debug("[received status]" + status);
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
}
}catch (IOException e){
throw new Exception("retry excuteMethod Exceptioin = " + e.getMessage());
}
if (status == 302) {
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
relayResponseHeaderKeys += ",Location";
}
} else {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseMessage);
throw new Exception(errMsg);
}
// ----------------------------------------------------------
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] RETRY responseType =" + vo.getResponseType());
logger.debug("HttpClientAdapterServiceRest] RETRY RECV " + vo.getEncode() + "("
+ vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode())
+ "]");
logger.debug("HttpClientAdapterServiceRest] RETRY RECV (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(responseMessage));
}
// ----------------------------------------------------------
}
}
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
"RECV [" + new String(responseMessage, vo.getEncode()) + "]"
+ CommonLib.getDumpMessage(responseMessage));
}
return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), messageType,
relayResponseHeaderKeys, status, responseHeaderProp);
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
"HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
+ ste.toString(),
ste);
}
logger.error("HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
+ ste.toString(), ste);
throw ste;
} catch (ConnectException ce) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
"HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
+ ce.toString(),
ce);
}
logger.error("HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
+ ce.toString(), ce);
throw ce;
} catch (Exception e) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(), e.toString(), e);
}
logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
e);
throw e;
} finally {
if (status != HttpStatus.SC_OK) {
String[] msgArgs = new String[1];
msgArgs[0] = String.valueOf(status);
String resMsg = ExceptionUtil.make("RDCEAIAHA013", msgArgs);
if (logger.isDebug()) {
logger.debug(resMsg);
}
}
}
}
private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) {
Properties headerProp = new Properties();
if (responseHeaders == null || responseHeaders.length == 0) {
return headerProp;
}
for (Header header : responseHeaders) {
headerProp.put(header.getName(), header.getValue());
}
return headerProp;
}
/**
* 현재 1단계 레이아웃에만 적용되도록 구현됨.
*
* @param method
* @param responseMessage
* @param headerGroupName
* @param encode
* @param messageType
* @param relayHeaderKeys
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
private String assignResponseHeaders(HttpUriRequestBase method, byte[] responseMessage, String headerGroupName,
String encode, String messageType, String relayHeaderKeys, int status, Properties responseHeaderProp) throws Exception {
if (!MessageType.JSON.equals(messageType) || StringUtils.isBlank(headerGroupName)
|| StringUtils.isBlank(relayHeaderKeys)) {
return new String(responseMessage, encode);
}
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
JSONObject headerJson = new JSONObject();
for (String key : relayKeyArr) {
String value = responseHeaderProp.getProperty(key);
if (StringUtils.isNotBlank(value)) {
headerJson.put(key, value);
}
}
headerJson.put(HTTP_STATUS, String.valueOf(status));
if (headerJson.size() <= 0) {
return new String(responseMessage, encode);
}
JSONObject message = null;
if (responseMessage == null || responseMessage.length == 0) {
message = new JSONObject();
} else {
message = parseJson(new String(responseMessage, encode));
if (message == null) {
message = new JSONObject();
message.put("Malformed_Response_Message", new String(responseMessage, encode));
}
}
if (StringUtils.isNotBlank(headerGroupName) && message != null)
message.put(headerGroupName, headerJson);
logger.info("------------- message --------------- : {} ", message);
return message.toJSONString();
}
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
private void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
method.setHeader("Authorization",
String.format("%s", authorization));
}
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
method.setHeader("Authorization",
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
}
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
String encode) {
if (responseMessage == null) {
return false;
}
if (StringUtils.isBlank(tokenErrorCodeKey)) {
return false;
}
if (StringUtils.isBlank(tokenErrorCodeValues)) {
return false;
}
try {
DocumentContext jsonContext = JsonPath.parse(new String(responseMessage, encode));
String responseCode = jsonContext.read(tokenErrorCodeKey);
if (StringUtils.isBlank(responseCode)) {
return false;
}
String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenErrorCodeValues, ",");
return ArrayUtils.contains(arr, responseCode);
} catch (Exception e) {
logger.error("HttpClientAdapterServiceRest] checkTokenRetry error=" + e.getMessage());
}
return false;
}
private void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader) {
if (httpHeader == null) {
return;
}
if (httpHeader instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) httpHeader;
for (Object key : jsonObject.keySet()) {
Object obj = jsonObject.get(key);
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
continue;
}
method.setHeader((String) key, (String) obj);
if (logger.isDebugEnabled()) {
logger.debug("Request Header :" + (String) key + "=[" + (String) obj + "]");
}
}
}
}
@SuppressWarnings("deprecation")
private void assignPostBody(Object eaiBody, String contentType, String charset, HttpUriRequestBase method) {
if (eaiBody == null) {
return;
}
if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) {
if (eaiBody instanceof JSONObject) {
List<NameValuePair> params = new ArrayList<>();
JSONObject jsonObject = (JSONObject) eaiBody;
for (Object key : jsonObject.keySet()) {
params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key)));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, StandardCharsets.UTF_8);
method.setEntity(entity);
}
} else {
ContentType contentTypeObj = ContentType.create(contentType, charset);
// 요청 본문을 StringEntity 객체로 생성
StringEntity entity = new StringEntity(eaiBody.toString(), contentTypeObj);
// 요청에 본문 추가
method.setEntity(entity);
}
}
private HashMap<String, String> getParameters(Object message) throws Exception {
HashMap<String, String> result = new HashMap<String, String>();
if (message != null) {
if (message instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) message;
for (Object key : jsonObject.keySet()) {
Object obj = jsonObject.get(key);
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
continue;
}
result.put((String) key, getStringValue(obj));
}
} else {
String[] messages = ((String) message).split("&");
String[] data = null;
for (int i = 0; i < messages.length; i++) {
data = messages[i].split("=", 2);
if (data.length == 2) {
result.put(data[0], data[1]);
}
}
}
}
return result;
}
public String getStringValue(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof String) {
return (String) obj;
} else if (obj instanceof Long) {
return Long.toString((Long) obj);
} else if (obj instanceof BigDecimal) {
return ((BigDecimal) obj).toPlainString();
} else {
return (String) obj;
}
}
public static Object parseJsonGeneric(String jsonData) {
if (StringUtils.isBlank(jsonData)) {
return null;
}
try {
JSONParser parser = new JSONParser();
Object parsedObj = parser.parse(jsonData);
return parsedObj;
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("deprecation")
private JSONObject parseJson(String message) throws Exception {
if (message == null) {
return null;
}
return (JSONObject) JSONValue.parse(message);
}
private Document convertXmlDocument(String message) throws Exception {
SAXReader builder = new SAXReader();
Document document = builder.read(new StringReader(message));
return document;
}
@SuppressWarnings("deprecation")
private String changeUrl(String messageType, String url, String restOption, Object sendData) {
if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) {
try {
if (StringUtils.isBlank(restOption)) {
return url;
}
List<String> urlVariableList = new ArrayList<String>();
JSONObject restOptionObject = parseJson(restOption);
if (restOptionObject == null)
throw new Exception("restOption is NULL");
String type = (String) restOptionObject.get("type");
String requestExtraPath = (String) restOptionObject.get("extraPath");
url = getUrl(url, requestExtraPath);
if (TYPE_VARIABLE_URL_REQUEST.equals(type)) {
Pattern p = Pattern.compile("\\{(.*?)\\}");
Matcher m = p.matcher(requestExtraPath);
List<String> uriVariables = new ArrayList<>();
while(m.find()) {
String fieldName = m.group(1);
uriVariables.add(fieldName);
}
if (MessageType.JSON.equals(messageType)) {
JSONObject jsonObject;
if (sendData instanceof JSONObject) {
jsonObject = (JSONObject) sendData;
} else {
jsonObject = (JSONObject) JSONValue.parse((String) sendData);
}
for (Object tempObject : uriVariables) {
String urlVaribleId = (String) tempObject;
String uriVariable = (String) jsonObject.get(urlVaribleId);
urlVariableList.add(uriVariable);
jsonObject.remove(urlVaribleId);
}
} else if (MessageType.XML.equals(messageType)) {
Document doc;
if (sendData instanceof Document) {
doc = (Document) sendData;
} else {
doc = convertXmlDocument((String) sendData);
}
for (Object tempObject : uriVariables) {
String urlVaribleId = (String) tempObject;
Element element = (Element) doc.selectSingleNode("//" + urlVaribleId);
urlVariableList.add(element.getText());
if (doc.getRootElement() == element) {
doc = null;
} else {
element.getParent().remove(element);
}
}
}
if (urlVariableList.size() > 0) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(url).build();
Object[] urls = urlVariableList.toArray();
url = uriComponents.expand(urls).toUriString();
logger.debug("HttpClientAdapterServiceRest] after extand url=[" + url + "] ");
}
}
} catch (Exception e) {
logger.error("Failed to change url", e);
}
}
return url;
}
private String getUrl(String baseUrl, String extraPath) {
if (StringUtils.isBlank(extraPath)) {
return baseUrl;
}
if (StringUtils.contains(extraPath, "http://") || StringUtils.contains(extraPath, "https://")) {
return extraPath;
}
String targetUrl = baseUrl;
if (!targetUrl.endsWith("/")) {
targetUrl += "/";
}
if (extraPath.startsWith("/")) {
targetUrl += extraPath.substring(1);
} else {
targetUrl += extraPath;
}
logger.debug("HttpClientAdapterServiceRest] after concatenate url=[" + targetUrl + "] ");
return targetUrl;
}
}
@@ -0,0 +1,888 @@
package com.eactive.eai.adapter.http.client.impl.kjb;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpPut;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.http.protocol.BasicHttpContext;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URIBuilder;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import com.eactive.eai.adapter.http.HttpMemoryLogger;
import com.eactive.eai.adapter.http.HttpMethodType;
import com.eactive.eai.adapter.http.client.HttpClient5AdapterServiceSupport;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.util.CommonLib;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.openbanking.eai.common.token.AccessTokenManager;
import com.openbanking.eai.common.token.AccessTokenVO;
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
import com.eactive.eai.common.property.PropManager;
public class HttpClient5AdapterServiceKbank extends HttpClient5AdapterServiceSupport
implements HttpClientAdapterServiceKey {
public static final String TYPE_VARIABLE_URL_REQUEST = "variableUrlRequest";
public static final String TYPE_SIMPLE_REQUEST = "simpleRequest";
public static final String REST_OPTION = "REST_OPTION";
//public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_GROUP = "HEADER_GROUP";
public static final String HTTP_STATUS = "HTTP_STATUS";
public static final String HEADER_KEYS = "HEADER_KEYS";
public static final String AUTH_TOKEN = "AUTH_TOKEN";
/**
* 1. 기능 : REST API 통신에 사용 <br>
* 2. 처리 개요 : - 속성 정보를 설정 하고 수동 시스템 서비스를 호출 한다. <br>
* 3. 주의사항 <br>
*
* @param prop Http Adapter 속성 정보
* @return 반환 된 Object
* @exception Exception 수동 시스템 간 통신 중 발생
**/
@SuppressWarnings({ "unchecked" })
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
HttpClientAdapterVO vo = super.setting(prop, tempProp);
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
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 relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, "");
boolean useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
// ex) $.dataHeader.GW_RSLT_CD
String tokenErrorCodeKey = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_KEY");
String tokenErrorCodeValues = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_VALUES");
String tokenErrorHttpStatusCode = prop.getProperty("ADAPTER_TOKEN_ERROR_HTTP_STATUS_CODE"); // 200 or 400번대
// 레이아웃 메시지 타입 REST URL 추출 시 활용. Default JSON
String messageType = prop.getProperty("MESSAGE_TYPE", MessageType.JSON);
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
/**
* @formatter:off
* TSEAIHE02.RESTOPTION 정보 => JSON 형태로 구성
* - type : simpleRequest, variableUrlRequest
* - extraPath : 어댑터 프로퍼티 URL에 추가될 HTTP REST URL
* - method : get, delete, post, put
* - contentType: application/json(default), application/x-www-form-urlencoded
* - adapterTokenUseYn: Y, N(default)
* ex)
* {"type":"simpleRequest","extraPath":"v2.0/accout/balance","method":"post"}
* @formatter:on
*/
String restOptionData = tempProp.getProperty(REST_OPTION);
JSONObject restOptionObject = parseJson(restOptionData);
if (restOptionObject == null) {
throw new Exception("OptionData parsing result is NULL");
}
String rmethod = (String) restOptionObject.getOrDefault("method", inboundMethod);
String contentType = (String) restOptionObject.getOrDefault("contentType", "application/json");
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
}
HttpClient mclient = this.client;
String sendData = "";
if (data instanceof String) {
sendData = (String) data;
} else if (data instanceof byte[]) {
sendData = new String((byte[]) data, vo.getEncode());
}
////mclient.getParams().setContentCharset(vo.getEncode());
JSONObject dataObject = null;
Object httpHeader = null;
Object dataContent = null;
if (MessageType.JSON.equals(messageType)) {
Object parsed = parseJsonGeneric(sendData);
if(parsed instanceof JSONObject) {
dataObject = parseJson(sendData);
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) {
httpHeader = dataObject.get(headerGroupName);
dataObject.remove(headerGroupName);
}
dataContent = dataObject;
if (dataObject.containsKey("innerList")) {
JSONArray innerList = (JSONArray) dataObject.get("innerList");
dataContent = innerList;
}
}
}
logger.debug("HttpClientAdapterServiceRest] dataObject1 = [" + dataObject + "]");
logger.debug("HttpClientAdapterServiceRest] dataContent = [" + dataContent + "]");
// interface 에 설정된게 우선한다.
String uri = null;
if (dataObject == null) {
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData);
} else {
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject);
}
// ////if (vo.getConnectionTimeout() != 0) {
// mclient.getHttpConnectionManager().getParams().setConnectionTimeout(vo.getConnectionTimeout());
// }
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL=[" + vo.getUrl() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") PARAMETER_NAME=[" + vo.getParameterName() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") ENCODE=[" + vo.getEncode() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") RESPONSE_TYPE=[" + vo.getResponseType() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL_ENCODE_YN=[" + vo.getUrlEncodeYn() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") CONNECTION_TIMEOUT=[" + vo.getConnectionTimeout() + "] ");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") REST_OPTION=[" + restOptionData + "] ");
}
HttpUriRequestBase method = null;
// 메소드 타입에 따라 HttpRequestBase 인스턴스 생성
switch (HttpMethodType.getValue(rmethod)) {
case GET:
method = new HttpGet(uri);
break;
case DELETE:
method = new HttpDelete(uri);
break;
case POST:
method = new HttpPost(uri);
break;
case PUT:
method = new HttpPut(uri);
break;
default:
method = new HttpGet(uri);
break;
}
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
if(useForwardProxy) {
java.net.URL url = new java.net.URL(forwardProxyUrl);
// 프로토콜, 호스트, 포트 추출
String protocol = url.getProtocol();
String host = url.getHost();
int port = url.getPort();
HttpHost proxy = new HttpHost(protocol,host, port);
requestConfigBuilder.setProxy(proxy);
}
configureHttpClient(requestConfigBuilder, vo, method);
switch (HttpMethodType.getValue(rmethod)) {
case GET:
case DELETE:
HashMap<String, String> h = getParameters(dataObject);
URIBuilder uriBuilder = new URIBuilder(uri);
for (Map.Entry<String, String> entry : h.entrySet()) {
uriBuilder.addParameter(entry.getKey(), entry.getValue());
}
URI fullUri = uriBuilder.build();
if (method instanceof HttpGet) {
((HttpGet) method).setUri(fullUri);
} else if (method instanceof HttpDelete) {
((HttpDelete) method).setUri(fullUri);
}
if (logger.isDebug()) {
logger.debug("HttpClientAdapterServiceRest] (get Method) QueryString = [" + fullUri.getQuery() + "]");
}
break;
case PUT:
case POST:
//assignPostBody(dataObject, contentType, vo.getEncode(), method);
assignPostBody(dataContent, contentType, vo.getEncode(), method);
break;
default:
break;
}
if (logger.isDebugEnabled()) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") method.getParams()=["
+ method.getEntity() + "] ");
}
OAuth2AccessTokenVO accessToken = null;
if (useAdapterToken) {
AccessTokenManagerByDB tokenManager = AccessTokenManagerByDB.getInstance();
accessToken = (OAuth2AccessTokenVO) tokenManager.getAccessTokenVO(vo.getAdapterGroupName());
// 토큰이 없거나 만료 됐으면 재발급
if (accessToken == null || accessToken.isExpired()) {
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
accessToken = (OAuth2AccessTokenVO) tokenManager.retryAccessTokenVO(vo.getAdapterGroupName(), prop,
oldToken);
}
if (logger.isDebug()) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") TOKEN = ["
+ accessToken + "]");
}
setAuthHeaders(method, accessToken);
if (logger.isDebug()) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
+ ") RequestHeader [Authorization=" + method.getHeader("Authorization") + "]");
}
}
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
if(!StringUtils.isBlank(authToken)) {
setAuthHeaders(method, authToken);
}
// 전달 header 셋팅
// Adapter 레벨 header 보다 data로 넘어온 httpHeader를 우선 적용(header명이 같다면 덮어씀)
assignRequestHeaders(method, httpHeader);
int status = -1;
try {
byte[] responseMessage = null;
Header[] responseHeaders;
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
if (dataContent != null) { //dataObject
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = ["
+ dataContent.toString() + "]"); //dataObject.toJSONString()
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(dataContent.toString())); //dataObject.toJSONString()
} else {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" + sendData
+ "]");
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(sendData));
}
}
StopWatch stopWatch = new StopWatch();
stopWatch.start();
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
}
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
HttpContext context = new BasicHttpContext();
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME, vo.getAdapterGroupName());
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_NAME, vo.getAdapterName());
Integer logProcessNo = (Integer) tempProp.get(HttpClientAdapterServiceKey.LOG_PROCESS_NO);
if (logProcessNo != null && logProcessNo > 0) {
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
}
try {
if (logger.isDebug()) {
logger.debug("[method getName]" + method.getMethod());
logger.debug("[method getEntity]" + method.getEntity());
logger.debug("[method getRequestHeaders]" + method.getHeaders().toString());
logger.debug("[method getURI]" + method.getPath());
}
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
status = response.getCode();
if (status >= 200 && status <= 207) {
status = 200;
}
// if (status == 404) {
// status = 200;
// }
responseMessage = EntityUtils.toByteArray(response.getEntity());
responseHeaders = response.getHeaders();
if (logger.isDebug()) {
logger.debug("[received status]" + status);
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]");
}
}
} catch (ConnectException e) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
}
throw new Exception("excuteMethod java.net.ConnectException = " + e.getMessage());
}
stopWatch.stop();
/**
* @formatter:off
* OAuth 토큰 응답 체크(Adapter properties로 설정)
* 응답코드에 따라 유효한 토큰 확인(토큰 재발급 여부 확인)
* API 서비스 마다 정책이 다름(보통 400대에서 체크하나 200에서도 체크할 수 있음)
*
* TOKEN_ERROR_CODE_KEY: 응답 json error code key
* __ex) $.dataHeader.resultCode
* TOKEN_ERROR_CODE_VALUES: 토큰 재발급이 필요한 응답 error codes(ex: O0001,O0002,O0003)
* TOKEN_ERROR_HTTP_STATUS_CODE: 보통 400대에서 체크하나 API 제공자에 따라 200에서도 체클할수 있음
* ex) TOKEN_ERROR_HTTP_STATUS_CODE = 200
* @formatter:on
*/
boolean needReissue = false;
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders);
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 (status >= 200 && status <= 207) {
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
}
} else if (status == 302) {
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
relayResponseHeaderKeys += ",Location";
}
} else {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseString);
// logger.error(errMsg);
if (status >= 400 && status < 500) {
if (useAdapterToken) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
}
// if (!needReissue) {
// throw new Exception(errMsg);
// }
} else {
throw new Exception(errMsg);
}
}
// ----------------------------------------------------------
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
logger.debug("HttpClientAdapterServiceRest] RECV " + vo.getEncode() + "(" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
// logger.debug("HttpClientAdapterServiceRest] RECV MS949 (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "MS949") + "]");
// logger.debug("HttpClientAdapterServiceRest] RECV euc-kr (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "euc-kr") + "]");
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = " + CommonLib.getDumpMessage(responseMessage));
}
if (useAdapterToken) {
if (responseMessage == null) {
throw new Exception("responseMessage is NULL, HttpStatus=" + status);
}
// OAuth 토큰 재요청 코드 확인
if (needReissue) {
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] retry access token response code = ["
+ vo.getResponseType() + "] message = [" + new String(responseMessage, vo.getEncode())
+ "]");
}
// 토큰 재발급
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
OAuth2AccessTokenVO newaccessToken = (OAuth2AccessTokenVO) tokenManager
.retryAccessTokenVO(vo.getAdapterGroupName(), prop, oldToken);
method.setHeader("Authorization", newaccessToken.getAuthorization());
setAuthHeaders(method, newaccessToken);
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
+ ") RETRY TOKEN = [" + newaccessToken + "]");
}
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method)) {
status = response.getCode();
responseMessage = EntityUtils.toByteArray(response.getEntity());
if (logger.isDebug()) {
logger.debug("[received status]" + status);
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
}
}catch (IOException e){
throw new Exception("retry excuteMethod Exceptioin = " + e.getMessage());
}
if (status == 302) {
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
relayResponseHeaderKeys += ",Location";
}
} else {
String errMsg = String.format(
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseMessage);
throw new Exception(errMsg);
}
// ----------------------------------------------------------
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
logger.debug("HttpClientAdapterServiceRest] RETRY responseType =" + vo.getResponseType());
logger.debug("HttpClientAdapterServiceRest] RETRY RECV " + vo.getEncode() + "("
+ vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode())
+ "]");
logger.debug("HttpClientAdapterServiceRest] RETRY RECV (" + vo.getAdapterGroupName() + ") = "
+ CommonLib.getDumpMessage(responseMessage));
}
// ----------------------------------------------------------
}
}
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
"RECV [" + new String(responseMessage, vo.getEncode()) + "]"
+ CommonLib.getDumpMessage(responseMessage));
}
return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), messageType,
relayResponseHeaderKeys, status, responseHeaderProp);
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
"HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
+ ste.toString(),
ste);
}
logger.error("HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
+ ste.toString(), ste);
throw ste;
} catch (ConnectException ce) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
"HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
+ ce.toString(),
ce);
}
logger.error("HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
+ ce.toString(), ce);
throw ce;
} catch (Exception e) {
if (vo.getTraceLevel() >= 3) {
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(), e.toString(), e);
}
logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
e);
throw e;
} finally {
if (status != HttpStatus.SC_OK) {
String[] msgArgs = new String[1];
msgArgs[0] = String.valueOf(status);
String resMsg = ExceptionUtil.make("RDCEAIAHA013", msgArgs);
if (logger.isDebug()) {
logger.debug(resMsg);
}
}
}
}
private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) {
Properties headerProp = new Properties();
if (responseHeaders == null || responseHeaders.length == 0) {
return headerProp;
}
for (Header header : responseHeaders) {
headerProp.put(header.getName(), header.getValue());
}
return headerProp;
}
/**
* 현재 1단계 레이아웃에만 적용되도록 구현됨.
*
* @param method
* @param responseMessage
* @param headerGroupName
* @param encode
* @param messageType
* @param relayHeaderKeys
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
private String assignResponseHeaders(HttpUriRequestBase method, byte[] responseMessage, String headerGroupName,
String encode, String messageType, String relayHeaderKeys, int status, Properties responseHeaderProp) throws Exception {
if (!MessageType.JSON.equals(messageType) || StringUtils.isBlank(headerGroupName)
|| StringUtils.isBlank(relayHeaderKeys)) {
return new String(responseMessage, encode);
}
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
JSONObject headerJson = new JSONObject();
for (String key : relayKeyArr) {
String value = responseHeaderProp.getProperty(key);
if (StringUtils.isNotBlank(value)) {
headerJson.put(key, value);
}
}
headerJson.put(HTTP_STATUS, String.valueOf(status));
if (headerJson.size() <= 0) {
return new String(responseMessage, encode);
}
JSONObject message = null;
if (responseMessage == null || responseMessage.length == 0) {
message = new JSONObject();
} else {
message = parseJson(new String(responseMessage, encode));
if (message == null) {
message = new JSONObject();
message.put("Malformed_Response_Message", new String(responseMessage, encode));
}
}
if (StringUtils.isNotBlank(headerGroupName) && message != null)
message.put(headerGroupName, headerJson);
logger.info("------------- message --------------- : {} ", message);
return message.toJSONString();
}
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
private void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
method.setHeader("Authorization",
String.format("%s", authorization));
}
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
method.setHeader("Authorization",
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
}
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
String encode) {
if (responseMessage == null) {
return false;
}
if (StringUtils.isBlank(tokenErrorCodeKey)) {
return false;
}
if (StringUtils.isBlank(tokenErrorCodeValues)) {
return false;
}
try {
DocumentContext jsonContext = JsonPath.parse(new String(responseMessage, encode));
String responseCode = jsonContext.read(tokenErrorCodeKey);
if (StringUtils.isBlank(responseCode)) {
return false;
}
String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenErrorCodeValues, ",");
return ArrayUtils.contains(arr, responseCode);
} catch (Exception e) {
logger.error("HttpClientAdapterServiceRest] checkTokenRetry error=" + e.getMessage());
}
return false;
}
private void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader) {
if (httpHeader == null) {
return;
}
if (httpHeader instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) httpHeader;
for (Object key : jsonObject.keySet()) {
Object obj = jsonObject.get(key);
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
continue;
}
method.setHeader((String) key, (String) obj);
if (logger.isDebugEnabled()) {
logger.debug("Request Header :" + (String) key + "=[" + (String) obj + "]");
}
}
}
}
@SuppressWarnings("deprecation")
private void assignPostBody(Object eaiBody, String contentType, String charset, HttpUriRequestBase method) {
if (eaiBody == null) {
return;
}
if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) {
if (eaiBody instanceof JSONObject) {
List<NameValuePair> params = new ArrayList<>();
JSONObject jsonObject = (JSONObject) eaiBody;
for (Object key : jsonObject.keySet()) {
params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key)));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, StandardCharsets.UTF_8);
method.setEntity(entity);
}
} else {
ContentType contentTypeObj = ContentType.create(contentType, charset);
// 요청 본문을 StringEntity 객체로 생성
StringEntity entity = new StringEntity(eaiBody.toString(), contentTypeObj);
// 요청에 본문 추가
method.setEntity(entity);
}
}
private HashMap<String, String> getParameters(Object message) throws Exception {
HashMap<String, String> result = new HashMap<String, String>();
if (message != null) {
if (message instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) message;
for (Object key : jsonObject.keySet()) {
Object obj = jsonObject.get(key);
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
continue;
}
result.put((String) key, getStringValue(obj));
}
} else {
String[] messages = ((String) message).split("&");
String[] data = null;
for (int i = 0; i < messages.length; i++) {
data = messages[i].split("=", 2);
if (data.length == 2) {
result.put(data[0], data[1]);
}
}
}
}
return result;
}
public String getStringValue(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof String) {
return (String) obj;
} else if (obj instanceof Long) {
return Long.toString((Long) obj);
} else if (obj instanceof BigDecimal) {
return ((BigDecimal) obj).toPlainString();
} else {
return (String) obj;
}
}
public static Object parseJsonGeneric(String jsonData) {
if (StringUtils.isBlank(jsonData)) {
return null;
}
try {
JSONParser parser = new JSONParser();
Object parsedObj = parser.parse(jsonData);
return parsedObj;
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("deprecation")
private JSONObject parseJson(String message) throws Exception {
if (message == null) {
return null;
}
return (JSONObject) JSONValue.parse(message);
}
private Document convertXmlDocument(String message) throws Exception {
SAXReader builder = new SAXReader();
Document document = builder.read(new StringReader(message));
return document;
}
@SuppressWarnings("deprecation")
private String changeUrl(String messageType, String url, String restOption, Object sendData) {
if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) {
try {
if (StringUtils.isBlank(restOption)) {
return url;
}
List<String> urlVariableList = new ArrayList<String>();
JSONObject restOptionObject = parseJson(restOption);
if (restOptionObject == null)
throw new Exception("restOption is NULL");
String type = (String) restOptionObject.get("type");
String requestExtraPath = (String) restOptionObject.get("extraPath");
url = getUrl(url, requestExtraPath);
if (TYPE_VARIABLE_URL_REQUEST.equals(type)) {
Pattern p = Pattern.compile("\\{(.*?)\\}");
Matcher m = p.matcher(requestExtraPath);
List<String> uriVariables = new ArrayList<>();
while(m.find()) {
String fieldName = m.group(1);
uriVariables.add(fieldName);
}
if (MessageType.JSON.equals(messageType)) {
JSONObject jsonObject;
if (sendData instanceof JSONObject) {
jsonObject = (JSONObject) sendData;
} else {
jsonObject = (JSONObject) JSONValue.parse((String) sendData);
}
for (Object tempObject : uriVariables) {
String urlVaribleId = (String) tempObject;
String uriVariable = (String) jsonObject.get(urlVaribleId);
urlVariableList.add(uriVariable);
jsonObject.remove(urlVaribleId);
}
} else if (MessageType.XML.equals(messageType)) {
Document doc;
if (sendData instanceof Document) {
doc = (Document) sendData;
} else {
doc = convertXmlDocument((String) sendData);
}
for (Object tempObject : uriVariables) {
String urlVaribleId = (String) tempObject;
Element element = (Element) doc.selectSingleNode("//" + urlVaribleId);
urlVariableList.add(element.getText());
if (doc.getRootElement() == element) {
doc = null;
} else {
element.getParent().remove(element);
}
}
}
if (urlVariableList.size() > 0) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(url).build();
Object[] urls = urlVariableList.toArray();
url = uriComponents.expand(urls).toUriString();
logger.debug("HttpClientAdapterServiceRest] after extand url=[" + url + "] ");
}
}
} catch (Exception e) {
logger.error("Failed to change url", e);
}
}
return url;
}
private String getUrl(String baseUrl, String extraPath) {
if (StringUtils.isBlank(extraPath)) {
return baseUrl;
}
if (StringUtils.contains(extraPath, "http://") || StringUtils.contains(extraPath, "https://")) {
return extraPath;
}
String targetUrl = baseUrl;
if (!targetUrl.endsWith("/")) {
targetUrl += "/";
}
if (extraPath.startsWith("/")) {
targetUrl += extraPath.substring(1);
} else {
targetUrl += extraPath;
}
logger.debug("HttpClientAdapterServiceRest] after concatenate url=[" + targetUrl + "] ");
return targetUrl;
}
}
@@ -85,4 +85,11 @@ public interface HttpAdapterServiceKey {
//응답 처리 용 표준 전문 오브젝트 //응답 처리 용 표준 전문 오브젝트
static final String STANDARD_MESSAGE_OBJECT = "STANDARD_MESSAGE_OBJECT"; static final String STANDARD_MESSAGE_OBJECT = "STANDARD_MESSAGE_OBJECT";
//송신(Outbound) 요청 표준 전문 오브젝트
static final String OUT_REQ_STD_MSG = "OUT_REQ_STD_MSG";
// 어댑터별 인증 키 헤더 이름
static final String ADAPTER_TOKEN_HEADER_NAME = "ADAPTER_TOKEN_HEADER_NAME";
static final String ADAPTER_APIKEY_HEADER_NAME = "ADAPTER_APIKEY_HEADER_NAME";
} }
@@ -6,7 +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 org.json.simple.JSONObject;
import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager; import com.eactive.eai.adapter.AdapterManager;
@@ -18,8 +18,12 @@ 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.TransactionContextKeys;
import com.eactive.eai.common.server.EAIServerManager;
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; import com.eactive.eai.common.util.UUIDGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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,20 +31,25 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
public static final String HEADER_NAME_CLIENT_ID = "x-elink-client-id"; public static final String HEADER_NAME_CLIENT_ID = "x-elink-client-id";
public static final String PROPERTIES_NAME_CLIENT_ID = "clientId"; public static final String PROPERTIES_NAME_CLIENT_ID = "clientId";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private ObjectMapper mapper = new ObjectMapper();
EAIServerManager eaiServerManager;
String instid = null;
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); String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
boolean bMDCput = false; boolean bMDCput = false;
if(uuid == null) { if(uuid == null) {
uuid = UUIDGenerator.getUUID().toString().replaceAll("-", ""); if(instid == null) {
eaiServerManager = EAIServerManager.getInstance();
instid = eaiServerManager.getGroupInstId();
}
uuid = instid + UUIDGenerator.getUUID().toString().replaceAll("-", "");
prop.setProperty(TransactionContextKeys.TRANSACTION_UUID, uuid); prop.setProperty(TransactionContextKeys.TRANSACTION_UUID, uuid);
} }
if(MDC.get(Logger.DISCRIMINATOR) == null) { bMDCput = TxSiftContext.begin(uuid);
MDC.put(Logger.DISCRIMINATOR, uuid);
bMDCput = true;
}
try { try {
String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID); String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID);
@@ -65,6 +74,13 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
Object obj = null; Object obj = null;
try { try {
message = doPreFilters(adptGrpName, adptName, message, prop, request, response); message = doPreFilters(adptGrpName, adptName, message, prop, request, response);
if (message instanceof JSONObject) {
message = ((JSONObject) message).toJSONString();
} else if (message instanceof JsonNode) {
message = mapper.writeValueAsString((JsonNode) message);
}
obj = RequestDispatcher.getRequestDispatcher(adptGrpName).handle(adptName, message, prop); obj = RequestDispatcher.getRequestDispatcher(adptGrpName).handle(adptName, message, prop);
obj = doPostFilters(adptGrpName, adptName, obj, prop, request, response); obj = doPostFilters(adptGrpName, adptName, obj, prop, request, response);
} catch (HttpStatusException e) { // inbound error } catch (HttpStatusException e) { // inbound error
@@ -72,13 +88,13 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
throw e; throw e;
} catch (JwtAuthException e) { // filter error } catch (JwtAuthException e) { // filter error
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e); logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response, // UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
"RECEAIIRP202", e); // "RECEAIIRP202", e);
throw e; throw e;
} catch (Exception e) { // filter error } catch (Exception e) { // filter error
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e); logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response, // UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
"RECEAIIRP201", e); // "RECEAIIRP201", e);
throw e; throw e;
} }
@@ -88,12 +104,16 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
return (byte[]) obj; return (byte[]) obj;
} else if (obj instanceof String) { } else if (obj instanceof String) {
return (String) obj; return (String) obj;
} else if (obj instanceof JSONObject) {
return ((JSONObject) obj).toJSONString();
} else if (obj instanceof JsonNode) {
return (JsonNode) obj;
} else { } else {
throw new Exception("RECEAIAHA001"); throw new Exception("RECEAIAHA001");
} }
} finally { } finally {
if(bMDCput) if(bMDCput)
MDC.remove(Logger.DISCRIMINATOR); TxSiftContext.end();
} }
} }
@@ -22,10 +22,10 @@ import com.eactive.eai.inbound.error.InboundErrorInfoVO;
import com.eactive.eai.inbound.error.InboundErrorKeys; import com.eactive.eai.inbound.error.InboundErrorKeys;
import com.eactive.eai.util.HexaConverter; import com.eactive.eai.util.HexaConverter;
public class UnkownMessageLogUtils { public class UnknownMessageLogUtils {
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
private UnkownMessageLogUtils() { private UnknownMessageLogUtils() {
throw new IllegalStateException("Utility class"); throw new IllegalStateException("Utility class");
} }
@@ -49,7 +49,7 @@ public class UnkownMessageLogUtils {
StringBuffer sb = new StringBuffer(); StringBuffer sb = new StringBuffer();
sb.append(errorMsg).append("\n").append("Remote Addr : ").append(request.getRemoteAddr()).append("\n").append("Request URI : ") sb.append(errorMsg).append("\n").append("Remote Addr : ").append(request.getRemoteAddr()).append("\n").append("Request URI : ")
.append(request.getRequestURI()).append("\n").append("clientId=").append(clientId).append(",hexClientId=").append(hexClientId).append(",txProp=").append(prop).append("\n").append("Exception : ").append(e.getMessage()); .append(request.getRequestURI()).append("\n").append("clientId=").append(clientId).append(",hexClientId=").append(hexClientId).append(",txProp=").append(prop).append("\n").append("Exception : ").append(e.getMessage());
UnkownMessageLogUtils.logUnknownMessage(txId, adptGrpName, adptName, UnknownMessageLogUtils.logUnknownMessage(txId, adptGrpName, adptName,
"", "", false, errCode, sb.toString(), System.currentTimeMillis(), "", "", false, errCode, sb.toString(), System.currentTimeMillis(),
serverName, InboundErrorKeys.IN_UNKNOWN, message); serverName, InboundErrorKeys.IN_UNKNOWN, message);
} catch (Exception ex) { } catch (Exception ex) {
@@ -1,19 +1,17 @@
package com.eactive.eai.adapter.http.dynamic.filter; package com.eactive.eai.adapter.http.dynamic.filter;
import com.eactive.eai.adapter.http.HttpStatusException; import java.util.Properties;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport; import javax.servlet.http.HttpServletRequest;
import com.eactive.eai.common.util.Logger; import javax.servlet.http.HttpServletResponse;
import com.eactive.eai.util.IpUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest; import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import javax.servlet.http.HttpServletResponse; import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import java.util.Properties; import com.eactive.eai.common.util.Logger;
import java.util.regex.Matcher; import com.eactive.eai.util.IpUtil;
import java.util.regex.Pattern;
public class AdapterAllowIPListFilter implements HttpAdapterFilter { public class AdapterAllowIPListFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
@@ -30,9 +28,9 @@ public class AdapterAllowIPListFilter implements HttpAdapterFilter {
} }
if (!IpUtil.isMatchIp(allowedIps, requestIp)) { if (!IpUtil.isMatchIp(allowedIps, requestIp)) {
throw new HttpStatusException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value()); throw new FilterException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value());
} }
} catch (HttpStatusException e) { } catch (FilterException e) {
logger.debug(e.getMessage()); logger.debug(e.getMessage());
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@@ -8,14 +8,12 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.IpUtil; import com.eactive.eai.util.IpUtil;
public class AdapterBlockIpListFilter implements HttpAdapterFilter { public class AdapterBlockIpListFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
@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 {
@@ -28,9 +26,9 @@ public class AdapterBlockIpListFilter implements HttpAdapterFilter {
} }
if (IpUtil.isMatchIp(blockIps, requestIp)) { if (IpUtil.isMatchIp(blockIps, requestIp)) {
throw new HttpStatusException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value()); throw new FilterException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value());
} }
} catch (HttpStatusException e) { } catch (FilterException e) {
logger.debug(e.getMessage()); logger.debug(e.getMessage());
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@@ -46,9 +44,9 @@ public class AdapterBlockIpListFilter implements HttpAdapterFilter {
if (ipAddress == null) { if (ipAddress == null) {
ipAddress = request.getRemoteAddr(); ipAddress = request.getRemoteAddr();
} }
if(ipAddress.indexOf(",") >= 0) { if (ipAddress.indexOf(",") >= 0) {
ipAddress = org.springframework.util.StringUtils.tokenizeToStringArray(ipAddress, ",")[0]; ipAddress = org.springframework.util.StringUtils.tokenizeToStringArray(ipAddress, ",")[0];
} }
return ipAddress; return ipAddress;
} }
@@ -1,5 +1,26 @@
package com.eactive.eai.adapter.http.dynamic.filter; package com.eactive.eai.adapter.http.dynamic.filter;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport; import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.authserver.service.BearerTokenInfo; import com.eactive.eai.authserver.service.BearerTokenInfo;
//import com.eactive.eai.authserver.vo.BearerTokenInfo; //import com.eactive.eai.authserver.vo.BearerTokenInfo;
@@ -13,7 +34,6 @@ import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.session.SessionManager; import com.eactive.eai.common.session.SessionManager;
import com.eactive.eai.common.stdmessage.STDMessageManager; import com.eactive.eai.common.stdmessage.STDMessageManager;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.StringUtil;
import com.eactive.eai.inbound.action.ActionFactory; import com.eactive.eai.inbound.action.ActionFactory;
import com.eactive.eai.inbound.action.RequestAction; import com.eactive.eai.inbound.action.RequestAction;
import com.eactive.eai.inbound.processor.Processor; import com.eactive.eai.inbound.processor.Processor;
@@ -24,31 +44,13 @@ import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.util.IOUtils; import com.nimbusds.jose.util.IOUtils;
import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.SignedJWT;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
public class ApiAuthFilter implements HttpAdapterFilter { public class ApiAuthFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer"; public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath"; public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";
public static final String PASS_SCOPE_LIST = "pass.scope.list";
public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL"; public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL";
public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL"; public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL";
@@ -58,7 +60,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";
private JWSVerifier jwsVerifier; private JWSVerifier jwsVerifier;
private String[] passScopeArr = {};
// // CA 토큰 저장소 // // CA 토큰 저장소
// private final Map<String, BearerTokenInfo> CATokenStore = new ConcurrentHashMap<>(); // private final Map<String, BearerTokenInfo> CATokenStore = new ConcurrentHashMap<>();
@@ -88,22 +90,32 @@ public class ApiAuthFilter implements HttpAdapterFilter {
} catch (IOException | NoSuchAlgorithmException | RuntimeException | InvalidKeySpecException e) { } catch (IOException | NoSuchAlgorithmException | RuntimeException | InvalidKeySpecException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
String passScope = vo.getProperty(PASS_SCOPE_LIST, "oob,public");
passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
} }
@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");
if (StringUtils.isBlank(apiId)) { String eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info"); if(StringUtils.isEmpty(eaiSvcCd)) {
if(StringUtils.isEmpty(apiId))
apiId = assignApiId(adptGrpName, adptName, message, prop, request);
if (StringUtils.isBlank(apiId)) {
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info");
}
/**
* TODO: 중복되는 표준메시지 조회로 리소드 낭비가 있을 수 있으므로 체크 자체를 추후 RequestProcessor혹은 RestProcessor로 이동 필요 해 보임
* ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음
*/
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
eaiSvcCd = StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
} }
/**
* TODO: 중복되는 표준메시지 조회로 리소드 낭비가 있을 수 있으므로 체크 자체를 추후 RequestProcessor혹은 RestProcessor로 이동 필요 해 보임
* ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음
*/
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
String 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);
@@ -116,7 +128,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
switch (eaiMessage.getAuthType()){ switch (eaiMessage.getAuthType()){
case "oauth": case "oauth":
try { try {
String token = extractBearerToken(request); String token = extractBearerToken(request, prop);
SignedJWT signedJWT = SignedJWT.parse(token); SignedJWT signedJWT = SignedJWT.parse(token);
if (signedJWT.getJWTClaimsSet().getExpirationTime() == null) { if (signedJWT.getJWTClaimsSet().getExpirationTime() == null) {
@@ -135,26 +147,32 @@ public class ApiAuthFilter implements HttpAdapterFilter {
OAuth2Manager manager = OAuth2Manager.getInstance(); OAuth2Manager manager = OAuth2Manager.getInstance();
HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId); HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
if (scopeArr == null || scopeArr.length == 0) {
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
}
for (String scope : scopeArr) { // API에 scope 설정이 안되어 있는 경우, scope 체크를 pass하고 나중에 client API 맵을 체크한다.
if (StringUtils.equals(scope, "oob") || StringUtils.equals(scope, "public")) { if(CollectionUtils.isEmpty(scopeSet))
isPassScope = true; isPassScope = true;
}
}
if (!isPassScope) { if(!isPassScope) {
if (!verifyScope(scopeSet, signedJWT)) { String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
if (scopeArr == null || scopeArr.length == 0) {
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL, throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString())); String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
} }
for (String scope : scopeArr) {
if (StringUtils.equals(scope, "oob") || StringUtils.equals(scope, "public")) {
isPassScope = true;
}
}
if (!isPassScope) {
if (!verifyScope(scopeSet, signedJWT)) {
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
}
}
} }
// header 값으로 전송된 clientId와 토큰 소유 clientId check // header 값으로 전송된 clientId와 토큰 소유 clientId check
if (!verifyClientId(prop, signedJWT)) { if (!verifyClientId(prop, signedJWT)) {
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "client_id not matched(header/token)"); throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "client_id not matched(header/token)");
@@ -168,8 +186,23 @@ public class ApiAuthFilter implements HttpAdapterFilter {
} }
break; break;
case "api_key": case "api_key":
String apiKeyName = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key"); // 기관별로 API Key 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_TOKEN_HEADER_NAME)을 우선 사용하고
String apiKey = request.getHeader(apiKeyName); // 없으면 ApiConfig 전역 설정(api.key.name)을 사용. 콤마로 다중 헤더명 지정 시 순서대로 값이 있는 헤더를 사용
String apiKeyNameConf = prop.getProperty(HttpAdapterServiceKey.ADAPTER_APIKEY_HEADER_NAME);
if (StringUtils.isBlank(apiKeyNameConf)) {
apiKeyNameConf = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
}
String[] apiKeyNames = org.springframework.util.StringUtils.tokenizeToStringArray(apiKeyNameConf, ",");
String apiKeyName = null;
String apiKey = null;
for (String name : apiKeyNames) {
String value = request.getHeader(name);
if (StringUtils.isNotBlank(value)) {
apiKeyName = name;
apiKey = value.trim();
break;
}
}
if(apiKey == null){ if(apiKey == null){
// QueryString으로 전달된 access_token 파라미터 확인 // QueryString으로 전달된 access_token 파라미터 확인
String apiKeyParamName = PropManager.getInstance().getProperty("ApiConfig", "api.key.param.name", "x-api-key"); String apiKeyParamName = PropManager.getInstance().getProperty("ApiConfig", "api.key.param.name", "x-api-key");
@@ -177,14 +210,14 @@ public class ApiAuthFilter implements HttpAdapterFilter {
if (StringUtils.isNotBlank(queryApiKey)) { if (StringUtils.isNotBlank(queryApiKey)) {
apiKey = queryApiKey.trim(); apiKey = queryApiKey.trim();
} else { } else {
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyName+"\" in Http Header"); throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyNameConf+"\" in Http Header");
} }
} }
prop.setProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, apiKey); prop.setProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, apiKey);
isPassScope = true; isPassScope = true;
break; break;
case "ca": case "ca":
String token = extractBearerToken(request); String token = extractBearerToken(request, prop);
BearerTokenInfo bearerTokenInfo = SessionManager.getInstance().getCAToken(token); BearerTokenInfo bearerTokenInfo = SessionManager.getInstance().getCAToken(token);
if ( bearerTokenInfo != null ) { if ( bearerTokenInfo != null ) {
if( bearerTokenInfo.isExpired() ) { if( bearerTokenInfo.isExpired() ) {
@@ -288,9 +321,6 @@ public class ApiAuthFilter implements HttpAdapterFilter {
return false; return false;
} }
String passScope = "oob,public";
String[] passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
for (String scope : scopeArr) { for (String scope : scopeArr) {
for (String pScope : passScopeArr) { for (String pScope : passScopeArr) {
if ( StringUtils.equalsAny(scope, pScope)) { if ( StringUtils.equalsAny(scope, pScope)) {
@@ -305,8 +335,13 @@ public class ApiAuthFilter implements HttpAdapterFilter {
return false; return false;
} }
public static String extractBearerToken(HttpServletRequest request) throws JwtAuthException { public static String extractBearerToken(HttpServletRequest request, Properties prop) throws JwtAuthException {
String tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization"); // 기관별로 토큰 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_APPKEY_HEADER_NAME)을 우선 사용하고
// 없으면 ApiConfig 전역 설정(token.header.name)을 사용
String tokenHeaderName = prop != null ? prop.getProperty(HttpAdapterServiceKey.ADAPTER_TOKEN_HEADER_NAME) : null;
if (StringUtils.isBlank(tokenHeaderName)) {
tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization");
}
String authorization = request.getHeader(tokenHeaderName); String authorization = request.getHeader(tokenHeaderName);
if (StringUtils.isBlank(authorization)) { if (StringUtils.isBlank(authorization)) {
// QueryString으로 전달된 access_token 파라미터 확인 // QueryString으로 전달된 access_token 파라미터 확인
@@ -0,0 +1,96 @@
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 org.springframework.http.HttpStatus;
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 FilterException("path not found - " + requestPath, ERROR_PRE_FAIL, HttpStatus.FORBIDDEN.value());
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);
}
}
@@ -0,0 +1,33 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.eactive.eai.common.util.Logger;
public class DebugLoggerInFilter implements HttpAdapterFilter {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("Inbound Filter RCV adptGrpName : {}", adptGrpName);
logger.debug("Inbound Filter RCV adptName : {}", adptName);
logger.debug("Inbound Filter RCV message : {}", message);
logger.debug("Inbound Filter RCV prop : {}", prop);
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("Inbound Filter SND adptGrpName : {}", adptGrpName);
logger.debug("Inbound Filter SND adptName : {}", adptName);
logger.debug("Inbound Filter SND resultMessage : {}", resultMessage);
logger.debug("Inbound Filter SND prop : {}", prop);
return resultMessage;
}
}
@@ -0,0 +1,18 @@
package com.eactive.eai.adapter.http.dynamic.filter;
@SuppressWarnings("serial")
public class FilterCryptoException extends FilterException {
public FilterCryptoException(String msg) {
super(msg);
}
public FilterCryptoException(String msg, int status) {
super(msg, status);
}
public FilterCryptoException(String msg, String code, int status) {
super(msg, code, status);
}
public FilterCryptoException(String msg, String code, int status, Throwable cause) {
super(msg, code, status, cause);
}
}
@@ -0,0 +1,21 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import com.eactive.eai.adapter.http.HttpStatusException;
@SuppressWarnings("serial")
public class FilterException extends HttpStatusException {
public FilterException(String msg) {
super(msg);
}
public FilterException(String msg, int status) {
super(msg, status);
}
public FilterException(String msg, String code, int status) {
super(msg, code, status);
}
public FilterException(String msg, String code, int status, Throwable cause) {
super(msg, code, status, cause);
}
}
@@ -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();
} }
@@ -7,6 +7,9 @@ import javax.servlet.http.HttpServletResponse;
public interface HttpAdapterFilter { public interface HttpAdapterFilter {
public static final String ERROR_PRE_FAIL = "E.PREFILTER_FAIL";
public static final String ERROR_POST_FAIL = "E.POSTFILTER_FAIL";
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;
@@ -25,13 +25,19 @@ import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
*/ */
public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterFilter { public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterFilter {
/** public static final String ERROR_DEC_FAIL = "E.DECRYPT_FAIL";
public static final String ERROR_ENC_FAIL = "E.ENCRYPT_FAIL";
/**
* DYNAMIC 키 도출용 컨텍스트를 구성한다. * DYNAMIC 키 도출용 컨텍스트를 구성한다.
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다. * STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
* 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다. * 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다.
*/ */
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) { protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
return new HashMap<>(); Map<String, String> context = new HashMap<>();
for (String key : prop.stringPropertyNames()) {
context.put(key, prop.getProperty(key));
}
return context;
} }
/** /**
@@ -43,7 +49,9 @@ public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterF
if (StringUtils.isBlank(headerName) || request == null) { if (StringUtils.isBlank(headerName) || request == null) {
return null; return null;
} }
String headerValue = request.getHeader(headerName); Properties inboundHeaderProp = (Properties)prop.get("INBOUND_HEADER");
String headerValue = inboundHeaderProp.getProperty(headerName);
// String headerValue = request.getHeader(headerName);
return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8); return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
} }
@@ -0,0 +1,67 @@
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.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.JsonPathUtil;
import com.fasterxml.jackson.databind.JsonNode;
public class JsonToSetStatusFilter implements HttpAdapterFilter {
private static final String PROPGROUP = "JsonToSetStatusFilter";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
/** HTTP 상태코드 유효 범위 (RFC 7231) */
private static final int MIN_HTTP_STATUS = 100;
private static final int MAX_HTTP_STATUS = 599;
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// do nothing
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String fieldName = "";
String statusParam = "";
try {
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
fieldName = getFieldName(adptGrpName);
if (rootNode != null && StringUtils.isNotBlank(fieldName) && rootNode.has(fieldName)) {
statusParam = rootNode.get(fieldName).asText();
int httpStatus = Integer.parseInt(statusParam);
if (isValidHttpStatus(httpStatus)) {
response.setStatus(httpStatus);
} else {
logger.warn("유효하지 않은 HTTP 상태코드. 상태코드를 설정하지 않음. fieldName={}, value={}", fieldName, statusParam);
}
} else {
logger.warn("설정과 맞지 않는 메시지. 상태코드를 설정하지 않음. fieldName={}", fieldName);
}
} catch (Exception e) {
logger.warn("상태코드 추출 실패. fieldName={}, value={}", fieldName, statusParam, e);
}
return resultMessage;
}
/** HTTP 상태코드로 사용 가능한 값인지 확인한다. (100 ~ 599) */
private boolean isValidHttpStatus(int httpStatus) {
return httpStatus >= MIN_HTTP_STATUS && httpStatus <= MAX_HTTP_STATUS;
}
private String getFieldName(String adptGrpName) {
return PropManager.getInstance().getProperty(PROPGROUP, adptGrpName);
}
private JsonNode parseJson(String adptGrpName, Object message) throws Exception {
return JsonPathUtil.toTree(message);
}
}
@@ -51,13 +51,6 @@ public class ReflectAllHeaderFilter 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 {
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("doPreFilter ReflectAllHeaderFilter Start."); logger.debug("doPreFilter ReflectAllHeaderFilter Start.");
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim(); String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
@@ -89,6 +82,45 @@ public class ReflectAllHeaderFilter implements HttpAdapterFilter {
} }
logger.debug("doPreFilter ReflectAllHeaderFilter End."); logger.debug("doPreFilter ReflectAllHeaderFilter End.");
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("doPostFilter ReflectAllHeaderFilter Start.");
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);
}
try {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
if( StringUtils.equalsAnyIgnoreCase( headerName, HttpHeaderRelayBlackList ) ) {
logger.debug("Skip Processing Key ["+headerName+"], value ["+headerValue+"] in HttpHeaderRelayBlackList");
continue;
}
response.setHeader(headerName, headerValue);
}
} catch (Exception e) {
logger.error(e.getMessage());
}
logger.debug("doPostFilter ReflectAllHeaderFilter End.");
return resultMessage; return resultMessage;
} }
@@ -0,0 +1,263 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
/**
* 수신 Http Header 중 <b>허용 목록(white list)에 등록된 헤더만</b> 응답 헤더로 복사하는 Inbound Adapter Filter.
*
* <p>모든 헤더를 복사하고 제외 목록으로 걸러내는 {@link ReflectAllHeaderFilter} 와 달리,
* 복사 대상을 어댑터 그룹별로 명시하는 방식이다.
*
* <p>설정은 <code>HttpHeaderFilter</code> property 그룹에 등록하며, 아래 순서로 가장 먼저 찾은 키 하나만 사용한다.
* (합집합이 아니라 override 이므로 어댑터 그룹 키를 지정하면 전역 키는 무시된다.)
*
* <pre>
* 1. ReflectHeaderFilter.whiteList.{어댑터그룹명} 어댑터 그룹 단위
* 2. ReflectHeaderFilter.whiteList 전역 기본값
* </pre>
*
* <p>값은 콤마로 구분한 헤더명 목록이며 대소문자를 구분하지 않는다.
* 헤더명 끝에 <code>*</code> 를 붙이면 접두사 일치로 처리한다.
*
* <pre>
* HttpHeaderFilter.ReflectHeaderFilter.whiteList = x-elink-client-id
* HttpHeaderFilter.ReflectHeaderFilter.whiteList.djbTrans = x-obp-txid, x-obp-partnercode, X-KKB-*
* </pre>
*
* <p>설정이 없거나 비어 있으면 어떤 헤더도 복사하지 않는다.
*/
public class ReflectHeaderFilter implements HttpAdapterFilter {
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
public static final String HEADER_KEY_NAMES = "ReflectHeaderFilter.whiteList";
/**
* 허용 목록에 등록되어 있어도 복사하지 않는 헤더.
* 요청측 값이 응답 본문/커넥션과 불일치하면 응답 자체가 깨지므로 설정으로 열 수 없게 한다.
*/
private static final Set<String> NEVER_REFLECT = createHeaderSet(
"Content-Length",
"Transfer-Encoding",
"Connection",
"Keep-Alive",
"Upgrade",
"TE",
"Trailer");
/** 어댑터 그룹별 허용 목록 캐시. 필터 인스턴스는 HttpAdapterFilterFactory 에서 싱글톤으로 공유된다. */
private final ConcurrentHashMap<String, CachedWhiteList> whiteListCache = new ConcurrentHashMap<String, CachedWhiteList>();
public ReflectHeaderFilter() {
super();
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("doPreFilter ReflectHeaderFilter Start.");
reflectHeaders(adptGrpName, request, response);
logger.debug("doPreFilter ReflectHeaderFilter End.");
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.debug("doPostFilter ReflectHeaderFilter Start.");
reflectHeaders(adptGrpName, request, response);
logger.debug("doPostFilter ReflectHeaderFilter End.");
return resultMessage;
}
private void reflectHeaders(String adptGrpName, HttpServletRequest request, HttpServletResponse response) {
WhiteList whiteList = getWhiteList(adptGrpName);
if (whiteList.isEmpty()) {
logger.debug("No reflect header configured for adapter group [" + adptGrpName + "]. Skip all.");
return;
}
try {
for (String headerName : whiteList.getNames()) {
String headerValue = request.getHeader(headerName);
if (headerValue == null) {
continue;
}
setHeader(headerName, headerValue, response);
}
// 접두사(*) 설정이 있을 때만 수신 헤더를 순회한다.
if (whiteList.hasPrefix()) {
java.util.Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames != null && headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (!whiteList.matchesPrefix(headerName)) {
continue;
}
setHeader(headerName, request.getHeader(headerName), response);
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
private void setHeader(String headerName, String headerValue, HttpServletResponse response) {
if (NEVER_REFLECT.contains(headerName)) {
logger.debug("Skip Processing Key [" + headerName + "] in NEVER_REFLECT list");
return;
}
// 헤더명/값에 CR, LF 가 있으면 응답 분할(response splitting) 위험이 있다.
if (containsCrLf(headerName) || containsCrLf(headerValue)) {
logger.error("Skip Processing Key [" + headerName + "] - CR/LF detected in header name or value");
return;
}
response.setHeader(headerName, headerValue);
logger.debug("Processing Key [" + headerName + "], value [" + headerValue + "]");
}
private boolean containsCrLf(String value) {
return value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0);
}
/**
* 어댑터 그룹 → 전역 순으로 property 를 찾아 허용 목록을 반환한다.
* property 값이 바뀌지 않는 동안은 파싱 결과를 재사용한다.
*/
private WhiteList getWhiteList(String adptGrpName) {
String cacheKey = StringUtils.defaultString(adptGrpName);
String propValue = findPropValue(adptGrpName);
CachedWhiteList cached = whiteListCache.get(cacheKey);
if (cached != null && StringUtils.equals(cached.propValue, propValue)) {
return cached.whiteList;
}
WhiteList whiteList = parse(propValue);
whiteListCache.put(cacheKey, new CachedWhiteList(propValue, whiteList));
return whiteList;
}
private String findPropValue(String adptGrpName) {
PropManager propManager = PropManager.getInstance();
if (StringUtils.isNotBlank(adptGrpName)) {
String value = propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES + "." + adptGrpName, "");
if (StringUtils.isNotBlank(value)) {
return StringUtils.trimToEmpty(value);
}
}
return StringUtils.trimToEmpty(propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, ""));
}
private static WhiteList parse(String propValue) {
Set<String> names = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
List<String> prefixes = new ArrayList<String>();
if (StringUtils.isNotBlank(propValue)) {
for (String token : propValue.split(",")) {
String name = StringUtils.trimToEmpty(token);
if (name.isEmpty()) {
continue;
}
if (name.endsWith("*")) {
String prefix = name.substring(0, name.length() - 1);
if (!prefix.isEmpty()) {
prefixes.add(prefix);
}
continue;
}
names.add(name);
}
}
return new WhiteList(names, prefixes);
}
private static Set<String> createHeaderSet(String... names) {
Set<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
Collections.addAll(set, names);
return Collections.unmodifiableSet(set);
}
/** 파싱된 허용 목록. 헤더명 완전일치 목록과 접두사(*) 목록으로 구성된다. */
private static class WhiteList {
private final Set<String> names;
private final List<String> prefixes;
WhiteList(Set<String> names, List<String> prefixes) {
this.names = Collections.unmodifiableSet(names);
this.prefixes = Collections.unmodifiableList(prefixes);
}
boolean isEmpty() {
return names.isEmpty() && prefixes.isEmpty();
}
Set<String> getNames() {
return names;
}
boolean hasPrefix() {
return !prefixes.isEmpty();
}
boolean matchesPrefix(String headerName) {
for (String prefix : prefixes) {
if (StringUtils.startsWithIgnoreCase(headerName, prefix)) {
return true;
}
}
return false;
}
}
private static class CachedWhiteList {
private final String propValue;
private final WhiteList whiteList;
CachedWhiteList(String propValue, WhiteList whiteList) {
this.propValue = propValue;
this.whiteList = whiteList;
}
}
}
@@ -0,0 +1,71 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonToSetStatusFilter implements HttpAdapterFilter {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
JsonNode rootNode = parseJson(adptGrpName, message);
int httpStatus = 200;
if (rootNode.has("apiRsltCd") && !rootNode.get("apiRsltCd").asText().isEmpty()) {
try {
String statusParam = rootNode.get("apiRsltCd").asText();
httpStatus = Integer.parseInt(statusParam);
} catch (NumberFormatException e) {
httpStatus = 400; // 잘못된 입력일 경우 400 Bad Request 반환
}
// HTTP 상태 코드 설정
response.setStatus(httpStatus);
}
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
int httpStatus = 200;
if (rootNode.has("apiRsltCd") && !rootNode.get("apiRsltCd").asText().isEmpty()) {
try {
String statusParam = rootNode.get("apiRsltCd").asText();
httpStatus = Integer.parseInt(statusParam);
} catch (NumberFormatException e) {
httpStatus = 400; // 잘못된 입력일 경우 400 Bad Request 반환
}
// HTTP 상태 코드 설정
response.setStatus(httpStatus);
}
return resultMessage;
}
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException {
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
String orgMessageString;
if(message instanceof byte[]) {
orgMessageString = new String((byte[])message, charset);
} else {
orgMessageString = (String) message;
}
return mapper.readTree(orgMessageString);
}
}
@@ -0,0 +1,110 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class JsonToStdConverterFilter implements HttpAdapterFilter {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
JsonNode rootNode = parseJson(adptGrpName, message);
if (rootNode.has("header_part")) {
JsonNode headerPart = rootNode.path("header_part");
if (!headerPart.has("eaiIntfId") || headerPart.get("eaiIntfId").asText().isEmpty()) {
// /api/v1/public/getUserInfo.svc
String inboundUri = prop.getProperty(HttpAdapterServiceKey.INBOUND_EXTURI);
String apiPath = prop.getProperty(HttpAdapterServiceKey.API_PATH) + "/";
// getUserInfo.svc
String eaiIntfId = org.apache.commons.lang.StringUtils.removeStart(inboundUri, apiPath);
// header_part 아래 eaiIntfId가 추가 또는 수정
if (headerPart instanceof ObjectNode) {
((ObjectNode) headerPart).put("eaiIntfId", eaiIntfId);
// 요청 응답 구분코드 강제 설정 -> API명 찾아갈때 필요
if (!headerPart.has("reqRspnsDscd")) {
((ObjectNode) headerPart).put("reqRspnsDscd", "S");
}
// GUID 강제 설정
String orgnlTx = "";
if (headerPart.has("orgnlTx") && !headerPart.get("orgnlTx").asText().isEmpty()) {
orgnlTx = headerPart.get("orgnlTx").asText();
}
if (!headerPart.has("tlgrWrtnDt")) {
((ObjectNode) headerPart).put("tlgrWrtnDt", orgnlTx);
}
if (!headerPart.has("tlgrCrtnSysNm")) {
((ObjectNode) headerPart).put("tlgrCrtnSysNm", "");
}
if (!headerPart.has("tlgrSrlNo")) {
((ObjectNode) headerPart).put("tlgrSrlNo", "");
}
}
}
}
else {
return message;
}
return rootNode.toString();
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
if (rootNode.has("header_part")) {
JsonNode headerPart = rootNode.path("header_part");
if (!headerPart.has("eaiIntfId") || headerPart.get("eaiIntfId").asText().isEmpty()) {
// /api/v1/public/getUserInfo.svc
String inboundUri = prop.getProperty(HttpAdapterServiceKey.INBOUND_EXTURI);
String apiPath = prop.getProperty(HttpAdapterServiceKey.API_PATH) + "/";
// getUserInfo.svc
String eaiIntfId = org.apache.commons.lang.StringUtils.removeStart(inboundUri, apiPath);
// header_part 아래 eaiIntfId가 추가 또는 수정
if (headerPart instanceof ObjectNode) {
((ObjectNode) headerPart).put("eaiIntfId", eaiIntfId);
// 요청 응답 구분코드 강제 설정 -> API명 찾아갈때 필요
((ObjectNode) headerPart).put("reqRspnsDscd", "S");
}
}
}
else {
return resultMessage;
}
return rootNode.toString();
}
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException {
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
String orgMessageString;
if(message instanceof byte[]) {
orgMessageString = new String((byte[])message, charset);
} else {
orgMessageString = (String) message;
}
return mapper.readTree(orgMessageString);
}
}
@@ -0,0 +1,68 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
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.AdapterManager;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class KbankEaiJsonParseFilter implements HttpAdapterFilter {
private ObjectMapper mapper = new ObjectMapper();
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
JsonNode rootNode = parseJson(adptGrpName, message);
ObjectNode replacedJson = mapper.createObjectNode();
for(Iterator<String> it = rootNode.fieldNames(); it.hasNext();) {
String fieldName = it.next();
String value = rootNode.get(fieldName).asText();
JsonNode jsonNode = mapper.readTree(value);
replacedJson.set(fieldName, jsonNode);
}
return replacedJson.toString();
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
ObjectNode replacedJson = mapper.createObjectNode();
for(Iterator<String> it = rootNode.fieldNames(); it.hasNext();) {
String fieldName = it.next();
JsonNode childNode = rootNode.get(fieldName);
String childString = mapper.writeValueAsString(childNode);
replacedJson.put(fieldName, childString);
}
return replacedJson.toString();
}
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException {
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
String orgMessageString;
if(message instanceof byte[]) {
orgMessageString = new String((byte[])message, charset);
} else {
orgMessageString = (String) message;
}
return mapper.readTree(orgMessageString);
}
}
@@ -0,0 +1,215 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.text.SimpleDateFormat;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.Mac;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.common.property.PropManager;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.net.util.Base64;
import org.springframework.http.HttpStatus;
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.server.Keys;
import com.eactive.eai.common.util.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
public class KbankHmacSha256VerifyFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private static final String SERVICE_CODE_UNKNOWN = "00";
private static final int TIMESTAMP_EXPIRATION_SECONDS = 60*10*1000;
private static final String GROUP_NAME = "HMAC_INFO";
private ObjectMapper mapper = new ObjectMapper();
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
String hmacSignature = getHeaderCaseInsensitive(request, "hmac_signature");
String hmacTimestamp = getHeaderCaseInsensitive(request, "hmac_timestamp");
if(StringUtils.isBlank(hmacSignature) || StringUtils.isBlank(hmacTimestamp)) {
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Can not find hmac info");
}
long inputTime = new SimpleDateFormat("yyyyMMddHHmmss").parse(hmacTimestamp).getTime();
long currentTime = new Date().getTime();
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
logger.debug("### inputTime :"+inputTime);
logger.debug("### currentTime :"+currentTime);
logger.debug("### timeStamp :"+timeStamp);
String systemMode = System.getProperty(Keys.EAI_SYSTEMMODE);
if(currentTime - inputTime > TIMESTAMP_EXPIRATION_SECONDS) {
logger.debug("### systemMode:" + systemMode);
logger.debug("### currentTime - inputTime:" + (currentTime - inputTime));
if("P".equals(systemMode)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN,"00"), "Signature verifying failed");
}
}
String sMessage = (String)message;
sMessage = sMessage.replace(" ","");
sMessage = sMessage.replace("\n","");
sMessage = sMessage.replace("\r","");
sMessage = sMessage + hmacTimestamp;
logger.info("### sMessage:"+sMessage);
String alCoId = "AL2021012901001";
JsonNode rootNode = parseJson(adptGrpName, message);
for(Iterator<String> it = rootNode.fieldNames(); it.hasNext();){
String fieldName = it.next();
if(fieldName.equals("ALCO_ID")) alCoId = rootNode.get(fieldName).asText();
}
logger.info("### alCoId:"+alCoId);
Properties hmacProp = PropManager.getInstance().getProperties(GROUP_NAME);
String hmacKey = hmacProp.getProperty(alCoId+"-hmack-key","");
String hmacKek = hmacProp.getProperty(alCoId+"-hmack-kek","");
String hmacIvForKey = hmacProp.getProperty(alCoId+"-hmack-iv-for-key","");
logger.debug("### hmacKey:"+hmacKey);
logger.debug("### hmacIvForKey:"+hmacIvForKey);
logger.debug("### hmacKek:"+hmacKek);
//kbank.payment.hmac-key
int hmacKeyLen = hmacKey.length();
byte[] hmacKeyData = new byte[hmacKeyLen/2];
for(int i=0;i<hmacKeyLen;i+=2){
hmacKeyData[i/2] = (byte)((Character.digit(hmacKey.charAt(i), 16) << 4) + Character.digit(hmacKey.charAt(i+1), 16));
}
//kbank.payment.hmac-kek
int hmacKekLen = hmacKek.length();
byte[] hmacKekData = new byte[hmacKekLen/2];
for(int i=0;i<hmacKekLen;i+=2){
hmacKekData[i/2] = (byte)((Character.digit(hmacKek.charAt(i),16) << 4) + Character.digit(hmacKek.charAt(i+1), 16));
}
//kbank.payment.hmac-iv-for-key
int hmacIvForKeyLen = hmacIvForKey.length();
byte[] hmacIvForKeyData = new byte[hmacIvForKeyLen/2];
for(int i=0;i<hmacIvForKeyLen;i+=2){
hmacIvForKeyData[i/2] = (byte)((Character.digit(hmacIvForKey.charAt(i),16) << 4) + Character.digit(hmacIvForKey.charAt(i+1), 16));
}
byte[] decrypedHmacKey = null;
try{
Cipher cp = Cipher.getInstance("AES/CBC/PKCS5Padding");
cp.init(Cipher.DECRYPT_MODE, new SecretKeySpec(hmacIvForKeyData,"AES"), new IvParameterSpec(hmacIvForKeyData));
decrypedHmacKey = cp.doFinal(hmacKeyData);
} catch (Exception e) {
logger.debug(e.getMessage());
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed");
}
SecretKeySpec secretKey = new SecretKeySpec(decrypedHmacKey, "HmacSHA256");
String madeSignature = "";
logger.info("### secretKey:"+secretKey);
try{
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKey);
madeSignature= Base64.encodeBase64String(mac.doFinal(sMessage.getBytes()));
} catch (Exception e) {
logger.debug(e.getMessage());
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed");
}
logger.info("### madeSignature:"+madeSignature);
if(!StringUtils.equals(hmacSignature, madeSignature)){
logger.info("### hmacSignature:"+hmacSignature);
logger.info("### madeSignature:"+madeSignature);
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed" );
}
} catch (JwtAuthException e) {
logger.debug(e.getMessage());
throw e;
} catch (Exception e) {
logger.debug(e.getMessage());
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed(Unknown)" );
}
logger.info("### pass: "+"success");
return message;
}
/**
* 대소문자 구분 없이 HTTP 헤더 값을 가져오는 메소드
* @param request HTTP 요청 객체
* @param headerName 찾고자 하는 헤더 이름
* @return 헤더 값 또는 null
*/
public static String getHeaderCaseInsensitive(HttpServletRequest request, String headerName) {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
if (header != null && header.equalsIgnoreCase(headerName)) {
return request.getHeader(header);
}
}
return null;
}
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException,JsonProcessingException {
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
String orgMessageString;
if(message instanceof byte[]) {
orgMessageString = new String((byte[])message, charset);
} else {
orgMessageString = (String) message;
}
return mapper.readTree(orgMessageString);
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
return resultMessage;
}
}
@@ -0,0 +1,205 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.vo.ClientVO;
import com.eactive.eai.common.util.Logger;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SnapHmacSha512VerifyFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private static final String SERVICE_CODE_UNKNOWN = "00";
private static final int X_TIMESTAMP_EXPIRATION_SECONDS = 60 * 5;
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// @formatter:off
/*
* HMAC_SHA512 (clientSecret, stringToSign)
*
* stringToSign = HTTPMethod:EndpointUrl:AccessToken
* :Lowercase(HexEncode(SHA-256(minify(RequestBody)))):TimeStamp
*/
try {
String xSignature = request.getHeader("X-SIGNATURE");
if (StringUtils.isBlank(xSignature)) {
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Can not find X-SIGNATURE");
}
String clientSecret = assignClientSecret(prop, request);
StringBuilder sb = new StringBuilder();
sb.append(request.getMethod())
.append(":")
.append(getEndpointUrl(request))
.append(":")
.append(SnapSimpleOauth2Filter.extractToken(request))
.append(":")
.append(getRequestBody((String)message))
.append(":")
.append(getTimeStamp(request));
// @formatter:on
if (logger.isDebug()) {
logger.debug(sb.toString());
}
String hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_512, clientSecret).hmacHex(sb.toString());
if (!StringUtils.equals(xSignature, hmac)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Signature verifying failed");
}
} catch (JwtAuthException e) {
logger.debug(e.getMessage());
throw e;
} catch (Exception e) {
logger.error(e.getMessage());
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Signature verifying failed(unkown)");
}
return message;
}
private String assignClientSecret(Properties prop, HttpServletRequest request) throws Exception {
String clientId = assignClientId(prop, request);
if (StringUtils.isBlank(clientId)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Can not find clientId info");
}
ClientVO client = (ClientVO) OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
if (client == null) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Can not find client info");
}
return client.getClientSecret();
}
private String assignClientId(Properties prop, HttpServletRequest request) {
String clientId = null;
try {
// 이전 filter에서 셋팅한 clientId 확보
clientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
// 이전 filter에서 셋팅한 값이 없다면 header 정보에서 확보
if (StringUtils.isBlank(clientId)) {
clientId = request.getHeader(HttpAdapterServiceSupport.HEADER_NAME_CLIENT_ID);
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return clientId;
}
private String getEndpointUrl(HttpServletRequest request) {
String servletPath = request.getServletPath();
String pathInfo = request.getPathInfo();
String queryString = request.getQueryString();
StringBuilder url = new StringBuilder();
url.append(servletPath);
if (pathInfo != null) {
url.append(pathInfo);
}
if (queryString != null) {
url.append("?").append(queryString);
}
return url.toString();
}
private String getRequestBody(String message) throws Exception {
if (StringUtils.isEmpty(message)) {
return "";
}
// Lowercase(HexEncode(SHA-256(minify(RequestBody))))
String body = minifyJson(message);
try {
body = DigestUtils.sha256Hex(body);
} catch (Exception e) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"SHA-256 digest error");
}
return body.toLowerCase();
}
private String minifyJson(String json) {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readValue(json, JsonNode.class);
return jsonNode.toString();
} catch (Exception e) {
// json이 아닐경우
return json;
}
}
private String getTimeStamp(HttpServletRequest request) throws Exception {
String timeStamp = request.getHeader("X-TIMESTAMP");
if (StringUtils.isBlank(timeStamp)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Can not find X-TIMESTAMP");
}
// 2020-12-23T09:10:11+07:00(현재시간이랑 비교로직 추가-보안)
ZonedDateTime xTimeStamp = ZonedDateTime.parse(timeStamp, DateTimeFormatter.ISO_DATE_TIME);
xTimeStamp = xTimeStamp.plusSeconds(X_TIMESTAMP_EXPIRATION_SECONDS);
ZonedDateTime now = ZonedDateTime.now();
if (xTimeStamp.isBefore(now.withZoneSameInstant(xTimeStamp.getZone()))) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Too old X-TIMESTAMP");
}
return timeStamp;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
return resultMessage;
}
public static void main(String[] args) {
String str = "POST:/api/test/aaa/type02/555:"
+ "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzbmFwIl0sImV4cCI6MTY3NjUyNDI1MSwianRpIjoiNzI0MWMyZGUtZjJhNi00NTFmLWExMjQtZGM3NmI0ZGY1OThjIiwiY2xpZW50X2lkIjoic1NHSkR3bEpzZWw1V2VYb2R6ZDNKM01RMjBLWUNacEwifQ.Obe8t8IwuQ3P7i4yCSASYze-9DLmVwXe0g_gBYlwv_Jzc7V8-ooTwp6SVnaNsM6pp1GV-9lxMpAzQO_CRHQq_hdMeiPI_CXHh3gETvjQThn57QGEGdCNp_lUEVYtMPUxi5u3X6Of07o0_OB83WI7rA1zD0DaP8V67pgfq-jMRe_3IXztOYu_nwMgREbGvs9tqcsjxppRKkEHcK1S8Eq4HzaLwjwyHJAhIlTba27yd4T8ELC7IpFphJBfEPF_t0ZpYW15lOrg5pHPjy1xpTV0Kgipog5wycTZlFUeCWWjfxTrpVmt_1XlEOlZH9X6xAefWrH93_fpbt3OcxwP4u9KhA"
+ ":424058b889b9d860d13b79d90df52ddcbefba2fc9d27607e999c7c3c4d61d9c8:" + "2023-02-16T09:47:07+07:00";
String hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_512,
"o3Hre3voTrHbLueDrHC0LYM5effHQZILI8t23htbD12TKD5QJUMONqFqv4494iQS46bzHS0xW1fBC5LHo3D0SzhZvQcPUaJZw0iQ79NnzYFUCTrEsoFJRL300dp3Ql4y")
.hmacHex(str);
// System.out.println(hmac);
}
}
@@ -0,0 +1,237 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthFilter;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.common.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
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.StandardMessageUtil;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.util.IOUtils;
import com.nimbusds.jwt.SignedJWT;
public class SnapJwtAuthFilter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";
private static final String SERVICE_CODE_UNKNOWN = "00";
public static final String PAYLOAD_PARAM_NAME_SCOPE = "scope";
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";
private JWSVerifier jwsVerifier;
public SnapJwtAuthFilter() {
super();
PropGroupVO vo = PropManager.getInstance().getPropGroupVO(PROP_GROUP_AUTH_SERVER);
String publicKeyPath = "/certificate/elink-oauth-dev.pub";
if (vo != null) {
publicKeyPath = vo.getProperty(PROP_KEYSTORE_PATH);
} else {
logger.warn("The properties has not been set.[" + PROP_GROUP_AUTH_SERVER + "]");
}
Resource resource = new ClassPathResource(publicKeyPath);
String publicKey = null;
try {
publicKey = IOUtils.readInputStreamToString(resource.getInputStream());
publicKey = StringUtils.replace(publicKey, "-----BEGIN PUBLIC KEY-----", "");
publicKey = StringUtils.replace(publicKey, "-----END PUBLIC KEY-----", "");
publicKey = StringUtils.remove(publicKey, "\r");
publicKey = StringUtils.remove(publicKey, "\n");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyFactory.generatePublic(keySpec));
} catch (final IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String apiId = assignApiId(adptGrpName, adptName, message, prop, request);
if (StringUtils.isBlank(apiId)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Can not find apiId(apiCode) info");
}
try {
String token = JwtAuthFilter.extractJWTToken(request);
SignedJWT signedJWT = SignedJWT.parse(token);
if (new Date().after(signedJWT.getJWTClaimsSet().getExpirationTime())) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
"Invalid Token - expired (B2B)");
}
if (!signedJWT.verify(jwsVerifier)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
"Invalid Token (B2B)");
}
OAuth2Manager manager = OAuth2Manager.getInstance();
HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
if (!verifyScope(scopeSet, signedJWT)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
}
// header 값으로 전송된 clientId와 토큰 소유 clientId check
if (!verifyClientId(prop, signedJWT)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"client_id not matched(header/token)");
}
} catch (JwtAuthException e) {
logger.debug(e.getMessage());
throw e;
} catch (Exception e) {
logger.error(e.getMessage());
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
"Invalid Token (B2B)");
}
return message;
}
private boolean verifyClientId(Properties prop, SignedJWT signedJWT) {
try {
// 이전 filter에서 셋팅한 clientId 확보
String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
String tokenClientId = (String) signedJWT.getJWTClaimsSet().getClaim(PAYLOAD_PARAM_NAME_CLIENT_ID);
// 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보
if (StringUtils.isBlank(headerClientId)) {
if (StringUtils.isNotBlank(tokenClientId)) {
prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId);
}
} else { // header로 전달된 clientId가 있을때만 비교
if (!headerClientId.equals(tokenClientId)) {
return false;
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return true;
}
private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request) {
String apiId = null;
try {
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
RequestAction action = ActionFactory.createAction(actionName);
action.setAdapterInfo(adptGrpName, adptName, prop);
String[] keys = action.perform(message);
apiId = keys[0];
// PathVariable 지원 추가
apiId = StandardMessageUtil.getMatchedKey(apiId, actionName);
} catch (Exception e) {
logger.error(e.getMessage());
}
// // header 에서 확보
// String apiId = request.getHeader(HEADER_NAME_API_CODE);
// if (StringUtils.isBlank(apiId)) {
// // url에서 확보
// // /ONLWeb/api/v1/public/getUserInfo.svc/
// apiId = request.getRequestURI();
// // /ONLWeb/api/v1/public/getUserInfo.svc
// apiId = StringUtils.removeEnd(apiId, "/");
// // getUserInfo.svc
// apiId = StringUtils.substringAfterLast(apiId, "/");
// }
return apiId;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
return resultMessage;
}
private boolean verifyScope(HashSet<String> scopeSet, SignedJWT signedJWT) throws ParseException {
if (scopeSet == null || scopeSet.isEmpty()) {
return true; // If no scope is specified in the api, all are allowed
}
String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
if (scopeArr == null || scopeArr.length == 0) {
return false;
}
for (String scope : scopeArr) {
if (scopeSet.contains(scope)) {
return true;
}
}
return false;
}
public static String extractJWTToken(HttpServletRequest request) throws JwtAuthException {
String authorization = request.getHeader("Authorization");
if (StringUtils.isBlank(authorization)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"No have header info: Authorization");
}
String[] components = authorization.split("\\s");
if (components.length != 2) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Malformat [Authorization] content");
}
if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"[Bearer] is needed");
}
return components[1].trim();
}
}
@@ -0,0 +1,205 @@
package com.eactive.eai.adapter.http.dynamic.filter.custom;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.authserver.service.OAuth2Manager;
import com.eactive.eai.authserver.util.BeanUtils;
import com.eactive.eai.common.util.Logger;
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.StandardMessageUtil;
public class SnapSimpleOauth2Filter implements HttpAdapterFilter {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private static final String SERVICE_CODE_UNKNOWN = "00";
private TokenStore tokenStore;
public SnapSimpleOauth2Filter() {
super();
tokenStore = BeanUtils.getBean("tokenStore", TokenStore.class);
}
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
String apiId = assignApiId(adptGrpName, adptName, message, prop, request);
if (StringUtils.isBlank(apiId)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Can not find apiId(apiCode) info");
}
String tokenStr = extractToken(request);
OAuth2AccessToken token = tokenStore.readAccessToken(tokenStr);
if (token == null) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "03"),
"Token Not Found (B2B)");
}
if (token.isExpired()) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
"Invalid Token - expired (B2B)");
}
OAuth2Authentication authentication = tokenStore.readAuthentication(token);
if (!authentication.isAuthenticated()) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
"Invalid Token (B2B)");
}
// check scope
OAuth2Manager manager = OAuth2Manager.getInstance();
HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
if (!verifyScope(scopeSet, authentication)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
}
// header 값으로 전송된 clientId와 토큰 소유 clientId check
if (!verifyClientId(prop, authentication)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"client_id not matched(header/token)");
}
} catch (JwtAuthException e) {
logger.debug(e.getMessage());
throw e;
} catch (Exception e) {
logger.error(e.getMessage());
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
"Invalid Token (B2B)");
}
return message;
}
private boolean verifyScope(HashSet<String> scopeSet, OAuth2Authentication auth) {
if (scopeSet == null || scopeSet.isEmpty()) {
return true; // If no scope is specified in the api, all are allowed
}
Set<String> scopesInToken = auth.getOAuth2Request().getScope();
if (scopesInToken == null || scopesInToken.isEmpty()) {
return false;
}
for (String scope : scopesInToken) {
if (scopeSet.contains(scope)) {
return true;
}
}
return false;
}
private boolean verifyClientId(Properties prop, Authentication authentication) {
try {
// 이전 filter에서 셋팅한 clientId 확보
String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
String tokenClientId = ((OAuth2Authentication) authentication).getOAuth2Request().getClientId();
// 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보
if (StringUtils.isBlank(headerClientId)) {
if (StringUtils.isNotBlank(tokenClientId)) {
prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId);
}
} else { // header로 전달된 clientId가 있을때만 비교
if (!headerClientId.equals(tokenClientId)) {
return false;
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return true;
}
private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request) {
String apiId = null;
try {
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
RequestAction action = ActionFactory.createAction(actionName);
action.setAdapterInfo(adptGrpName, adptName, prop);
String[] keys = action.perform(message);
apiId = keys[0];
// PathVariable 지원 추가
apiId = StandardMessageUtil.getMatchedKey(apiId, actionName);
} catch (Exception e) {
logger.error(e.getMessage());
}
// // header 에서 확보
// String apiId = request.getHeader(HEADER_NAME_API_CODE);
// if (StringUtils.isBlank(apiId)) {
// // url에서 확보
// // /ONLWeb/api/v1/public/getUserInfo.svc/
// apiId = request.getRequestURI();
// // /ONLWeb/api/v1/public/getUserInfo.svc
// apiId = StringUtils.removeEnd(apiId, "/");
// // getUserInfo.svc
// apiId = StringUtils.substringAfterLast(apiId, "/");
// }
return apiId;
}
public static String extractToken(HttpServletRequest request) throws JwtAuthException {
String authorization = request.getHeader("Authorization");
if (StringUtils.isBlank(authorization)) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"No have header info: Authorization");
}
String[] components = authorization.split("\\s");
if (components.length != 2) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"Malformat [Authorization] content");
}
if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) {
throw new JwtAuthException(
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
"[Bearer] is needed");
}
return components[1].trim();
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
return resultMessage;
}
}
@@ -1,5 +1,27 @@
package com.eactive.eai.adapter.http.dynamic.impl; package com.eactive.eai.adapter.http.dynamic.impl;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.mina.common.ByteBuffer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
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.AdapterPropManager;
@@ -8,8 +30,8 @@ import com.eactive.eai.adapter.Keys;
import com.eactive.eai.adapter.http.HttpMemoryLogger; import com.eactive.eai.adapter.http.HttpMemoryLogger;
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.HttpAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport; import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.adapter.http.dynamic.UnknownMessageLogUtils;
import com.eactive.eai.common.TransactionContextKeys; import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.util.CommonLib; import com.eactive.eai.common.util.CommonLib;
@@ -19,27 +41,6 @@ import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.env.ElinkConfig; import com.eactive.eai.env.ElinkConfig;
import com.eactive.eai.inbound.processor.Processor; import com.eactive.eai.inbound.processor.Processor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.mina.common.ByteBuffer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
{ {
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod"; public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
@@ -63,7 +64,8 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
AdapterGroupVO adapterGroupVo = null; AdapterGroupVO adapterGroupVo = null;
Properties prop = null; Properties prop = null;
byte[] requestBytes = null;
String encode = "MS949";
try { try {
AdapterManager adapterManager = AdapterManager.getInstance(); AdapterManager adapterManager = AdapterManager.getInstance();
AdapterVO adptVO = adapterManager.getAdapterVO(adptGrpName,adptName); AdapterVO adptVO = adapterManager.getAdapterVO(adptGrpName,adptName);
@@ -75,9 +77,9 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
} }
Properties httpProp = manager.getProperties(adptVO.getPropGroupName()); Properties httpProp = manager.getProperties(adptVO.getPropGroupName());
encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(), "MS949");
String responseType = httpProp.getProperty(RESPONSE_TYPE,"SYNC"); String responseType = httpProp.getProperty(RESPONSE_TYPE,"SYNC");
String urlDecodeYn = httpProp.getProperty(URL_DECODE_YN,"N"); String urlDecodeYn = httpProp.getProperty(URL_DECODE_YN,"N");
String encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(), "MS949");
String messageType = adapterManager.getAdapterGroupVO(adptGrpName).getMessageType(); String messageType = adapterManager.getAdapterGroupVO(adptGrpName).getMessageType();
String requestBodyYn = httpProp.getProperty(REQUEST_BODY_YN,"N"); String requestBodyYn = httpProp.getProperty(REQUEST_BODY_YN,"N");
String parameterName = httpProp.getProperty(PARAMETER_NAME,PARAMETER_MESSAGE); String parameterName = httpProp.getProperty(PARAMETER_NAME,PARAMETER_MESSAGE);
@@ -104,7 +106,6 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
logger.info("시작 >> encode = [" + encode + "]"); logger.info("시작 >> encode = [" + encode + "]");
byte[] requestBytes = null;
if ("Y".equals(requestBodyYn)){ if ("Y".equals(requestBodyYn)){
ServletInputStream sis = request.getInputStream(); ServletInputStream sis = request.getInputStream();
@@ -188,19 +189,19 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod()); prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod());
prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, "")); prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, ""));
String body = new String ( requestBytes,encode); // String body = new String ( requestBytes,encode);
//
try { // try {
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);
} // }
//
}catch (Exception e) { // }catch (Exception e) {
// ignore json이 아니기에 해줄것이 없음. // // ignore json이 아니기에 해줄것이 없음.
} // }
// 로컬 서비스 호출 // 로컬 서비스 호출
Object result = null; Object result = null;
@@ -212,6 +213,11 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
stopWatch.stop(); stopWatch.stop();
String apiInternalErrorCode = prop.getProperty("API_INTERNAL_ERROR_CODE", "");
if(StringUtils.isNotEmpty(apiInternalErrorCode))
response.setStatus(500);
byte[] responseBytes = null;
String responseData = ""; String responseData = "";
if (RESPONSE_TYPE_ASYNC.equals(responseType)){ if (RESPONSE_TYPE_ASYNC.equals(responseType)){
if (stopWatch.getTime() > slowTranTime){ if (stopWatch.getTime() > slowTranTime){
@@ -222,17 +228,22 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
if ( result == null) { if ( result == null) {
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName); responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
responseBytes = responseData.getBytes();
} else if (result instanceof byte[]) { } else if (result instanceof byte[]) {
responseData = new String((byte[]) result, encode); responseData = new String((byte[]) result, encode);
responseBytes = (byte[]) result;
} else if (result instanceof String) { } else if (result instanceof String) {
responseData = (String) result; responseBytes = ((String)result).getBytes();
if ( StringUtils.isBlank( responseData ) ) { if ( StringUtils.isBlank((String)result ) ) {
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName, adptName); responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName, adptName);
responseBytes = responseData.getBytes();
} }
} }
response.setCharacterEncoding(encode); response.setCharacterEncoding(encode);
response.getWriter().print(responseData); // response.getWriter().print(responseData);
response.getOutputStream().write(responseBytes);
if (traceLevel >= 3){ if (traceLevel >= 3){
HttpMemoryLogger.txlog(adptGrpName+adptName, "SEND "+"["+responseData+"]"+ HttpMemoryLogger.txlog(adptGrpName+adptName, "SEND "+"["+responseData+"]"+
((responseData == null)?"":CommonLib.getDumpMessage(responseData)) ((responseData == null)?"":CommonLib.getDumpMessage(responseData))
@@ -240,45 +251,58 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
} }
} else { } else {
if(requestBytes != null) { if(requestBytes != null) {
if (result instanceof byte[]) { if ( result == null) {
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
responseBytes = responseData.getBytes();
} else if (result instanceof byte[]) {
responseData = new String((byte[]) result, encode); responseData = new String((byte[]) result, encode);
responseBytes = (byte[]) result;
} else if (result instanceof String) { } else if (result instanceof String) {
responseData = (String) result; responseBytes = ((String)result).getBytes(encode);
if ( StringUtils.isBlank((String)result ) ) {
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName, adptName);
responseBytes = responseData.getBytes(encode);
}
} }
logger.info("종료 >> encode = [" + encode + "]"); logger.info("종료 >> encode = [" + encode + "]");
if (addData.length() > 0){ if (addData.length() > 0){
responseData = addData + responseData; responseData = addData + responseData;
responseBytes = responseData.getBytes();
} }
try { // try {
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) {
// ignore json이 아니기에 해줄것이 없음. // // ignore json이 아니기에 해줄것이 없음.
} // }
// 300응답에서 받은 Header정보 사용하기 위해 읽어옴. // 300응답에서 받은 Header정보 사용하기 위해 읽어옴.
Map<String, Object> responsePropertyMap = (Map<String, Object>) prop.get(OUTBOUND_PROPERTY_MAP); // Map<String, Object> responsePropertyMap = (Map<String, Object>) prop.get(OUTBOUND_PROPERTY_MAP);
if(responsePropertyMap != null){ // if(responsePropertyMap != null){
Properties responseHeaders = (Properties)responsePropertyMap.get(OUTBOUND_RESPONSE_HEADERS); // Properties responseHeaders = (Properties)responsePropertyMap.get(OUTBOUND_RESPONSE_HEADERS);
Map<String, String> responseHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); // Map<String, String> responseHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
// Properties 내용을 모두 복사 // // Properties 내용을 모두 복사
for (String name : responseHeaders.stringPropertyNames()) { // for (String name : responseHeaders.stringPropertyNames()) {
responseHeaderMap.put(name, responseHeaders.getProperty(name)); // responseHeaderMap.put(name, responseHeaders.getProperty(name));
} // }
String contentTytpe = responseHeaderMap.get("Content-Type"); // String contentTytpe = responseHeaderMap.get("Content-Type");
response.setContentType(contentTytpe); // 300응답에서 받은 Content-Type을 그대로 적용. // response.setContentType(contentTytpe); // 300응답에서 받은 Content-Type을 그대로 적용.
} // }
// UI와 통신시(UTF-8) 변환오류로 ENCODE 제거 // UI와 통신시(UTF-8) 변환오류로 ENCODE 제거
response.setCharacterEncoding(encode); response.setCharacterEncoding(encode);
response.getWriter().print(responseData); // response.getWriter().print(responseData);
String resContentType = request.getContentType();
response.setContentType(resContentType);
response.getOutputStream().write(responseBytes);
if (logger.isDebug()){ if (logger.isDebug()){
logger.debug("HttpAdapter] SEND ("+adptGrpName+") = [" + responseData + "]"); logger.debug("HttpAdapter] SEND ("+adptGrpName+") = [" + responseData + "]");
logger.debug("HttpAdapter] SEND ("+adptGrpName+") = " + logger.debug("HttpAdapter] SEND ("+adptGrpName+") = " +
@@ -316,7 +340,14 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
} }
} catch (Exception e) { } catch (Exception e) {
if (traceLevel >= 3){ try {
UnknownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes, encode) : null, prop, request, response,
"RECEAIIRP202", e);
} catch (UnsupportedEncodingException e1) {
UnknownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes) : null, prop, request, response,
"RECEAIIRP202", e);
}
if (traceLevel >= 3){
HttpMemoryLogger.error(adptGrpName+adptName, e.toString(),e); HttpMemoryLogger.error(adptGrpName+adptName, e.toString(),e);
} }
logger.error("HttpAdapter] "+adptGrpName+"-"+adptName, e); logger.error("HttpAdapter] "+adptGrpName+"-"+adptName, e);
@@ -0,0 +1,766 @@
package com.eactive.eai.adapter.http.dynamic.impl;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.mina.common.ByteBuffer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.AntPathMatcher;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.adapter.Keys;
import com.eactive.eai.adapter.http.HttpMemoryLogger;
import com.eactive.eai.adapter.http.HttpMethodType;
import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.util.CommonLib;
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.env.ElinkConfig;
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.StandardMessageUtil;
/*
* Kbank 가상계좌 INBOUND
* @see com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
* @Deprecated
*/
// FIXME : kbank - kbank에서는 Controller 방식을 사용하므로, 이 어댑터는 사용되지 않음 (VirtualAccountCryptoFilter 로 대체)
public class HttpAdapterServiceVirtualAccount extends HttpAdapterServiceSupport {
public static final String HEADER_GROUP = "HEADER_GROUP";
// HEADER_GROUP JSON에 추가할 항목 정의, 없으면 전체 header 추가
public static final String HEADER_KEYS = "HEADER_KEYS";
public static final String HTTP_STATUS = "HTTP_STATUS";
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private String logPrefix = "HttpAdapterServiceRest] ";
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String JSON_FIELD_NAME = "json-body";
private static final String FILE_GROUP_NAME = "image-file";
private static final String UPLOAD_ROOT_PATH = "UPLOAD_ROOT_PATH";
private Properties addCryptoFilter(Properties prop) {
String cryptoFilterName = "com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter";
String addedPreFilter = prop.getProperty(PRE_FILTERS);
String addedPostFilter = prop.getProperty(POST_FILTERS);
if(StringUtils.isBlank(addedPreFilter)) {
addedPreFilter = cryptoFilterName;
}
else {
addedPreFilter = addedPreFilter + "," +cryptoFilterName;
}
if(StringUtils.isBlank(addedPostFilter)) {
addedPostFilter = cryptoFilterName;
}
else {
addedPostFilter = cryptoFilterName + "," +addedPostFilter;
}
prop.setProperty(PRE_FILTERS, addedPreFilter);
prop.setProperty(POST_FILTERS, addedPostFilter);
return prop;
}
private String readMultipartBody(HttpServletRequest request) throws Exception {
String jsonString = null;
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// Set the maximum size of the files to be uploaded
factory.setSizeThreshold(1024 * 1024);
// Set the temporary directory to store uploaded files
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
factory.setRepository(tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
Map<String, String> fileMap = new HashMap<>();
InputStream fin = null;
try {
byte[] buffer = new byte[1024];
int read = 0;
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
// file
String fieldName = item.getFieldName();
String fileName = item.getName();
fin = item.getInputStream();
ByteArrayOutputStream fo = new ByteArrayOutputStream();
while ((read = fin.read(buffer)) > 0) {
fo.write(buffer, 0, read);
}
int fileSize = fo.size();
byte[] fileBytes = fo.toByteArray();
String fileContents = new String(fileBytes);
if (logger.isInfo()) {
logger.info("[FILE]-------------------------------------------------->");
logger.info("Field name = " + fieldName);
logger.info("File name = " + fileName + " contents length = " + fileSize);
logger.info("File Contents [" + fileContents + "]");
logger.info("[FILE]<--------------------------------------------------");
}
fileMap.put(fileName, fileContents);
fin.close();
} else {
// regular form field
String fieldName = item.getFieldName();
String fieldValue = item.getString();
if (logger.isInfo()) {
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
}
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|| JSON_FIELD_NAME.equals(fieldName)) {
jsonString = fieldValue;
}
}
}
if (logger.isInfo()) {
logger.info("Json body [" + jsonString + "]");
}
if (jsonString == null) {
jsonString = "{}";
} else {
// parsing json & add file contents
JSONObject jsonObject = (JSONObject) JSONValue.parse(jsonString);
if (jsonObject == null) {
jsonString = "{}";
} else {
JSONObject fileGroup = new JSONObject();
for (Map.Entry<String, String> entry : fileMap.entrySet()) {
fileGroup.put("fileName", entry.getKey());
fileGroup.put("fileContents", entry.getValue());
}
jsonObject.put(FILE_GROUP_NAME, fileGroup);
jsonString = jsonObject.toJSONString();
}
}
if (logger.isInfo()) {
logger.info("Json with file [" + jsonString + "]");
}
return jsonString;
} catch (Exception e) {
logger.error("Read multipart body error.", e);
throw e;
} finally {
if (fin != null) {
try {
fin.close();
} catch (Exception ex) {
// empty
}
}
}
}
private String checkRootPath(String path) {
if (StringUtils.isEmpty(path)) {
logger.info("upload dir not set(UPLOAD_ROOT_PATH), use system temp " + path);
return System.getProperty("java.io.tmpdir");
}
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
return path;
}
private String uploadMultipartBody(HttpServletRequest request, String uploadRootPath) throws Exception {
String jsonString = null;
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// Set the maximum size of the files to be uploaded
factory.setSizeThreshold(1024 * 1024);
// Set the temporary directory to store uploaded files
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
factory.setRepository(tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// if not exist, create folders
String uploadDir = checkRootPath(uploadRootPath);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
// file
String fieldName = item.getFieldName();
String fileName = item.getName();
String uploadFilePath = uploadDir + File.separator + fileName;
File uploadFile = new File(uploadFilePath);
item.write(uploadFile);
if (logger.isInfo()) {
logger.info("[FILE]-------------------------------------------------->");
logger.info("Field name = " + fieldName);
logger.info("File name = " + fileName + " path = " + uploadFile.getAbsolutePath());
logger.info("[FILE]<--------------------------------------------------");
}
} else {
// regular form field
String fieldName = item.getFieldName();
String fieldValue = item.getString();
if (logger.isInfo()) {
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
}
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|| JSON_FIELD_NAME.equals(fieldName)) {
jsonString = fieldValue;
}
}
}
if (logger.isInfo()) {
logger.info("Json body [" + jsonString + "]");
}
return jsonString;
} catch (Exception e) {
logger.error("Read multipart body error.", e);
throw e;
}
}
@SuppressWarnings({ "unchecked", "deprecation" })
public void service(String adptGrpName, String adptName, HttpServletRequest request, HttpServletResponse response) {
int traceLevel = 0;
AdapterVO adptVO = null;
AdapterPropManager manager = null;
Properties httpProp = null;
String responseType = null;
String urlDecodeYn = null;
String encode = null;
String traceLevelTemp = null;
String relayRequestHeaderKeys = null;
String headerGroupName = null;
boolean isParameterType = false;
String message = null;
StopWatch stopWatch = null;
Properties prop = null;
String paramValue = null;
String adptMsgType = null;
String errorResponseFormat = null;
String uploadRootPath = null;
try {
AdapterManager adapterManager = AdapterManager.getInstance();
adptVO = adapterManager.getAdapterVO(adptGrpName, adptName);
if (adptVO == null) {
throw new Exception("Adapter not found error");
}
manager = AdapterPropManager.getInstance();
httpProp = manager.getProperties(adptVO.getPropGroupName());
responseType = httpProp.getProperty(RESPONSE_TYPE, "SYNC");
urlDecodeYn = httpProp.getProperty(URL_DECODE_YN, "N");
// encode = httpProp.getProperty(ENCODE, "UTF-8");
encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(),
"UTF-8");
traceLevelTemp = httpProp.getProperty(TRACE_LEVEL, "0");
relayRequestHeaderKeys = httpProp.getProperty(HEADER_KEYS);
headerGroupName = httpProp.getProperty(HEADER_GROUP);
errorResponseFormat = httpProp.getProperty(ERROR_RESPONSE_FORMAT);
uploadRootPath = httpProp.getProperty(UPLOAD_ROOT_PATH);
prop = new Properties();
prop.put(INBOUND_METHOD, request.getMethod());
prop.put(INBOUND_URI, request.getRequestURI());
prop.put(INBOUND_HEADER, getHeaders(request));
prop.put(INBOUND_EXTPARAMS, StringUtils.defaultString(request.getQueryString()));
if (StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_REST)
|| StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_HTTP_CUSTOM)) {
// /api/v1/public/getUserInfo.svc
String extUrl = StringUtils.removeStart(request.getRequestURI(), request.getContextPath());
prop.put(INBOUND_EXTURI, extUrl);
} else {
prop.put(INBOUND_EXTURI, getExtUri(request));
}
prop.put(Processor.REQUEST_ACTION, adptVO.getAdapterGroupVO().getRefClass());
prop.put(API_PATH, httpProp.getProperty(API_PATH, ""));
prop.put(PRE_FILTERS, httpProp.getProperty(PRE_FILTERS, ""));
prop.put(POST_FILTERS, httpProp.getProperty(POST_FILTERS, ""));
prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod());
prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, ""));
isParameterType = false;
try {
traceLevel = Integer.parseInt(traceLevelTemp);
} catch (Exception e) {
traceLevel = 0;
}
stopWatch = new StopWatch();
stopWatch.start();
logger.debug("시작 >> encode = [" + encode + "]");
switch (HttpMethodType.getValue(request.getMethod())) {
case GET:
case DELETE:
isParameterType = true;
break;
case POST:
case PUT:
if (StringUtils.contains(request.getContentType(), "application/x-www-form-urlencoded")) {
isParameterType = true;
} else {
isParameterType = false;
}
break;
default:
break;
}
if (isParameterType) {
paramValue = request.getQueryString();
if (paramValue == null)
paramValue = "";
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
}
// json으로 변환
StringBuilder sb = new StringBuilder();
sb.append("{");
Map<String, String[]> paramMap = assignParameterMap(request, adptGrpName, adptName, null, prop);
int i = 0;
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
if (i > 0) {
sb.append(",");
}
sb.append("\"").append(entry.getKey()).append("\":");
String[] values = entry.getValue();
if (values.length > 1) {
// ["111", "222"]
sb.append("[");
for (int j = 0; j < values.length; j++) {
if (j > 0) {
sb.append(",");
}
sb.append("\"").append(JSONValue.escape(values[j])).append("\"");
}
sb.append("]");
} else {
sb.append("\"").append(JSONValue.escape(values[0])).append("\"");
}
i++;
}
sb.append("}");
paramValue = sb.toString();
} else {
if (ServletFileUpload.isMultipartContent(request)) {
// TODO : 아래의 로직은 업무에 맞게 수정이 필요함.
// 불필요할 경우 제거
// if(StringUtils.isEmpty(uploadRootPath)) {
// uploadRootPath = System.getProperty("java.io.tmpdir");
// }
// 임시로직 : UPLOAD_ROOT_PATH 가 없는 경우에는 JSON에 추가
if (StringUtils.isEmpty(uploadRootPath)) {
paramValue = readMultipartBody(request);
} else {
paramValue = uploadMultipartBody(request, uploadRootPath);
}
// TEST : 테스트용 임시코드
// response.setCharacterEncoding(encode);
// response.getWriter().print(paramValue);
// return;
} else {
ServletInputStream sis = request.getInputStream();
ByteBuffer bb = ByteBuffer.allocate(1024).setAutoExpand(true);
int i = 0;
byte[] cbuf = new byte[1024];
while ((i = sis.read(cbuf, 0, 1024)) != -1) {
if (i == 1024) {
bb.put(cbuf);
} else {
byte[] tail = new byte[i];
System.arraycopy(cbuf, 0, tail, 0, i);
bb.put(tail);
}
}
byte[] data = new byte[bb.position()];
bb.position(0);
bb.get(data);
paramValue = new String(data, encode);
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
}
}
}
if (paramValue == null) { // parameter가 없는 경우때문에 처리
paramValue = "";
}
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] RECV (" + adptGrpName + ") = [" + paramValue + "]\n"
+ CommonLib.getDumpMessage(paramValue));
}
if ("Y".equals(urlDecodeYn) && isParameterType) {
message = URLDecoder.decode(paramValue);
} else {
message = paramValue;
}
if (logger.isDebug()) {
String[] msgArgs = new String[2];
msgArgs[0] = adptGrpName;
msgArgs[1] = message;
String resMsg = ExceptionUtil.make("RICEAIAHA005", msgArgs);
logger.debug(logPrefix + resMsg);
}
adptMsgType = adptVO.getAdapterGroupVO().getMessageType();
if (StringUtils.equals(adptMsgType, MessageType.JSON)) {
response.setContentType(JSON_CONTENT_TYPE+"; charset="+encode);
}
// HEADER_GROUP 셋팅
if (MessageType.JSON.equals(adptMsgType) && StringUtils.isNotBlank(headerGroupName)
&& StringUtils.isNotBlank(relayRequestHeaderKeys)) {
JSONObject jsonMessage = (JSONObject) JSONValue.parse(message);
JSONObject headerJson = new JSONObject();
if (StringUtils.equalsIgnoreCase(relayRequestHeaderKeys, "ALL")) {
for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements();) {
String key = e.nextElement();
headerJson.put(key, request.getHeader(key));
}
} else {
String[] relayKeyArr = org.springframework.util.StringUtils
.tokenizeToStringArray(relayRequestHeaderKeys, ",");
for (String key : relayKeyArr) {
String headerValue = request.getHeader(key);
if (StringUtils.isNotBlank(headerValue)) {
headerJson.put(key, headerValue);
}
}
}
if (headerJson.size() > 0) {
jsonMessage.put(headerGroupName, headerJson);
message = jsonMessage.toJSONString();
}
}
if (message == null) {
message = "";
}
// com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
prop = addCryptoFilter(prop);
// 로컬 서비스 호출 ,encoding 처리 추가
String result = (String) service(adptGrpName, adptName, message, prop, request, response);
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] result " + encode + " (" + adptGrpName + ") = [" + result + "]");
}
stopWatch.stop();
String responseData = "";
if (RESPONSE_TYPE_ASYNC.equals(responseType)) {
if (stopWatch.getTime() > slowTranTime && (logger.isInfo())) {
logger.info("HttpAdapterServiceRest] dummy response time = " + stopWatch.toString() + ", message = "
+ message);
}
if (result == null) {
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
}
response.setCharacterEncoding(encode);
response.getWriter().print(responseData);
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
}
} else {
responseData = result;
logger.info("종료 >> encode = [" + encode + "]");
JSONObject dataObject = null;
if (MessageType.JSON.equals(adptMsgType)) {
dataObject = (JSONObject) JSONValue.parse(responseData);
}
// HEADER_GROUP 하위 필드를 response Header에 세팅한다.
HashMap<String, String> header = new HashMap<>();
boolean redirect = assignHttpHeaders(header, dataObject, headerGroupName);
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] response header field (" + adptGrpName + ") = ["
+ header.toString() + "]");
}
if (redirect) {
response.setStatus(302);
logger.debug("HttpAdapterServiceRest] set response status_code: 302");
} else {
String httpStatus = header.get(HTTP_STATUS);
if (httpStatus != null && !"".equals(httpStatus))
response.setStatus(Integer.parseInt(httpStatus));
header.remove(HTTP_STATUS);
}
// response header 셋팅
for (Map.Entry<String, String> entry : header.entrySet()) {
response.setHeader(entry.getKey(), entry.getValue());
}
if (dataObject != null) {
responseData = dataObject.toJSONString();
}
// UI와 통신시(UTF-8) 변환오류로 ENCODE 제거
response.setCharacterEncoding(encode);
response.getWriter().print(responseData);
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = [" + responseData + "]");
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = "
+ CommonLib.getDumpMessage(responseData));
}
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
}
}
} catch (HttpStatusException e) {
if (traceLevel >= 3) {
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
}
logger.warn("HttpAdapter] " + adptGrpName + "-" + adptName + ">>" + e.getMessage());
response.setStatus(e.getStatus());
try {
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
e.getMessage(), errorResponseFormat);
response.getWriter().println(errorMsg);
} catch (Exception ex) {
// IGNORE
}
} catch (JwtAuthException e) {
if (traceLevel >= 3) {
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
}
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try {
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
e.getMessage(), errorResponseFormat);
response.getWriter().println(errorMsg);
} catch (Exception ex) {
// IGNORE
}
} catch (Exception e) {
if (traceLevel >= 3) {
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
}
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
try {
response.getWriter().println(e.getMessage());
String errCode = ExceptionUtil.getErrorCode(e, "RECEAIAHA003");
throw new Exception(errCode);
} catch (Exception ex) {
// IGNORE
logger.warn(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
}
} finally {
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
String url = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_EXTURI);
String method = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_METHOD);
String adapterGroupName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME);
String adapterName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME);
int httpStatusCode = response.getStatus();
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, 400, adapterGroupName, adapterName, new HashMap<>(), url, method, httpStatusCode);
}
}
private Map<String, String[]> assignParameterMap(HttpServletRequest request, String adptGrpName, String adptName,
Object requestBytes, Properties prop) {
// PathVariable 체크
if (StringUtils.equalsAnyIgnoreCase(request.getMethod(), HttpMethod.GET.name(), HttpMethod.DELETE.name())
&& StringUtils.isBlank(request.getQueryString())) {
try {
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
RequestAction action = ActionFactory.createAction(actionName);
action.setAdapterInfo(adptGrpName, adptName, prop);
String[] keys = action.perform(requestBytes);
String requestPath = keys[0];
// PathVariable 지원 추가
String ruledPath = StandardMessageUtil.getMatchedKey(requestPath, actionName);
if (!StringUtils.equals(requestPath, ruledPath) && StringUtils.contains(ruledPath, "{")) {
Map<String, String> paramMap = new AntPathMatcher().extractUriTemplateVariables(ruledPath,
requestPath);
if (paramMap != null && paramMap.size() > 0) {
Map<String, String[]> returnMap = new HashMap<>();
for (String key : paramMap.keySet()) {
if (StringUtils.equalsIgnoreCase(key, "method")) {
continue;
}
returnMap.put(key, new String[] { paramMap.get(key) });
}
return returnMap;
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
return request.getParameterMap();
}
private void validateServiceAndAdapter(String adptGrpName, String adptName, byte[] requestBytes, Properties prop)
throws JwtAuthException {
}
/**
* request header 를 hashmap으로 조립
*
* @param request
* @return
*/
private Properties getHeaders(HttpServletRequest request) {
Properties prop = new Properties();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
prop.setProperty(key, value);
}
return prop;
}
/**
* adapter property의 HEADER_GROUP으로 정의된 (MFE_HEADER) 그룹의 하위 필드를 http Header에
* 세팅한다.
*
* @param header
* @param object
*/
private boolean assignHttpHeaders(HashMap<String, String> header, Object msg, String headerGroupName) {
boolean redirect = false;
if (msg == null || StringUtils.isBlank(headerGroupName)) {
return false;
}
if (msg instanceof JSONObject) {
JSONObject headerObject = (JSONObject) ((JSONObject) msg).get(headerGroupName);
if (headerObject == null) {
return false;
}
// List<String> headerKeys = new ArrayList<>();
for (Object key : headerObject.keySet()) {
Object obj = headerObject.get(key);
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
continue;
}
header.put((String) key, (String) obj);
if (StringUtils.equalsIgnoreCase((String) key, "Location")) {
redirect = true;
}
}
((JSONObject) msg).remove(headerGroupName);
}
return redirect;
}
/**
* 어댑터 명 이후의 URI값을 가져온다.
*
* @param request
* @return
*/
private String getExtUri(HttpServletRequest request) {
String orgUri = request.getRequestURI().replaceAll(request.getContextPath(), "");
String uri = getExtUri(orgUri, 3);
if (uri != null && uri.trim().length() > 0) {
return "/" + uri;
} else {
return "";
}
}
private static String getExtUri(String url, int length) {
String[] urls = url.split("/");
List<String> newUrls = new ArrayList<>();
Collections.addAll(newUrls, urls);
return StringUtils.join(newUrls.subList(length, urls.length).toArray(), "/");
}
// public static void main(String[] args) throws Exception {
// String orgUri = "/HTT/CbsInNetSys/abcd/123456";
// String result = "";
// result = getExtUri(orgUri, 3);
// System.out.println(result);
// }
}
@@ -66,7 +66,7 @@ public abstract class AbstractCryptoFilter {
* fromPath의 Base64 값을 복호화하여 toPath에 반영. JSON 파싱 1회. * fromPath의 Base64 값을 복호화하여 toPath에 반영. JSON 파싱 1회.
* toPath = "/" → fromPath 필드를 제거하고 복호화된 JSON을 body에 병합. * toPath = "/" → fromPath 필드를 제거하고 복호화된 JSON을 body에 병합.
*/ */
protected String decryptField(String body, String moduleName, Map<String, String> runtimeCtx, protected Object decryptField(String body, String moduleName, Map<String, String> runtimeCtx,
byte[] aad, String fromPath, String toPath) throws Exception { byte[] aad, String fromPath, String toPath) throws Exception {
JsonNode root = JsonPathUtil.toTree(body); JsonNode root = JsonPathUtil.toTree(body);
String encBase64 = JsonPathUtil.getAt(root, fromPath); String encBase64 = JsonPathUtil.getAt(root, fromPath);
@@ -81,7 +81,7 @@ public abstract class AbstractCryptoFilter {
return JsonPathUtil.mergeAtRoot(root, fromPath, plainText); return JsonPathUtil.mergeAtRoot(root, fromPath, plainText);
} }
JsonPathUtil.setAt(root, toPath, plainText); JsonPathUtil.setAt(root, toPath, plainText);
return JsonPathUtil.fromTree(root); return root;
} }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@@ -99,7 +99,7 @@ public abstract class AbstractCryptoFilter {
* fromPath 값을 암호화하여 toPath(Base64)에 반영. JSON 파싱 1회. * fromPath 값을 암호화하여 toPath(Base64)에 반영. JSON 파싱 1회.
* fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑 (파싱 불필요). * fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑 (파싱 불필요).
*/ */
protected String encryptField(String body, String moduleName, Map<String, String> runtimeCtx, protected Object encryptField(String body, String moduleName, Map<String, String> runtimeCtx,
byte[] aad, String fromPath, String toPath) throws Exception { byte[] aad, String fromPath, String toPath) throws Exception {
if (PATH_ROOT.equals(fromPath)) { if (PATH_ROOT.equals(fromPath)) {
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad, byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
@@ -112,13 +112,13 @@ public abstract class AbstractCryptoFilter {
String plainText = JsonPathUtil.getAt(root, fromPath); String plainText = JsonPathUtil.getAt(root, fromPath);
if (StringUtils.isBlank(plainText)) { if (StringUtils.isBlank(plainText)) {
logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath); logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath);
return body; return root;
} }
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad, byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
plainText.getBytes(StandardCharsets.UTF_8)); plainText.getBytes(StandardCharsets.UTF_8));
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes); String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
JsonPathUtil.setAt(root, toPath, encBase64); JsonPathUtil.setAt(root, toPath, encBase64);
return JsonPathUtil.fromTree(root); return root;
} }
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@@ -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.djb.DamoManager;
import com.eactive.ext.kjb.safedb.KjbSafedbWrapper;
@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,122 @@ 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)) {
if ("DAMO".equals(dbEncryptSolutionName)) {
com.eactive.ext.djb.DamoManager damoManager = new com.eactive.ext.djb.DamoManager();
try {
return damoManager.decrypt(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 if ("SAFEDB".equals(dbEncryptSolutionName)) {
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 (!isBase64(data)) {
return false;
}
// Base64 길이 체크 (4의 배수)
if (data.length() % 4 != 0) {
return false;
}
return true;
}
public static boolean isBase64(String str) {
if (str == null || str.isEmpty()) {
return false;
}
String base64Pattern = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$";
return str.matches(base64Pattern);
}
} }
@@ -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";
}
}
@@ -240,33 +240,32 @@ public class AccessTokenManagerByDB implements Lifecycle {
SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function<AccessTokenVO, AccessTokenVO>() { SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function<AccessTokenVO, AccessTokenVO>() {
@Override @Override
public AccessTokenVO apply(AccessTokenVO accessToken) { 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) { logger.debug("Token not exists for adapter group: {}", adapterGroupName);
return issueToken(credential); 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))){
// 다음 스케줄 전에 토큰이 만료되는 경우 재발급 // 다음 스케줄 전에 토큰이 만료되는 경우 재발급
return issueToken(credential); logger.debug("Token expired : {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
return issueToken(credential);
} else {
// 토큰이 아직 유효한 경우
logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
return accessToken;
}
} else { } else {
// 토큰이 아직 유효한 경우 //토큰이 있지만 만료 시간이 없는 경우
logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration()); logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName);
return accessToken; return accessToken;
} }
} else {
//토큰이 있지만 만료 시간이 없는 경우
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) {
logger.error("Token issuance failed for adapter group: {}", adapterGroupName, e); logger.error("Token issuance failed for adapter group: {}", adapterGroupName, e);
@@ -327,12 +326,12 @@ public class AccessTokenManagerByDB implements Lifecycle {
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) { } catch (Exception e) {
logger.error("Task execution failed for adapter group: {}", adapterGroupName, e); logger.error("Task execution failed for adapter group: {}", adapterGroupName, e);
} }
return accessToken; return accessToken;
} }
public void stopToken(String adapterGroupName) { public void stopToken(String adapterGroupName) {
@@ -474,18 +473,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( throw new Exception(
"There is no OAuthCredentialRegistered information, or whether to use it is 'N'."); "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, accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
outboundOAuthCredentialVo); outboundOAuthCredentialVo);
} }
} }
return accessToken; return accessToken;
@@ -1,12 +1,16 @@
package com.eactive.eai.common.exception; package com.eactive.eai.common.exception;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
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;
@@ -56,6 +60,9 @@ public class ExceptionHandler {
} }
public static EAIMessage handle(EAIMessage eaiMessage) { public static EAIMessage handle(EAIMessage eaiMessage) {
Properties prop = eaiMessage.getCallProp();
prop.setProperty("API_INTERNAL_ERROR_CODE", eaiMessage.getRspErrCd());
String inAdapterGroupName = eaiMessage.getSngSysItfTp(); String inAdapterGroupName = eaiMessage.getSngSysItfTp();
AdapterManager manager = AdapterManager.getInstance(); AdapterManager manager = AdapterManager.getInstance();
AdapterGroupVO group = manager.getAdapterGroupVO(inAdapterGroupName); AdapterGroupVO group = manager.getAdapterGroupVO(inAdapterGroupName);
@@ -749,6 +756,55 @@ 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.setOrgRspErrCd(resultEAIMessage.getRspErrCd());
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()) {
@@ -13,6 +13,7 @@ import javax.annotation.PostConstruct;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -44,9 +45,26 @@ public class HsmCryptoService implements PropertyChangeListener {
private static final String PROP_GROUP = "HSM"; private static final String PROP_GROUP = "HSM";
private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN"; private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";
private static final String PROP_CACHE_TTL_SEC = "CACHE_TTL_SEC";
private static long CACHE_TTL_MS = 10 * 60 * 1000; // 10분, 필요시 PropManager로 외부화
private static class CachedKey<T> {
final T key;
final long cachedAt;
CachedKey(T key) {
this.key = key;
this.cachedAt = System.currentTimeMillis();
}
boolean isExpired() {
return System.currentTimeMillis() - cachedAt > CACHE_TTL_MS;
}
}
// HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱 // HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
private final ConcurrentHashMap<String, SecretKey> secretKeyCache = new ConcurrentHashMap<String, SecretKey>(); private final ConcurrentHashMap<String, CachedKey<SecretKey>> secretKeyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, PublicKey> publicKeyCache = new ConcurrentHashMap<String, PublicKey>(); private final ConcurrentHashMap<String, CachedKey<PublicKey>> publicKeyCache = new ConcurrentHashMap<>();
@Autowired @Autowired
private PropManager propManager; private PropManager propManager;
@@ -70,6 +88,9 @@ public class HsmCryptoService implements PropertyChangeListener {
if ("Y".equalsIgnoreCase(reloadYn)) { if ("Y".equalsIgnoreCase(reloadYn)) {
clearKeyCache(); clearKeyCache();
} }
String propCacheTtlSec = propManager.getProperty(PROP_GROUP, PROP_CACHE_TTL_SEC, "600");
CACHE_TTL_MS = (Integer.parseInt(propCacheTtlSec.trim())) * 1000; // 10분, 필요시 PropManager로 외부화
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -78,54 +99,97 @@ public class HsmCryptoService implements PropertyChangeListener {
/** /**
* HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다. * HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
* HSM 장애 시 만료된 캐시가 있으면 그것을 반환하여 서비스 연속성을 유지합니다.
*/ */
public PublicKey getPublicKey(String keyAlias) throws HsmException { public PublicKey getPublicKey(String keyAlias) throws HsmException {
checkReady(); // 1. 유효한 캐시 즉시 반환 (HSM 상태 무관)
PublicKey cached = publicKeyCache.get(keyAlias); CachedKey<PublicKey> cached = publicKeyCache.get(keyAlias);
if (cached != null) { if (cached != null && !cached.isExpired()) {
return cached; return cached.key;
} }
try {
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias); // 2. 캐시 미스 또는 만료 → HSM 갱신 시도
if (cert == null) { if (HsmManager.getInstance().isReady()) {
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias); try {
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
if (cert == null) {
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
}
PublicKey key = cert.getPublicKey();
publicKeyCache.put(keyAlias, new CachedKey<>(key));
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
return key;
} catch (HsmException e) {
throw e;
} catch (Exception e) {
logger.warn("HsmCryptoService] 공개키 HSM 조회 실패: " + e.getMessage());
} }
PublicKey key = cert.getPublicKey();
publicKeyCache.put(keyAlias, key);
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
return key;
} catch (HsmException e) {
throw e;
} catch (Exception e) {
throw new HsmException("공개키 조회 실패: " + e.getMessage(), e);
} }
// 3. HSM 조회 불가 → 만료 캐시 fallback
if (cached != null) {
logger.warn("HsmCryptoService] HSM 장애, 만료 공개키 캐시 fallback: alias=" + keyAlias);
return cached.key;
}
throw new HsmException("공개키 조회 불가: HSM 장애이며 캐시도 없습니다. alias=" + keyAlias);
} }
/** /**
* HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다. * HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
* HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다. * HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다.
*
* [캐싱 전략]
* HSM 에서 가져온 P11SecretKey(PKCS11 핸들 래퍼)를 그대로 캐싱하면, HSM Provider 가
* 재초기화될 때 세션 무효화로 인해 캐시된 키를 사용한 Cipher 연산이 실패한다.
* 따라서 getEncoded() 로 키 바이트를 추출하여 SecretKeySpec(JVM 메모리 키) 으로 변환 후
* 캐싱한다. encryptAes/decryptAes 는 이미 JVM 소프트웨어 Cipher 를 사용하므로,
* HSM Provider 상태와 완전히 독립적으로 동작한다.
*
* HSM 장애 시 만료된 캐시가 있으면 그것을 반환하여 서비스 연속성을 유지합니다.
*/ */
public SecretKey getSecretKey(String keyAlias) throws HsmException { public SecretKey getSecretKey(String keyAlias) throws HsmException {
checkReady(); // 1. 유효한 캐시 즉시 반환 (HSM 상태 무관)
SecretKey cached = secretKeyCache.get(keyAlias); CachedKey<SecretKey> cached = secretKeyCache.get(keyAlias);
if (cached != null) { if (cached != null && !cached.isExpired()) {
return cached; return cached.key;
} }
try {
KeyStore keyStore = HsmManager.getInstance().getKeyStore(); // 2. 캐시 미스 또는 만료 → HSM 갱신 시도
java.security.Key key = keyStore.getKey(keyAlias, null); if (HsmManager.getInstance().isReady()) {
if (!(key instanceof SecretKey)) { try {
throw new HsmException("AES 키를 찾을 수 없습니다 (CKA_EXTRACTABLE=true 확인 필요). alias=" + keyAlias); KeyStore keyStore = HsmManager.getInstance().getKeyStore();
java.security.Key key = keyStore.getKey(keyAlias, null);
if (key == null) {
throw new HsmException("키를 찾을 수 없습니다. alias=" + keyAlias);
}
SecretKey secretKey = (SecretKey) key;
// P11SecretKey → SecretKeySpec 변환: HSM Provider 의존성 제거
// getEncoded() 가 null 이면 non-extractable 키이므로 원본 유지
byte[] keyBytes = secretKey.getEncoded();
if (keyBytes != null) {
secretKey = new SecretKeySpec(keyBytes, secretKey.getAlgorithm());
}
secretKeyCache.put(keyAlias, new CachedKey<>(secretKey));
logger.warn("HsmCryptoService] 대칭키 캐시 등록(갱신): alias=" + keyAlias);
return secretKey;
} catch (HsmException e) {
throw e;
} catch (Exception e) {
logger.warn("HsmCryptoService] 대칭키 HSM 조회 실패: " + e.getMessage());
} }
SecretKey secretKey = (SecretKey) key;
secretKeyCache.put(keyAlias, secretKey);
logger.warn("HsmCryptoService] 대칭키 캐시 등록: alias=" + keyAlias);
return secretKey;
} catch (HsmException e) {
throw e;
} catch (Exception e) {
throw new HsmException("AES 키 조회 실패: " + e.getMessage(), e);
} }
// 3. HSM 조회 불가 → 만료 캐시 fallback
if (cached != null) {
logger.warn("HsmCryptoService] HSM 장애, 만료 대칭키 캐시 fallback: alias=" + keyAlias);
return cached.key;
}
throw new HsmException("키 조회 불가: HSM 장애이며 캐시도 없습니다. alias=" + keyAlias);
} }
/** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */ /** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */
@@ -254,13 +318,4 @@ public class HsmCryptoService implements PropertyChangeListener {
return decryptAes(secretKey, iv, ciphertext, null); return decryptAes(secretKey, iv, ciphertext, null);
} }
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
private void checkReady() throws HsmException {
if (!HsmManager.getInstance().isReady()) {
throw new HsmException("HsmManager 가 초기화되지 않았습니다.");
}
}
} }
@@ -5,12 +5,23 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.AuthProvider;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider; import java.security.Provider;
import java.security.Security; import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.eactive.eai.agent.encryption.EncryptionManager;
import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.exception.ExceptionUtil;
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;
@@ -24,13 +35,29 @@ 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
*
* ---------------------------------------------------------------------
* [세션 누적 방지 / 회로차단 로직 추가]
* 기존 구현은 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로
* 호출하여 PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의
* 최대 세션 수를 초과하면서 reload 가 영구적으로 실패하는 문제가 있었다.
* 이를 방지하기 위해:
* 1) 정상 상황에서는 "기존 keyStore 인스턴스"에 다시 load() 하여 세션 재사용
* 2) 실제 키 조회(probe)로 세션이 살아있는지 검증
* 3) 연속 실패가 임계치를 넘으면 Provider 자체를 logout 후 완전히 재생성
* 4) 재생성마저 실패하면 마지막으로 성공한 keyStore 를 유지 (서비스 연속성 우선)
* ---------------------------------------------------------------------
*/ */
@Component @Component
public class HsmManager implements Lifecycle { public class HsmManager implements Lifecycle {
@@ -40,13 +67,21 @@ public class HsmManager implements Lifecycle {
private static final String GROUP_NAME = "HSM"; private static final String GROUP_NAME = "HSM";
private static final String PROP_CONFIG = "PKCS11_CONFIG"; private static final String PROP_CONFIG = "PKCS11_CONFIG";
private static final String PROP_PIN = "PIN"; private static final String PROP_PIN = "PIN";
private static final String PROP_RELOAD_INTERVAL_MINUTES = "RELOAD_INTERVAL_MINUTES";
private Provider pkcs11Provider; private Provider pkcs11Provider;
private KeyStore keyStore; private volatile KeyStore keyStore;
private boolean started; private boolean started;
private final LifecycleSupport lifecycle = new LifecycleSupport(this); private final LifecycleSupport lifecycle = new LifecycleSupport(this);
private volatile char[] pin;
// 재로드 주기 (분 단위). 필요시 PropManager로 외부화 가능.
private static long RELOAD_INTERVAL_MINUTES = 1;
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> reloadFuture;
private HsmManager() { private HsmManager() {
} }
@@ -63,6 +98,7 @@ public class HsmManager implements Lifecycle {
try { try {
init(); init();
startReloadScheduler();
} catch (Exception e) { } catch (Exception e) {
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002")); throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
} }
@@ -72,8 +108,13 @@ public class HsmManager implements Lifecycle {
} }
private void init() throws Exception { private void init() throws Exception {
String configContent = PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG);
String pin = PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN); String reloadIntervalStr = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES, "10");
RELOAD_INTERVAL_MINUTES = Long.parseLong(reloadIntervalStr);
EncryptionManager encManager = EncryptionManager.getInstance();
String configContent = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
String pinStr = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));
if (configContent == null || configContent.trim().isEmpty()) { if (configContent == null || configContent.trim().isEmpty()) {
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다."); logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
@@ -90,18 +131,142 @@ public class HsmManager implements Lifecycle {
Security.addProvider(pkcs11Provider); Security.addProvider(pkcs11Provider);
keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider); keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
char[] pinChars = (pin != null) ? pin.toCharArray() : null; this.pin = (pinStr != null) ? pinStr.toCharArray() : null;
keyStore.load(null, pinChars); keyStore.load(null, pin);
logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName()); logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
java.util.Enumeration<String> aliases = keyStore.aliases(); logKeyStore(this.keyStore);
}
private void logKeyStore(KeyStore keyStore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
java.util.Enumeration<String> aliases = keyStore.aliases();
StringBuilder aliasList = new StringBuilder(); StringBuilder aliasList = new StringBuilder();
while (aliases.hasMoreElements()) { while (aliases.hasMoreElements()) {
if (aliasList.length() > 0) aliasList.append(", "); if (aliasList.length() > 0) aliasList.append(", ");
aliasList.append(aliases.nextElement());
String alias = aliases.nextElement();
aliasList.append(alias);
java.security.Key key = keyStore.getKey(alias, null);
if (key instanceof SecretKey) {
SecretKey secretKey = (SecretKey) key;
if (secretKey.getEncoded() == null) {
logger.warn("HsmManager] HSM - secretKey null {} : [{}]", alias, secretKey);
}
}
} }
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]"); logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
}
/**
* 별도 스레드에서 주기적으로 KeyStore 를 다시 로드하여
* HSM 에 새로 생성/추가된 키를 인식하도록 한다.
*/
private void startReloadScheduler() {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "hsm-keystore-reloader");
t.setDaemon(true);
return t;
});
reloadFuture = scheduler.scheduleWithFixedDelay(
this::reloadKeyStoreSafely,
RELOAD_INTERVAL_MINUTES,
RELOAD_INTERVAL_MINUTES,
TimeUnit.MINUTES
);
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + RELOAD_INTERVAL_MINUTES + "");
}
/**
* 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다.
* (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다)
*/
private void reloadKeyStoreSafely() {
try {
reloadKeyStoreIfNeeded();
} catch (Throwable t) {
logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage(), t);
}
}
/**
* KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
* 즉시 재시도용으로 직접 호출할 수도 있다.
*
* 세션 누적 방지를 위해 새 KeyStore 인스턴스를 만들지 않고,
* 기존 keyStore 객체에 다시 load() 하여 기존 PKCS11 세션을 재사용한다.
* 연속 실패가 임계치를 넘으면 Provider 자체를 재생성한다.
*/
public synchronized void reloadKeyStoreIfNeeded() throws Exception {
if (pkcs11Provider == null) {
return; // HSM 비활성화 상태
}
try {
if (keyStore != null) {
keyStore.load(null, pin);
logger.warn("HsmManager] KeyStore 재로드 완료 (기존 세션 재사용).");
} else {
KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
ks.load(null, pin);
this.keyStore = ks;
logger.warn("HsmManager] KeyStore 신규 생성 완료.");
}
logKeyStore(keyStore);
} catch (Exception e) {
logger.warn("HsmManager] KeyStore 재로드 실패:" + e.getMessage(), e);
logger.warn("HsmManager] Provider 전체 재초기화를 시도합니다.");
fullReinitialize();
}
}
/**
* 세션 누적, 네트워크 단절 등으로 일반 재로드가 더 이상 복구되지 않을 때
* 기존 세션을 정리하고 Provider 를 완전히 새로 생성한다.
*
* 신규 Provider/KeyStore 준비가 완전히 성공한 후에만 기존 Provider 를 제거하고 교체한다.
* 재초기화 중 예외가 발생하면 기존 Provider 와 keyStore 를 그대로 유지한다
* (서비스 중단보다 마지막 정상 상태 보존을 우선).
*/
private void fullReinitialize() throws Exception {
Provider oldProvider = this.pkcs11Provider;
try {
// 1. 신규 Provider 인스턴스 생성 (Security 미등록 상태)
String configContent = EncryptionManager.getInstance()
.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
Provider newProvider = createProvider(configContent.trim().replace("\\n", "\n"));
// 2. 신규 Provider 로 KeyStore 로드 테스트
// KeyStore.getInstance(type, providerInstance) 는 Security 등록 없이도 동작하므로
// 여기서 실패해도 oldProvider/keyStore 는 변경되지 않은 상태를 유지함
KeyStore ks = KeyStore.getInstance("PKCS11", newProvider);
ks.load(null, pin);
// 3. 신규 연결 성공 → 기존 Provider 정리 후 교체
if (oldProvider instanceof AuthProvider) {
try {
((AuthProvider) oldProvider).logout();
} catch (Exception logoutEx) {
logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage());
}
}
Security.removeProvider(oldProvider.getName());
Security.addProvider(newProvider);
this.pkcs11Provider = newProvider;
this.keyStore = ks;
logger.warn("HsmManager] Provider 전체 재초기화 성공.");
} catch (Exception e) {
logger.warn("HsmManager] Provider 전체 재초기화 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e);
throw e;
}
} }
/** /**
@@ -139,7 +304,21 @@ public class HsmManager implements Lifecycle {
} }
lifecycle.fireLifecycleEvent(STOPING_EVENT, this); lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
if (reloadFuture != null) {
reloadFuture.cancel(false);
}
if (scheduler != null) {
scheduler.shutdown();
}
if (pkcs11Provider != null) { if (pkcs11Provider != null) {
if (pkcs11Provider instanceof AuthProvider) {
try {
((AuthProvider) pkcs11Provider).logout();
} catch (Exception e) {
logger.warn("HsmManager] 종료 시 logout 실패(무시): " + e.getMessage());
}
}
Security.removeProvider(pkcs11Provider.getName()); Security.removeProvider(pkcs11Provider.getName());
} }
pkcs11Provider = null; pkcs11Provider = null;
@@ -180,4 +359,20 @@ public class HsmManager implements Lifecycle {
public boolean isReady() { public boolean isReady() {
return started && pkcs11Provider != null && keyStore != null; return started && pkcs11Provider != null && keyStore != null;
} }
}
/**
* isReady() 와 달리 실제 HSM 호출로 세션 생존 여부까지 확인하는 헬스체크.
* 모니터링/헬스체크 엔드포인트에서 사용을 권장한다.
*/
public boolean isHealthy() {
if (!isReady()) {
return false;
}
try {
logKeyStore(keyStore);
return true;
} catch (Exception e) {
return false;
}
}
}
@@ -104,8 +104,9 @@ public class InflowControlDAO extends BaseDAO {
for (InflowControl inflowControl : inflowControls) { for (InflowControl inflowControl : inflowControls) {
InflowTargetVO vo = new InflowTargetVO(); InflowTargetVO vo = new InflowTargetVO();
vo.setName(inflowControl.getId().getName()); vo.setName(inflowControl.getId().getName());
vo.setThreshold(inflowControl.getThreshold()); vo.setThreshold(inflowControl.getThreshold() == null ? 0 : inflowControl.getThreshold());
vo.setThresholdPerSecond(inflowControl.getThresholdpersecond()); vo.setThresholdPerSecond(
inflowControl.getThresholdpersecond() == null ? 0 : inflowControl.getThresholdpersecond());
vo.setThresholdTimeUnit(inflowControl.getThresholdtimeunit()); vo.setThresholdTimeUnit(inflowControl.getThresholdtimeunit());
vo.setActivate(!"0".equals(inflowControl.getUseyn())); vo.setActivate(!"0".equals(inflowControl.getUseyn()));
targetList.put(vo.getName(), vo); targetList.put(vo.getName(), vo);
@@ -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;
}
}
@@ -1,6 +1,7 @@
package com.eactive.eai.common.logger; package com.eactive.eai.common.logger;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import com.eactive.eai.adapter.ElinkAdapter; import com.eactive.eai.adapter.ElinkAdapter;
@@ -150,7 +151,7 @@ public class DBLogTransactionLogger implements TransactionLogger {
// 템플릿만 남겨두고, 나머지는 사이트에 맞게 수정 필요함. // 템플릿만 남겨두고, 나머지는 사이트에 맞게 수정 필요함.
//----------------------------------------------------> //---------------------------------------------------->
boolean itsmEnabled = false; boolean itsmEnabled = false;
if((itsmEnabled) && !MessageUtil.checkRspErrCd(message.getRspErrCd())) { if((itsmEnabled) && !MessageUtil.checkRspErrCd(message.getLogRspErrCd())) {
if(logger.isDebug()) { if(logger.isDebug()) {
logger.debug(guidLogPrefix + " ITSM Error Message Notify! "); logger.debug(guidLogPrefix + " ITSM Error Message Notify! ");
} }
@@ -176,6 +177,83 @@ public class DBLogTransactionLogger implements TransactionLogger {
} }
} }
/**
* 1. 기능 : 여러 건의 로깅 정보를 한 트랜잭션으로 적재 (COMMIT 1회)
* 2. 처리 개요 :
* - log()의 건별 처리와 달리 COMMIT 횟수를 배치 크기만큼 줄인다.
* - LOG_TYPE 판정 규칙은 log()과 동일하게 유지한다.
* 3. 주의사항
* - 실시간 모니터링(EAIServiceMonitor) 전달은 발행측 EAILogSender.send()에서
* 이미 처리하므로 여기서는 다루지 않는다.
*
* @param items {EAIMessage, Properties} 쌍의 목록
**/
public void logBatch(List<Object[]> items) {
if (items == null || items.isEmpty()) return;
logCount += items.size();
try {
insertLogBatch(items);
} catch (Exception e) {
errCount++;
if (logger.isError()) logger.error("DBLogTransactionLogger] logBatch ERROR. - " + e.getMessage(), e);
}
}
public static void insertLogBatch(List<Object[]> items) {
if (items == null || items.isEmpty()) return;
// Direct DB Logging - 판정 규칙은 log()과 동일
String logType = "DB";
String setLogType = PropManager.getInstance().getProperty("LOG_TYPE");
if (setLogType != null) {
logType = setLogType;
}
if (!"DB".equals(logType) || !EAIDBLogControl.isEnable()) {
for (Object[] item : items) {
writeFileLog((EAIMessage) item[0], (Properties) item[1]);
}
return;
}
EAILogBatchWriter writer = ApplicationContextProvider.getContext().getBean(EAILogBatchWriter.class);
try {
writer.writeBatch(items);
} catch (Exception be) {
// 배치는 한 건만 실패해도 트랜잭션 전체가 롤백된다.
// 정상 건까지 유실되지 않도록 건별 독립 트랜잭션으로 재시도한다.
if (logger.isError()) {
logger.error("DBLogTransactionLogger] insertLogBatch failed, retry one by one. size=" + items.size(), be);
}
for (Object[] item : items) {
EAIMessage eaiMessage = (EAIMessage) item[0];
Properties prop = (Properties) item[1];
try {
writer.writeOne(eaiMessage, prop);
} catch (Exception e) {
String message = e.getMessage();
// DB Connection Error일 경우에만 DB 로깅을 중단한다.
if ("ConnectionError".equals(message) || StringUtils.contains(message, "JDBCConnectionException")
|| StringUtils.contains(message, "Unable to acquire JDBC Connection")) {
EAIDBLogControl.setEnable(false);
}
if (logger.isError()) {
logger.error("DBLogTransactionLogger] insertLogBatch single retry failed. - " + message, e);
}
writeFileLog(eaiMessage, prop);
}
}
}
}
private static void writeFileLog(EAIMessage eaiMessage, Properties prop) {
try {
EAIFileLogger.getInstance().setLog(eaiMessage, prop);
} catch (Exception fe) {
if (logger.isError()) logger.error("DBLogTransactionLogger] file log failed. - " + fe.getMessage(), fe);
}
}
public static void insertLog(EAIMessage eaiMessage, Properties prop) throws EAILogException { public static void insertLog(EAIMessage eaiMessage, Properties prop) throws EAILogException {
String guidLogPrefix = "DBLogTransactionLogger] GUID["+ eaiMessage.getMapper().getGuid(eaiMessage.getStandardMessage()) String guidLogPrefix = "DBLogTransactionLogger] GUID["+ eaiMessage.getMapper().getGuid(eaiMessage.getStandardMessage())
+"] UUID["+eaiMessage.getSvcOgNo()+"] "; +"] UUID["+eaiMessage.getSvcOgNo()+"] ";
@@ -253,7 +253,9 @@ public class EAIFileLogger
* @exception DAOException * @exception DAOException
**/ **/
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) 호출 여부 결정
@@ -274,6 +276,8 @@ public class EAIFileLogger
String psvItfTp = svcMsg.getPsvItfTp(); // 수동 Syunc/Async String psvItfTp = svcMsg.getPsvItfTp(); // 수동 Syunc/Async
int logPssSno = message.getLogPssSno(); int logPssSno = message.getLogPssSno();
String rspErrCd = message.getRspErrCd(); String rspErrCd = message.getRspErrCd();
// 내부오류 대체응답 시 원본 에러코드 기준으로 로깅한다.
String logRspErrCd = message.getLogRspErrCd();
// Duplication Error 방지를 위해 // Duplication Error 방지를 위해
// 원 로그처리일련번호를 저장 : 2009.07.13 // 원 로그처리일련번호를 저장 : 2009.07.13
@@ -517,27 +521,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");
@@ -626,7 +631,7 @@ public class EAIFileLogger
sb.appendAndDelimeter( NullControl.addSpace(message.getSngSysItfTp())); //기동시스템어댑터업무그룹명 sb.appendAndDelimeter( NullControl.addSpace(message.getSngSysItfTp())); //기동시스템어댑터업무그룹명
sb.appendAndDelimeter( NullControl.addSpace(message.getLydMsgID())); //현재메시지ID명 sb.appendAndDelimeter( NullControl.addSpace(message.getLydMsgID())); //현재메시지ID명
sb.appendAndDelimeter( NullControl.addSpace(message.getRspErrCd())); //응답에러코드명 sb.appendAndDelimeter( NullControl.addSpace(logRspErrCd)); //응답에러코드명
sb.appendAndDelimeter( msgPssTm); //메시지처리시각 sb.appendAndDelimeter( msgPssTm); //메시지처리시각
sb.appendAndDelimeter( String.valueOf(message.getSvrLogLvl())); //서버로그레벨번호 sb.appendAndDelimeter( String.valueOf(message.getSvrLogLvl())); //서버로그레벨번호
//index 20 //index 20
@@ -712,8 +717,8 @@ public class EAIFileLogger
sb.appendAndDelimeter( NullControl.addSpace(message.getRspnsChngMsgType())); // 응답변환유형 sb.appendAndDelimeter( NullControl.addSpace(message.getRspnsChngMsgType())); // 응답변환유형
// log level 'E' or 'F' // log level 'E' or 'F'
if ("E".equals(message.getRspErrCd().substring(1, 2)) || "F".equals(message.getRspErrCd().substring(1, 2))) { if ("E".equals(logRspErrCd.substring(1, 2)) || "F".equals(logRspErrCd.substring(1, 2))) {
sb.appendAndDelimeter( NullControl.addSpace(message.getRspErrCd())); //EAI에러코드 sb.appendAndDelimeter( NullControl.addSpace(logRspErrCd)); //EAI에러코드
sb.appendAndDelimeter( StringUtil.chunkString(message.getRspErrMsg(),1000)); //EAI에러내용 sb.appendAndDelimeter( StringUtil.chunkString(message.getRspErrMsg(),1000)); //EAI에러내용
} }
else { else {
@@ -0,0 +1,49 @@
package com.eactive.eai.common.logger;
import java.util.List;
import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.common.message.EAIMessage;
/**
* 1. 기능 : 비동기 거래로그를 여러 건 묶어 한 트랜잭션으로 적재
* 2. 처리 개요 :
* - EAILogDAO는 클래스 레벨 @Transactional(기본 propagation REQUIRED)이므로
* writeBatch 안에서 호출하면 별도 트랜잭션을 열지 않고 바깥 트랜잭션에 합류한다.
* - 결과적으로 N건이 COMMIT 1회로 처리되어 Oracle log file sync 대기가 1/N로 줄어든다.
* 3. 주의사항
* - 배치 중 한 건이라도 예외가 나면 트랜잭션 전체가 롤백된다.
* 호출측(DBLogTransactionLogger.insertLogBatch)에서 건별 재시도로 폴백해야 한다.
*/
@Service
public class EAILogBatchWriter {
@Autowired
private EAILogDAO dao;
/**
* N건을 하나의 트랜잭션으로 적재한다. (COMMIT 1회)
*
* @param items {EAIMessage, Properties} 쌍의 목록
*/
@Transactional
public void writeBatch(List<Object[]> items) throws Exception {
for (Object[] item : items) {
dao.addEAISvcLog((EAIMessage) item[0], (Properties) item[1]);
}
}
/**
* 배치 실패 시 건별 재시도용. 각 건이 독립 트랜잭션이므로
* 특정 건의 실패가 나머지 건에 영향을 주지 않는다.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeOne(EAIMessage message, Properties prop) throws Exception {
dao.addEAISvcLog(message, prop);
}
}
@@ -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
@@ -227,6 +220,8 @@ public class EAILogDAO {
String psvItfTp = svcMsg.getPsvItfTp(); // 수동 Syunc/Async String psvItfTp = svcMsg.getPsvItfTp(); // 수동 Syunc/Async
int logPssSno = message.getLogPssSno(); int logPssSno = message.getLogPssSno();
String rspErrCd = message.getRspErrCd(); String rspErrCd = message.getRspErrCd();
// 내부오류 대체응답 시 원본 에러코드 기준으로 로깅한다.
String logRspErrCd = message.getLogRspErrCd();
// Duplication Error 방지를 위해 // Duplication Error 방지를 위해
// 원 로그처리일련번호를 저장 : 2009.07.13 // 원 로그처리일련번호를 저장 : 2009.07.13
@@ -478,20 +473,12 @@ public class EAILogDAO {
} }
*/ */
// jwhong safeDB 암호화 try {
biz = EncryptionManager.getInstance().encryptDBData(bizMsgStr);
if ("Y".equalsIgnoreCase(EncryptionManager.getInstance().getEncryptYN())) { } catch (Exception e) {
try { logger.error("EAIFileLogger] bizMsgStr encryption Error - " + e.getMessage());
biz = EncryptSafeDb(bizMsgStr);
} catch (Exception e) {
logger.error("EAIFileLogger] bizMsgStr encryption Error - " + e.getMessage());
biz = bizMsgStr;
}
} else {
biz = bizMsgStr; biz = bizMsgStr;
} }
// jwhong safeDB end
// 임시코드 시작 // 임시코드 시작
String logType = PropManager.getInstance().getProperty("BIZ_LOG_TYPE"); String logType = PropManager.getInstance().getProperty("BIZ_LOG_TYPE");
@@ -580,7 +567,7 @@ public class EAILogDAO {
// 현재메시지ID명 // 현재메시지ID명
eaiLog.setPrsntmsgidname(message.getLydMsgID()); eaiLog.setPrsntmsgidname(message.getLydMsgID());
// 응답에러코드명 // 응답에러코드명
eaiLog.setRspnserrcdname(message.getRspErrCd()); eaiLog.setRspnserrcdname(logRspErrCd);
// 메시지처리시각 // 메시지처리시각
eaiLog.setMsgprcssyms(msgPssTm); eaiLog.setMsgprcssyms(msgPssTm);
// 서버로그레벨번호 // 서버로그레벨번호
@@ -674,10 +661,10 @@ public class EAILogDAO {
eaiLog.setRspnschngmsgtype(message.getRspnsChngMsgType()); eaiLog.setRspnschngmsgtype(message.getRspnsChngMsgType());
// log level 'E' or 'F' // log level 'E' or 'F'
if ("E".equals(message.getRspErrCd().substring(1, 2)) if ("E".equals(logRspErrCd.substring(1, 2))
|| "F".equals(message.getRspErrCd().substring(1, 2))) { || "F".equals(logRspErrCd.substring(1, 2))) {
// EAI에러코드 // EAI에러코드
eaiLog.setEaierrcd(message.getRspErrCd()); eaiLog.setEaierrcd(logRspErrCd);
// EAI에러내용 // EAI에러내용
eaiLog.setEaierrctnt(StringUtil.chunkString(message.getRspErrMsg(), 1000)); eaiLog.setEaierrctnt(StringUtil.chunkString(message.getRspErrMsg(), 1000));
} }
@@ -739,11 +726,11 @@ public class EAILogDAO {
} }
// 에러로그를 별도의 테이블에 저장하도록 한다. // 에러로그를 별도의 테이블에 저장하도록 한다.
if (!MessageUtil.checkRspErrCd(message.getRspErrCd())) { if (!MessageUtil.checkRspErrCd(logRspErrCd)) {
try { try {
// 거래통제, 유량제어에 의한 에러는 저장하지 않도록 한다. // 거래통제, 유량제어에 의한 에러는 저장하지 않도록 한다.
if (!(EAIMessageKeys.EAI_BLOCKED_CODE.equals(message.getRspErrCd()) if (!(EAIMessageKeys.EAI_BLOCKED_CODE.equals(logRspErrCd)
|| EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(message.getRspErrCd()))) { || EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(logRspErrCd))) {
addErrorLog(message); // 에러로그 addErrorLog(message); // 에러로그
} }
} catch (Exception ex) { } catch (Exception ex) {
@@ -756,6 +743,8 @@ public class EAILogDAO {
public void addErrorLog(EAIMessage message) throws DAOException { public void addErrorLog(EAIMessage message) throws DAOException {
try { try {
String serverName = EAIServerManager.getInstance().getLocalServerName(); String serverName = EAIServerManager.getInstance().getLocalServerName();
// 내부오류 대체응답 시 원본 에러코드 기준으로 로깅한다.
String logRspErrCd = message.getLogRspErrCd();
EAIErrorLog eaiErrorLog = (EAIErrorLog) applicationContext.getBean(RollingTable.class, EAIErrorLog.class, EAIErrorLog eaiErrorLog = (EAIErrorLog) applicationContext.getBean(RollingTable.class, EAIErrorLog.class,
message.getMsgRcvTm()); message.getMsgRcvTm());
@@ -775,16 +764,16 @@ public class EAILogDAO {
// EAI서비스명 // EAI서비스명
eaiErrorLog.setEaisvcname(message.getEAISvcCd()); eaiErrorLog.setEaisvcname(message.getEAISvcCd());
// 응답에러코드명 // 응답에러코드명
eaiErrorLog.setRspnserrcdname(message.getRspErrCd()); eaiErrorLog.setRspnserrcdname(logRspErrCd);
// 기동시스템어댑터업무그룹명 // 기동시스템어댑터업무그룹명
eaiErrorLog.setGstatsysadptrbzwkgroupname(message.getSngSysItfTp()); eaiErrorLog.setGstatsysadptrbzwkgroupname(message.getSngSysItfTp());
// 수동시스템어댑터업무그룹명 // 수동시스템어댑터업무그룹명
eaiErrorLog.setPsvsysadptrbzwkgroupname(message.getCurrentSvcMsg().getPsvSysItfTp()); eaiErrorLog.setPsvsysadptrbzwkgroupname(message.getCurrentSvcMsg().getPsvSysItfTp());
if ("E".equals(message.getRspErrCd().substring(1, 2)) if ("E".equals(logRspErrCd.substring(1, 2))
|| "F".equals(message.getRspErrCd().substring(1, 2))) { || "F".equals(logRspErrCd.substring(1, 2))) {
// EAI에러코드 // EAI에러코드
eaiErrorLog.setEaierrcd(message.getRspErrCd()); eaiErrorLog.setEaierrcd(logRspErrCd);
// EAI에러내용 // EAI에러내용
eaiErrorLog.setEaierrctnt(StringUtil.chunkString(message.getRspErrMsg(), 500)); eaiErrorLog.setEaierrctnt(StringUtil.chunkString(message.getRspErrMsg(), 500));
} }
@@ -860,88 +849,6 @@ public class EAILogDAO {
} }
return false; return false;
} }
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;
}
/* /*
@@ -1,5 +1,6 @@
package com.eactive.eai.common.logger; package com.eactive.eai.common.logger;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import org.apache.commons.lang3.SerializationUtils; import org.apache.commons.lang3.SerializationUtils;
@@ -71,7 +72,7 @@ public class EAILogSender {
} }
} }
else if(svcLogLvl == 1) { else if(svcLogLvl == 1) {
isLogging = ! MessageUtil.checkRspErrCd(message.getRspErrCd()); isLogging = ! MessageUtil.checkRspErrCd(message.getLogRspErrCd());
} }
else { else {
isLogging = false; isLogging = false;
@@ -105,23 +106,48 @@ public class EAILogSender {
// 실시간 모니터링 로그 // 실시간 모니터링 로그
EAIServiceMonitor servicemonitor = EAIServiceMonitor.getInstance(); EAIServiceMonitor servicemonitor = EAIServiceMonitor.getInstance();
if(EAIMessageKeys.EAI_BLOCKED_CODE.equals(message.getRspErrCd())) { servicemonitor.receiveLogMessage(message);
if(logger.isWarn()) { // if(EAIMessageKeys.EAI_BLOCKED_CODE.equals(message.getRspErrCd())) {
logger.warn(guidLogPrefix + " 거래통제 실시간 모니터링 SKIP - " + message.getEAISvcCd() // if(logger.isWarn()) {
+ ", "+message.getMapper().getGuid(message.getStandardMessage()) ); // logger.warn(guidLogPrefix + " 거래통제 실시간 모니터링 SKIP - " + message.getEAISvcCd()
} // + ", "+message.getMapper().getGuid(message.getStandardMessage()) );
}else if (EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(message.getRspErrCd())) { // }
if(logger.isWarn()) { // }else if (EAIMessageKeys.EAI_INFLOW_BLOCKED_CODE.equals(message.getRspErrCd())) {
logger.warn(guidLogPrefix + " 유량제어 실시간 모니터링 SKIP - " + message.getEAISvcCd() // if(logger.isWarn()) {
+ ", "+ message.getMapper().getGuid(message.getStandardMessage()) ); // logger.warn(guidLogPrefix + " 유량제어 실시간 모니터링 SKIP - " + message.getEAISvcCd()
} // + ", "+ message.getMapper().getGuid(message.getStandardMessage()) );
}else { // }
servicemonitor.receiveLogMessage(message); // }else {
} // servicemonitor.receiveLogMessage(message);
// }
} }
} }
public static void logDirect(EAIMessage message, Properties prop) throws EAILogException { public static void logDirect(EAIMessage message, Properties prop) throws EAILogException {
txLogger.log(message, prop); txLogger.log(message, prop);
}
/**
* 여러 건을 한 트랜잭션(COMMIT 1회)으로 적재한다.
* 비동기 로깅 컨슈머(CustomEventHandler)가 모아둔 배치를 넘길 때 사용한다.
*
* @param items {EAIMessage, Properties} 쌍의 목록
*/
public static void logDirectBatch(List<Object[]> items) {
if (items == null || items.isEmpty()) return;
if (txLogger instanceof DBLogTransactionLogger) {
((DBLogTransactionLogger) txLogger).logBatch(items);
return;
}
// 배치를 지원하지 않는 TransactionLogger 구현이면 건별 처리로 폴백한다.
for (Object[] item : items) {
try {
txLogger.log((EAIMessage) item[0], (Properties) item[1]);
} catch (Exception e) {
logger.error("logDirectBatch fallback failed.", e);
}
}
} }
} }
@@ -1,16 +1,22 @@
package com.eactive.eai.common.logger; package com.eactive.eai.common.logger;
import java.util.List;
import com.eactive.eai.common.logger.mapper.HttpAdapterExtraLogMapper; import com.eactive.eai.common.logger.mapper.HttpAdapterExtraLogMapper;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog; import com.eactive.eai.data.entity.onl.logger.HttpAdapterExtraLog;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class HttpLoggingService { public class HttpLoggingService {
@Autowired static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
@Autowired
private HttpAdapterExtraLogMapper mapper; private HttpAdapterExtraLogMapper mapper;
@Autowired @Autowired
@@ -19,11 +25,26 @@ public class HttpLoggingService {
@Autowired @Autowired
private HttpAdapterExtraLogFileLogger fileLogger; private HttpAdapterExtraLogFileLogger fileLogger;
/**
* 여러 건을 한 트랜잭션(COMMIT 1회)으로 적재한다.
* HttpAdapterExtraLogLogger가 @Transactional(REQUIRED)이므로 이 트랜잭션에 합류한다.
*
* 배치 중 한 건이라도 실패하면 전체가 롤백되므로,
* 호출측에서 insertHttpAdapterExtraLog()로 건별 재시도해야 한다.
*/
@Transactional
public void insertHttpAdapterExtraLogBatch(List<HttpAdapterExtraLogVo> voList) throws Throwable {
for (HttpAdapterExtraLogVo vo : voList) {
dbLogger.save(mapper.toEntity(vo));
}
}
public void insertHttpAdapterExtraLog(HttpAdapterExtraLogVo httpAdapterExtraLogVo) throws Throwable{ public void insertHttpAdapterExtraLog(HttpAdapterExtraLogVo httpAdapterExtraLogVo) throws Throwable{
HttpAdapterExtraLog httpAdapterExtraLog = mapper.toEntity(httpAdapterExtraLogVo); HttpAdapterExtraLog httpAdapterExtraLog = mapper.toEntity(httpAdapterExtraLogVo);
if (EAIDBLogControl.isEnable()) { if (EAIDBLogControl.isEnable()) {
try { try {
dbLogger.save(httpAdapterExtraLog); dbLogger.save(httpAdapterExtraLog);
logger.debug("inserted to DB");
} catch(Exception e){ } catch(Exception e){
String message = e.getMessage(); String message = e.getMessage();
// 오류 메시지 추가 - Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection // 오류 메시지 추가 - Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection
@@ -33,9 +54,11 @@ public class HttpLoggingService {
EAIDBLogControl.setEnable(false); EAIDBLogControl.setEnable(false);
} }
fileLogger.writeFileLog(httpAdapterExtraLog); fileLogger.writeFileLog(httpAdapterExtraLog);
logger.debug("wrote to File");
} }
} else { } else {
fileLogger.writeFileLog(httpAdapterExtraLog); fileLogger.writeFileLog(httpAdapterExtraLog);
logger.debug("wrote to File");
} }
} }
} }
@@ -3,59 +3,124 @@ package com.eactive.eai.common.logger.async;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.eactive.eai.common.logger.EAILogException;
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;
import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.Logger;
import com.lmax.disruptor.EventHandler; import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.LifecycleAware;
import com.lmax.disruptor.TimeoutHandler;
public class CustomEventHandler implements EventHandler<LoggingEvent> { /**
* 1. 기능 : 거래로그 이벤트를 모아 한 트랜잭션(COMMIT 1회)으로 적재
* 2. 처리 개요 :
* - Disruptor의 endOfBatch는 "지금 링버퍼에 더 처리할 이벤트가 없다"는 신호다.
* 이것을 flush 조건으로 쓰면 한산할 때는 건당 즉시 적재되어 지연이 늘지 않고,
* 부하가 몰릴 때만 배치가 커진다. 별도 타임아웃 flush 스레드가 필요 없다.
* - batchSize는 하한이 아니라 상한이다. "100건 모일 때까지 대기"가 아니라
* "한 트랜잭션이 100건을 넘지 않게 끊는다"는 의미다.
* - 안전망으로 TimeoutHandler를 구현한다. 유휴 상태가 지속되면 Disruptor가
* onTimeout()을 호출하므로, 버퍼에 남은 로그가 방치되지 않는다.
* (WaitStrategy가 TimeoutBlockingWaitStrategy = 설정값 "TIME"일 때 동작. 기본값)
* 3. 주의사항
* - LoggingEvent.clear()는 EAIMessage 내용까지 비우므로 버퍼에 담은 뒤 호출하면 안 된다.
* 슬롯 참조만 끊고, EAIMessage 해제는 적재 완료 후 releaseMessages()에서 처리한다.
*/
public class CustomEventHandler implements EventHandler<LoggingEvent>, LifecycleAware, TimeoutHandler {
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
String name;
int sleepMs; private final String name;
private int batchSize = 1; private final int sleepMs;
private final int batchSize;
private int count = 0;
private final List<LoggingEvent> eventList = new ArrayList<>(); private final List<Object[]> buffer;
public CustomEventHandler() {
}
public CustomEventHandler(String name, int sleepMs, int batchSize) { public CustomEventHandler(String name, int sleepMs, int batchSize) {
this.name = name; this.name = name;
this.sleepMs = sleepMs; this.sleepMs = sleepMs;
this.batchSize = batchSize; this.batchSize = (batchSize < 1) ? 1 : batchSize;
this.buffer = new ArrayList<Object[]>(this.batchSize);
} }
@Override @Override
public void onEvent(LoggingEvent event, long sequence, boolean endOfBatch) throws Exception { public void onEvent(LoggingEvent event, long sequence, boolean endOfBatch) throws Exception {
if(sleepMs > 0) Thread.sleep(sleepMs); if (sleepMs > 0) Thread.sleep(sleepMs);
if(batchSize > 1) {
eventList.add(event); EAIMessage message = event.getMessage();
if (++count >= batchSize) { if (message != null) {
processBatch(); buffer.add(new Object[] { message, event.getProperty() });
eventList.clear(); }
count = 0;
} // 링버퍼 슬롯은 재사용되므로 참조만 끊는다. (event.clear() 사용 금지 - 상단 주석 참고)
event.setMessage(null);
event.setProperty(null);
if (endOfBatch || buffer.size() >= batchSize) {
flush();
} }
else {
EAIMessage message = event.getMessage();
if(logger.isInfo()) {
logger.info(String.format("CustomWorkHandler: %s LoggingEvent: %s %s\n"
,name, message.getSvcOgNo() ,message.getLogPssSno())
);
}
EAILogSender.logDirect(event.getMessage(), event.getProperty());
if(event != null) {
event.clear();
event = null;
}
}
} }
private void processBatch() throws EAILogException { private void flush() {
for(LoggingEvent event:eventList) { if (buffer.isEmpty()) return;
EAILogSender.logDirect(event.getMessage(), event.getProperty()); int size = buffer.size();
// info 레벨이 꺼져 있으면 시각 측정 자체를 하지 않는다.
final boolean measure = logger.isInfo();
final long t0 = measure ? System.currentTimeMillis() : 0L;
try {
EAILogSender.logDirectBatch(buffer);
} catch (Throwable th) {
logger.error(String.format("%s] batch log failed. size=%d", name, size), th);
} finally {
// DB 적재 시간만 측정한다. releaseMessages()는 EAIMessage 100건의
// setBizData(null)/svcMsgs.clear()를 도는 비용이라 측정에 섞이면 안 된다.
final long elapsed = measure ? (System.currentTimeMillis() - t0) : 0L;
releaseMessages();
buffer.clear();
if (measure) {
logger.info("{} batch logging. size={}, elapsed={}ms", name, size, elapsed);
}
}
}
/** 적재가 끝난 EAIMessage의 내부 버퍼를 해제한다. (기존 LoggingEvent.clear()가 하던 역할) */
private void releaseMessages() {
for (Object[] item : buffer) {
EAIMessage message = (EAIMessage) item[0];
if (message == null) continue;
try {
message.clear();
} catch (Exception e) {
// 해제 실패는 적재 결과에 영향이 없으므로 무시한다.
}
}
}
/**
* 유휴 시 Disruptor가 호출하는 안전망.
* 정상 흐름에서는 endOfBatch로 이미 flush되지만, onEvent가 예외로 중단되어
* 버퍼가 남은 뒤 거래가 끊기는 경우를 대비한다.
*/
@Override
public void onTimeout(long sequence) throws Exception {
if (buffer.isEmpty()) return;
if (logger.isDebug()) {
logger.debug(String.format("%s] flush by timeout. remain=%d", name, buffer.size()));
}
flush();
}
@Override
public void onStart() {
if (logger.isWarn()) {
logger.warn(String.format(">> %s start. batchSize = %d", name, batchSize));
}
}
/** 컨슈머 스레드 종료 시 버퍼에 남은 로그를 반드시 적재한다. */
@Override
public void onShutdown() {
flush();
if (logger.isWarn()) {
logger.warn(String.format("<< %s shutdown.", name));
} }
} }
} }
@@ -0,0 +1,135 @@
package com.eactive.eai.common.logger.async;
import java.util.ArrayList;
import java.util.List;
import com.eactive.eai.common.logger.EAIDBLogControl;
import com.eactive.eai.common.logger.HttpAdapterExtraLogVo;
import com.eactive.eai.common.logger.HttpLoggingService;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.LifecycleAware;
import com.lmax.disruptor.TimeoutHandler;
/**
* 1. 기능 : HTTP 헤더 로그 이벤트를 모아 한 트랜잭션(COMMIT 1회)으로 적재
* 2. 처리 개요 :
* - endOfBatch를 flush 조건으로 사용한다. 한산할 때는 건당 즉시 적재되고
* 부하가 몰릴 때만 배치가 커지므로 별도 타임아웃 스레드가 필요 없다.
* - batchSize는 하한이 아니라 상한이다. "100건 모일 때까지 대기"가 아니라
* "한 트랜잭션이 100건을 넘지 않게 끊는다"는 의미다.
* - 안전망으로 TimeoutHandler를 구현한다. 유휴 시 Disruptor가 onTimeout()을
* 호출하므로 버퍼에 남은 로그가 방치되지 않는다.
* (WaitStrategy가 TimeoutBlockingWaitStrategy = 설정값 "TIME"일 때 동작. 기본값)
* - 배치 실패 시 기존 건별 경로(insertHttpAdapterExtraLog)로 재시도한다.
* 그 경로가 DB 장애 판정과 파일로그 폴백을 이미 담고 있다.
* 3. 주의사항
* - HttpLoggingService 빈은 필드에 캐싱한다. 이벤트마다 타입 기반 getBean을
* 호출하면 컨슈머 처리량이 떨어진다.
*/
public class HttpLoggingBatchEventHandler implements EventHandler<HttpLoggingEvent>, LifecycleAware, TimeoutHandler {
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
private final String name;
private final int batchSize;
private final List<HttpAdapterExtraLogVo> buffer;
private HttpLoggingService service;
public HttpLoggingBatchEventHandler(String name, int batchSize) {
this.name = name;
this.batchSize = (batchSize < 1) ? 1 : batchSize;
this.buffer = new ArrayList<HttpAdapterExtraLogVo>(this.batchSize);
}
private HttpLoggingService service() {
if (service == null) {
service = ApplicationContextProvider.getContext().getBean(HttpLoggingService.class);
}
return service;
}
@Override
public void onEvent(HttpLoggingEvent event, long sequence, boolean endOfBatch) throws Exception {
HttpAdapterExtraLogVo vo = event.getHttpAdapterExtraLogVo();
if (vo != null) {
buffer.add(vo);
}
// 링버퍼 슬롯은 재사용되므로 참조를 끊는다. VO 내용은 유지된다.
event.clear();
if (endOfBatch || buffer.size() >= batchSize) {
flush();
}
}
private void flush() {
if (buffer.isEmpty()) return;
int size = buffer.size();
// info 레벨이 꺼져 있으면 시각 측정 자체를 하지 않는다.
final boolean measure = logger.isInfo();
final long t0 = measure ? System.currentTimeMillis() : 0L;
try {
if (EAIDBLogControl.isEnable()) {
service().insertHttpAdapterExtraLogBatch(buffer);
if (logger.isDebug()) {
logger.debug(String.format("%s] flushed %d http log(s) in one transaction", name, size));
}
} else {
writeEach();
}
} catch (Throwable th) {
// 배치는 한 건만 실패해도 전체가 롤백된다. 건별로 재시도해 정상 건을 살린다.
logger.error(String.format("%s] batch http log failed, retry one by one. size=%d", name, size), th);
writeEach();
} finally {
buffer.clear();
}
if (measure) {
logger.info("{} batch logging. size={}, elapsed={}ms", name, size, System.currentTimeMillis() - t0);
}
}
/** 기존 건별 경로. DB 장애 판정과 파일로그 폴백이 이 안에 있다. */
private void writeEach() {
for (HttpAdapterExtraLogVo vo : buffer) {
try {
service().insertHttpAdapterExtraLog(vo);
} catch (Throwable th) {
logger.error("failed to insert async http log ", th);
}
}
}
/**
* 유휴 시 Disruptor가 호출하는 안전망.
* 정상 흐름에서는 endOfBatch로 이미 flush되지만, onEvent가 예외로 중단되어
* 버퍼가 남은 뒤 거래가 끊기는 경우를 대비한다.
*/
@Override
public void onTimeout(long sequence) throws Exception {
if (buffer.isEmpty()) return;
if (logger.isDebug()) {
logger.debug(String.format("%s] flush by timeout. remain=%d", name, buffer.size()));
}
flush();
}
@Override
public void onStart() {
if (logger.isWarn()) {
logger.warn(String.format(">> %s start. batchSize = %d", name, batchSize));
}
}
/** 컨슈머 스레드 종료 시 버퍼에 남은 로그를 반드시 적재한다. */
@Override
public void onShutdown() {
flush();
if (logger.isWarn()) {
logger.warn(String.format("<< %s shutdown.", name));
}
}
}
@@ -8,6 +8,14 @@ import lombok.Data;
public class HttpLoggingEvent { public class HttpLoggingEvent {
private HttpAdapterExtraLogVo httpAdapterExtraLogVo; private HttpAdapterExtraLogVo httpAdapterExtraLogVo;
/**
* 링버퍼 슬롯의 참조만 끊는다.
* VO 자체는 컨슈머가 배치 버퍼에 담아 사용하므로 내용을 비우면 안 된다.
*/
public void clear() {
this.httpAdapterExtraLogVo = null;
}
public final static EventFactory<HttpLoggingEvent> EVENT_FACTORY = new EventFactory<HttpLoggingEvent>() { public final static EventFactory<HttpLoggingEvent> EVENT_FACTORY = new EventFactory<HttpLoggingEvent>() {
public HttpLoggingEvent newInstance() { public HttpLoggingEvent newInstance() {
return new HttpLoggingEvent(); return new HttpLoggingEvent();
@@ -17,6 +17,8 @@ public class HttpLoggingPoolObject {
Disruptor<HttpLoggingEvent> disruptor = null; Disruptor<HttpLoggingEvent> disruptor = null;
RingBuffer<HttpLoggingEvent> ringBuffer = null; RingBuffer<HttpLoggingEvent> ringBuffer = null;
int id = 0; int id = 0;
// 실제 링버퍼 크기로 생성자에서 설정한다. shutdown()이 이 값과 remainingCapacity를
// 비교하므로 하드코딩하면 queue.size 변경 시 종료되지 않는다.
int queueMax = (int)Math.pow(2, 10); int queueMax = (int)Math.pow(2, 10);
int workerSize = 0; int workerSize = 0;
@@ -25,7 +27,6 @@ public class HttpLoggingPoolObject {
} }
private WaitStrategy getWaitStrategy(String waitStrategy) { private WaitStrategy getWaitStrategy(String waitStrategy) {
WaitStrategy ws = null;
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_BLOCK.equals(waitStrategy)) { if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_BLOCK.equals(waitStrategy)) {
// throughput and low-latency are not as important as CPU resource // throughput and low-latency are not as important as CPU resource
return new BlockingWaitStrategy(); return new BlockingWaitStrategy();
@@ -47,14 +48,23 @@ public class HttpLoggingPoolObject {
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_YIELD.equals(waitStrategy)) { if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_YIELD.equals(waitStrategy)) {
return new YieldingWaitStrategy(); return new YieldingWaitStrategy();
} }
return ws; // 알 수 없는 값이면 null이 아니라 기본값(TIME)을 돌려준다.
// null을 넘기면 컨슈머 스레드가 NPE로 죽어 로깅이 통째로 멈춘다.
// TimeoutBlockingWaitStrategy여야 배치 핸들러의 onTimeout 안전망도 동작한다.
if(waitStrategy != null && logger.isWarn()) {
logger.warn(String.format(">> unknown waitStrategy [%s], fallback to TIME(100ms)", waitStrategy));
}
return new TimeoutBlockingWaitStrategy(100 * 1000, TimeUnit.MICROSECONDS);
} }
public HttpLoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy) { public HttpLoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy, int batchSize) {
this.id = id; this.id = id;
this.queueMax = queueSize;
this.workerSize = workerSize;
if(logger.isWarn()) { if(logger.isWarn()) {
logger.warn(String.format(">> Disruptor-%d queueSize = %d", id, queueSize)); logger.warn(String.format(">> Disruptor-%d queueSize = %d", id, queueSize));
logger.warn(String.format(">> Disruptor-%d workerSize = %d", id, workerSize)); logger.warn(String.format(">> Disruptor-%d workerSize = %d", id, workerSize));
logger.warn(String.format(">> Disruptor-%d batchSize = %d", id, batchSize));
logger.warn(String.format(">> Disruptor-%d waitStrategy = %s", id, waitStrategy)); logger.warn(String.format(">> Disruptor-%d waitStrategy = %s", id, waitStrategy));
} }
CustomThreadFactory tFactory = new CustomThreadFactory(); CustomThreadFactory tFactory = new CustomThreadFactory();
@@ -63,12 +73,21 @@ public class HttpLoggingPoolObject {
ProducerType.SINGLE, ProducerType.SINGLE,
getWaitStrategy(waitStrategy)); getWaitStrategy(waitStrategy));
// BlockingWaitStrategy | SleepingWaitStrategy | YieldingWaitStrategy | BusySpinWaitStrategy // BlockingWaitStrategy | SleepingWaitStrategy | YieldingWaitStrategy | BusySpinWaitStrategy
WorkHandler<HttpLoggingEvent>[] handlers = new WorkHandler[workerSize]; if(workerSize > 1) {
for(int i=0; i< handlers.length; i++) { // 건별 COMMIT 경로. WorkHandler에는 endOfBatch가 없어 배치 적재가 불가능하다.
WorkHandler handler = new HttpLoggingWorkHandler(); WorkHandler<HttpLoggingEvent>[] handlers = new WorkHandler[workerSize];
handlers[i] = handler; for(int i=0; i< handlers.length; i++) {
WorkHandler handler = new HttpLoggingWorkHandler();
handlers[i] = handler;
}
disruptor.handleEventsWithWorkerPool(handlers);
}
else {
// 배치 COMMIT 경로. 컨슈머 병렬도는 디스크럽터 풀 개수(pool.maxsize)로 확보한다.
HttpLoggingBatchEventHandler handler =
new HttpLoggingBatchEventHandler(String.format("HttpLoggingBatchEventHandler%d-%d", id, 0), batchSize);
disruptor.handleEventsWith(handler);
} }
disruptor.handleEventsWithWorkerPool(handlers);
try { try {
@@ -18,7 +18,8 @@ public class HttpLoggingPoolObjectFactory extends BasePooledObjectFactory<HttpLo
int queueSize = ElinkConfig.getAsyncQueueSize(); int queueSize = ElinkConfig.getAsyncQueueSize();
int workers = ElinkConfig.getAsyncWorkers(); int workers = ElinkConfig.getAsyncWorkers();
String waitStrategy = ElinkConfig.getWaitStrategy(); String waitStrategy = ElinkConfig.getWaitStrategy();
return new HttpLoggingPoolObject(i++, queueSize, workers, waitStrategy); int batchSize = ElinkConfig.getAsyncBatchSize();
return new HttpLoggingPoolObject(i++, queueSize, workers, waitStrategy, batchSize);
} }
@Override @Override
@@ -22,6 +22,8 @@ public class LoggingPoolObject {
Disruptor<LoggingEvent> disruptor = null; Disruptor<LoggingEvent> disruptor = null;
RingBuffer<LoggingEvent> ringBuffer = null; RingBuffer<LoggingEvent> ringBuffer = null;
int id = 0; int id = 0;
// 실제 링버퍼 크기로 생성자에서 설정한다. shutdown()이 이 값과 remainingCapacity를
// 비교하므로 하드코딩하면 queue.size 변경 시 종료되지 않는다.
int queueMax = (int)Math.pow(2, 10); int queueMax = (int)Math.pow(2, 10);
int workerSize = 0; int workerSize = 0;
@@ -30,7 +32,6 @@ public class LoggingPoolObject {
} }
private WaitStrategy getWaitStrategy(String waitStrategy) { private WaitStrategy getWaitStrategy(String waitStrategy) {
WaitStrategy ws = null;
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_BLOCK.equals(waitStrategy)) { if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_BLOCK.equals(waitStrategy)) {
// throughput and low-latency are not as important as CPU resource // throughput and low-latency are not as important as CPU resource
return new BlockingWaitStrategy(); return new BlockingWaitStrategy();
@@ -52,33 +53,45 @@ public class LoggingPoolObject {
if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_YIELD.equals(waitStrategy)) { if(ConfigKeys.LOGGER_ASYNC_WAITSTRATEGY_YIELD.equals(waitStrategy)) {
return new YieldingWaitStrategy(); return new YieldingWaitStrategy();
} }
return ws; // 알 수 없는 값이면 null이 아니라 기본값(TIME)을 돌려준다.
// null을 넘기면 컨슈머 스레드가 NPE로 죽어 로깅이 통째로 멈춘다.
// TimeoutBlockingWaitStrategy여야 배치 핸들러의 onTimeout 안전망도 동작한다.
if(waitStrategy != null && logger.isWarn()) {
logger.warn(String.format(">> unknown waitStrategy [%s], fallback to TIME(100ms)", waitStrategy));
}
return new TimeoutBlockingWaitStrategy(100 * 1000, TimeUnit.MICROSECONDS);
} }
public LoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy) { public LoggingPoolObject(int id, int queueSize, int workerSize, String waitStrategy, int batchSize) {
this.id = id; this.id = id;
this.queueMax = queueSize;
this.workerSize = workerSize;
if(logger.isWarn()) { if(logger.isWarn()) {
logger.warn(String.format(">> Disruptor-%d queueSize = %d", id, queueSize)); logger.warn(String.format(">> Disruptor-%d queueSize = %d", id, queueSize));
logger.warn(String.format(">> Disruptor-%d workerSize = %d", id, workerSize)); logger.warn(String.format(">> Disruptor-%d workerSize = %d", id, workerSize));
logger.warn(String.format(">> Disruptor-%d batchSize = %d", id, batchSize));
logger.warn(String.format(">> Disruptor-%d waitStrategy = %s", id, waitStrategy)); logger.warn(String.format(">> Disruptor-%d waitStrategy = %s", id, waitStrategy));
} }
CustomThreadFactory tFactory = new CustomThreadFactory(); CustomThreadFactory tFactory = new CustomThreadFactory();
disruptor = new Disruptor<LoggingEvent>(LoggingEvent.EVENT_FACTORY, queueSize, tFactory, disruptor = new Disruptor<LoggingEvent>(LoggingEvent.EVENT_FACTORY, queueSize, tFactory,
ProducerType.SINGLE, ProducerType.SINGLE,
getWaitStrategy(waitStrategy)); getWaitStrategy(waitStrategy));
// BlockingWaitStrategy | SleepingWaitStrategy | YieldingWaitStrategy | BusySpinWaitStrategy // BlockingWaitStrategy | SleepingWaitStrategy | YieldingWaitStrategy | BusySpinWaitStrategy
if(workerSize > 1) { if(workerSize > 1) {
// 건별 COMMIT 경로. WorkHandler에는 endOfBatch가 없어 배치 적재가 불가능하다.
// 커밋 횟수를 줄이려면 worker.size=1로 두고 아래 배치 EventHandler를 사용한다.
WorkHandler<LoggingEvent>[] handlers = new WorkHandler[workerSize]; WorkHandler<LoggingEvent>[] handlers = new WorkHandler[workerSize];
for(int i=0; i< handlers.length; i++) { for(int i=0; i< handlers.length; i++) {
// TODO : 현재는 delay 없이 처리하도록 하고, 추후 DB부하를 줄이려면 sleep을 정의. // TODO : 현재는 delay 없이 처리하도록 하고, 추후 DB부하를 줄이려면 sleep을 정의.
CustomWorkHandler handler = new CustomWorkHandler(String.format("CustomWorkHandler%d-%d",id, i), 0, 1); CustomWorkHandler handler = new CustomWorkHandler(String.format("CustomWorkHandler%d-%d",id, i), 0, 1);
handlers[i] = handler; handlers[i] = handler;
} }
disruptor.handleEventsWithWorkerPool(handlers); disruptor.handleEventsWithWorkerPool(handlers);
} }
else { else {
CustomEventHandler handler = new CustomEventHandler(String.format("CustomEventHandler%d-%d",id, 0), 0, 1); // 배치 COMMIT 경로. 컨슈머 병렬도는 디스크럽터 풀 개수(pool.maxsize)로 확보한다.
CustomEventHandler handler = new CustomEventHandler(String.format("CustomEventHandler%d-%d",id, 0), 0, batchSize);
disruptor.handleEventsWith(handler); disruptor.handleEventsWith(handler);
} }
@@ -19,7 +19,8 @@ public class LoggingPoolObjectFactory extends BasePooledObjectFactory<LoggingPoo
int queueSize = ElinkConfig.getAsyncQueueSize(); int queueSize = ElinkConfig.getAsyncQueueSize();
int workers = ElinkConfig.getAsyncWorkers(); int workers = ElinkConfig.getAsyncWorkers();
String waitStrategy = ElinkConfig.getWaitStrategy(); String waitStrategy = ElinkConfig.getWaitStrategy();
return new LoggingPoolObject(i++, queueSize, workers, waitStrategy); int batchSize = ElinkConfig.getAsyncBatchSize();
return new LoggingPoolObject(i++, queueSize, workers, waitStrategy, batchSize);
} }
@Override @Override
@@ -1,11 +1,11 @@
package com.eactive.eai.common.message; package com.eactive.eai.common.message;
import java.io.Serializable; import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import com.eactive.eai.message.StandardMessage; import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.manager.StandardMessageManager; import com.eactive.eai.message.manager.StandardMessageManager;
@@ -109,7 +109,10 @@ public class EAIMessage implements EAIMessageKeys, Serializable, Comparable<EAIM
private String authkeyfieldname; private String authkeyfieldname;
private String authCheckdYn; private String authCheckdYn;
private Properties callProp;
private String orgRspErrCd;
public EAIMessage() { public EAIMessage() {
this.svcMsgs = new ArrayList<>(); this.svcMsgs = new ArrayList<>();
@@ -664,6 +667,31 @@ public class EAIMessage implements EAIMessageKeys, Serializable, Comparable<EAIM
public Map<String, Object> getTransactionContextTransfer() { public Map<String, Object> getTransactionContextTransfer() {
return transactionContextTransfer; return transactionContextTransfer;
} }
public Properties getCallProp() {
return callProp;
}
public void setCallProp(Properties callProp) {
this.callProp = callProp;
}
public String getOrgRspErrCd() {
return orgRspErrCd;
}
public void setOrgRspErrCd(String orgRspErrCd) {
this.orgRspErrCd = orgRspErrCd;
}
/**
* 로깅/모니터링용 응답에러코드를 반환한다.
* 내부오류를 어댑터 에러메시지 핸들러로 대체 응답한 경우
* rspErrCd 에는 정상코드가 설정되므로 원본 에러코드(orgRspErrCd)를 우선한다.
*/
public String getLogRspErrCd() {
return (orgRspErrCd != null && orgRspErrCd.length() > 0) ? orgRspErrCd : rspErrCd;
}
/** /**
* [비동기 전달 전용] 컨텍스트 전달용 Map 설정 * [비동기 전달 전용] 컨텍스트 전달용 Map 설정
@@ -292,7 +292,7 @@ public class EAIServiceMonitor implements Lifecycle {
msgPssTm = msg.getMsgPssTm(); msgPssTm = msg.getMsgPssTm();
msgRcvTm = msg.getMsgRcvTm(); msgRcvTm = msg.getMsgRcvTm();
logPssSno = msg.getLogPssSno(); logPssSno = msg.getLogPssSno();
rspErrCd = msg.getRspErrCd(); rspErrCd = msg.getLogRspErrCd();
eaiSvcCd = msg.getEAISvcCd(); eaiSvcCd = msg.getEAISvcCd();
svcOgNo = msg.getSvcOgNo(); svcOgNo = msg.getSvcOgNo();
bwkCls = msg.getBwkCls(); bwkCls = msg.getBwkCls();
@@ -316,6 +316,10 @@ public class EAIServiceMonitor implements Lifecycle {
if (rspErrCd.length() >= 12) { if (rspErrCd.length() >= 12) {
error = rspErrCd.substring(1, 2); error = rspErrCd.substring(1, 2);
} }
if (rspErrCd.equals("RECEAIINA001")) {
error = "S";
}
// 에러와 타임아웃을 분리 : 이동훈 // 에러와 타임아웃을 분리 : 이동훈
if (errorCode[0].equals(error.toUpperCase()) || errorCode[1].equals(error.toUpperCase())) { if (errorCode[0].equals(error.toUpperCase()) || errorCode[1].equals(error.toUpperCase())) {
@@ -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();
@@ -0,0 +1,28 @@
package com.eactive.eai.common.security;
import lombok.Data;
/**
* CryptoModuleManager 진단정보 DTO.
*
* 원본 암호키(encKey/decKey) 및 IV는 절대 포함하지 않는다. 메타데이터와
* 키 도출 전략(KeyDerivationStrategy) 로드 상태만 담는다.
*/
@Data
public class CryptoModuleDiagnosticDTO {
String cryptoName;
String cryptoDesc;
String algType;
String cipherMode;
String padding;
boolean hasIv;
String keySourceType;
String keyDerivStrategy;
boolean strategyLoaded;
String strategyClassName;
String strategyLoadError;
String cacheYn;
Integer cacheTtlSec;
String useYn;
}
@@ -1,5 +1,6 @@
package com.eactive.eai.common.security; package com.eactive.eai.common.security;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -144,11 +145,67 @@ 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());
} }
/**
* 등록된 전체 암호화모듈의 진단정보를 반환한다.
* 원본 키(encKey/decKey/ivHex)는 포함하지 않는다.
*/
public List<CryptoModuleDiagnosticDTO> describeAll() {
List<CryptoModuleDiagnosticDTO> result = new ArrayList<>();
for (String cryptoName : configMap.keySet()) {
result.add(describe(cryptoName));
}
return result;
}
/**
* 단일 암호화모듈의 진단정보를 반환한다. DYNAMIC 키 방식인 경우 resolveStrategy를 통해
* 전략 클래스 로드 성공 여부를 함께 확인한다. 원본 키(encKey/decKey/ivHex)는 포함하지 않는다.
*/
public CryptoModuleDiagnosticDTO describe(String cryptoName) {
CryptoModuleConfigVO vo = getVO(cryptoName);
CryptoModuleDiagnosticDTO dto = new CryptoModuleDiagnosticDTO();
dto.setCryptoName(vo.getCryptoName());
dto.setCryptoDesc(vo.getCryptoDesc());
dto.setAlgType(vo.getAlgType());
dto.setCipherMode(vo.getCipherMode());
dto.setPadding(vo.getPadding());
dto.setHasIv(vo.getIvHex() != null);
dto.setKeySourceType(vo.getKeySourceType());
dto.setCacheYn(vo.getCacheYn());
dto.setCacheTtlSec(vo.getCacheTtlSec());
dto.setUseYn(vo.getUseYn());
if (!"STATIC".equalsIgnoreCase(vo.getKeySourceType()) && vo.getKeyDerivStrategy() != null) {
dto.setKeyDerivStrategy(vo.getKeyDerivStrategy());
try {
KeyDerivationStrategy strategy = resolveStrategy(vo.getKeyDerivStrategy());
dto.setStrategyLoaded(true);
dto.setStrategyClassName(strategy.getClass().getName());
} catch (Exception e) {
dto.setStrategyLoaded(false);
dto.setStrategyLoadError(e.getMessage());
}
}
return dto;
}
/**
* 동적 키 캐시에 존재하는 캐시 키 목록을 반환한다. (전략별 buildCacheKey 결과이며, 원본 키 값이 아니다)
*/
public List<String> listDynamicCacheKeys() {
return new ArrayList<>(dynamicKeyCache.keySet());
}
private CryptoModuleConfigVO getVO(String cryptoName) { private CryptoModuleConfigVO getVO(String cryptoName) {
CryptoModuleConfigVO vo = configMap.get(cryptoName); CryptoModuleConfigVO vo = configMap.get(cryptoName);
if (vo == null) { if (vo == null) {
@@ -173,7 +230,7 @@ public class CryptoModuleManager implements Lifecycle {
private KeyDerivationStrategy resolveStrategy(String fqcn) { private KeyDerivationStrategy resolveStrategy(String fqcn) {
return strategyCache.computeIfAbsent(fqcn, key -> { return strategyCache.computeIfAbsent(fqcn, key -> {
try { try {
Class<?> clazz = Class.forName(key); Class<?> clazz = Class.forName(key.trim());
return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance(); return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e); throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e);
@@ -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;
}
} }
@@ -3,7 +3,12 @@ package com.eactive.eai.common.security.keyderiv;
import java.util.Map; 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";
/** /**
* 키를 도출한다. * 키를 도출한다.
* *
@@ -14,8 +14,6 @@ import com.eactive.eai.common.util.ApplicationContextProvider;
*/ */
public abstract class BaseHsmKeyDerivationStrategy implements KeyDerivationStrategy { public abstract class BaseHsmKeyDerivationStrategy implements KeyDerivationStrategy {
protected static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
protected byte[] getHsmKey(Map<String, String> params) throws HsmException { protected byte[] getHsmKey(Map<String, String> params) throws HsmException {
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS); String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
return getHsmKey(hsmKeyAlias); return getHsmKey(hsmKeyAlias);
@@ -26,6 +24,11 @@ public abstract class BaseHsmKeyDerivationStrategy implements KeyDerivationStrat
return masterKey.getEncoded(); 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) { private String required(Map<String, String> params, String key) {
String value = params.get(key); String value = params.get(key);
if (value == null || value.trim().isEmpty()) { if (value == null || value.trim().isEmpty()) {
@@ -1,65 +0,0 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import javax.crypto.SecretKey;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HSM 마스터키 + 런타임 컨텍스트값을 연결(concatenate)한 뒤 SHA-256 해싱으로 키를 도출하는 전략.
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
*
* key_deriv_params (JSON) 예시:
* {
* "hsmKeyAlias" : "MASTER_KEY_AES",
* "contextKey" : "X-Api-Group-Seq" -- runtimeContext에서 꺼낼 키 이름
* }
*/
public class HsmContextSha256KeyDerivationStrategy implements KeyDerivationStrategy {
private static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
private static final String PARAM_CONTEXT_KEY = "contextKey";
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
String contextKey = required(params, PARAM_CONTEXT_KEY);
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
byte[] masterKeyBytes = masterKey.getEncoded();
byte[] contextBytes = runtimeContext.getOrDefault(contextKey, "")
.getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[masterKeyBytes.length + contextBytes.length];
System.arraycopy(masterKeyBytes, 0, combined, 0, masterKeyBytes.length);
System.arraycopy(contextBytes, 0, combined, masterKeyBytes.length, contextBytes.length);
byte[] derived = MessageDigest.getInstance("SHA-256").digest(combined);
return new DerivedKey(derived, derived);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
String contextValue = runtimeContext.getOrDefault(contextKey, "");
return cryptoName + ":" + contextValue;
}
private String required(Map<String, String> params, String key) {
String value = params.get(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
}
return value;
}
private HsmCryptoService hsmCryptoService() {
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
}
}
@@ -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;
}
}
@@ -246,6 +246,8 @@ public abstract class SessionManager implements Lifecycle {
public abstract CacheInfoVO getSocketCache(); public abstract CacheInfoVO getSocketCache();
public abstract CacheInfoVO getOutboundAccessTokenCache();
public abstract String getMasterInstId(); public abstract String getMasterInstId();
// Convert terminalId to userId // Convert terminalId to userId
@@ -887,7 +887,12 @@ public class SessionManagerForEhcache extends SessionManager {
public CacheInfoVO getSocketCache() { public CacheInfoVO getSocketCache() {
return convert(cacheSocket); return convert(cacheSocket);
} }
@Override
public CacheInfoVO getOutboundAccessTokenCache() {
throw new UnsupportedOperationException();
}
@Override @Override
public void putWebSocketTimeout(String key, SessionVO value) { public void putWebSocketTimeout(String key, SessionVO value) {
cacheWebSocketTimeout.put(new Element(key, value)); cacheWebSocketTimeout.put(new Element(key, value));
@@ -818,6 +818,11 @@ public class SessionManagerForIgnite extends SessionManager {
public CacheInfoVO getSocketCache() { public CacheInfoVO getSocketCache() {
return convert(cacheSocket); return convert(cacheSocket);
} }
@Override
public CacheInfoVO getOutboundAccessTokenCache() {
return convert(cacheOutBoundAccessToken);
}
@Override @Override
public void putWebSocketTimeout(String key, SessionVO value) { public void putWebSocketTimeout(String key, SessionVO value) {
@@ -49,9 +49,10 @@ public class STDMessageDAO extends BaseDAO {
for (StandardMessageInfo info : list) { for (StandardMessageInfo info : list) {
vo = standardManager.getStandardMessage(); vo = standardManager.getStandardMessage();
vo.setServiceKey(info.getBzwksvckeyname()); vo.setServiceKey(info.getBzwksvckeyname());
vo.setEaiSvcCd(info.getEaisvcname());
// TODO // TODO
// to be defined... // to be defined...
mapper.setInterfaceId(vo, info.getEaitranname()); // mapper.setInterfaceId(vo, info.getEaitranname());
mapper.setSendRecvDivision(vo, info.getEaisendrecv()); // S | R mapper.setSendRecvDivision(vo, info.getEaisendrecv()); // S | R
mapper.setInExDivision(vo, info.getEaidirection()); // 1 | 2 mapper.setInExDivision(vo, info.getEaidirection()); // 1 | 2
List<StandardMessageItem> items = info.getStandardMessageItems(); List<StandardMessageItem> items = info.getStandardMessageItems();
@@ -2,24 +2,23 @@ package com.eactive.eai.common.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.http.Header;
import com.eactive.eai.common.logger.HttpAdapterExtraHeaderVo; import com.eactive.eai.common.logger.HttpAdapterExtraHeaderVo;
import com.eactive.eai.common.logger.HttpAdapterExtraLogVo; import com.eactive.eai.common.logger.HttpAdapterExtraLogVo;
import com.eactive.eai.common.logger.HttpLoggingService; import com.eactive.eai.common.logger.HttpLoggingService;
import com.eactive.eai.common.logger.async.AsyncHttpLoggingPoolManager; import com.eactive.eai.common.logger.async.AsyncHttpLoggingPoolManager;
import com.eactive.eai.env.ElinkConfig; import com.eactive.eai.env.ElinkConfig;
import org.apache.hc.core5.http.Header;
import org.apache.commons.lang3.StringUtils;
public class HttpAdapterExtraLogUtil { public class HttpAdapterExtraLogUtil {
public static final int MAX_HEADER_VALUE_SIZE = 400;
public static final String BODY_FIELD_NAME = "eapim-adapter-send-body";
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
public static boolean isHttpHeaderMode() { public static boolean isHttpHeaderMode() {
@@ -62,14 +61,23 @@ public class HttpAdapterExtraLogUtil {
httpAdapterExtraLogVo.setHttpMethod(httpMethod); httpAdapterExtraLogVo.setHttpMethod(httpMethod);
for (HttpAdapterExtraHeaderVo httpAdapterExtraHeaderVo : headerVoList) { for (HttpAdapterExtraHeaderVo httpAdapterExtraHeaderVo : headerVoList) {
String name = httpAdapterExtraHeaderVo.getName();
// if(StringUtils.isNotBlank(name) && "authorization".equals(name.toLowerCase())) {
// httpAdapterExtraHeaderVo.setValue("{hidden}");
// }
if(StringUtils.isNotBlank(name) && BODY_FIELD_NAME.equals(name))
continue;
String value = httpAdapterExtraHeaderVo.getValue(); String value = httpAdapterExtraHeaderVo.getValue();
if(StringUtils.isNotBlank(value) && value.length() > MAX_HEADER_VALUE_SIZE) {
if (value == null) { value = value.substring(0, MAX_HEADER_VALUE_SIZE) + "...";
httpAdapterExtraHeaderVo.setValue(" "); httpAdapterExtraHeaderVo.setValue(value);
} }else if(value == null){
httpAdapterExtraHeaderVo.setValue(" ");
}
} }
httpAdapterExtraLogVo.setHeaderList(headerVoList); httpAdapterExtraLogVo.setHeaderList(headerVoList);
httpAdapterExtraLogVo.setHttpStatus(httpStatus); httpAdapterExtraLogVo.setHttpStatus(httpStatus);
@@ -110,7 +118,7 @@ public class HttpAdapterExtraLogUtil {
} }
public static List<HttpAdapterExtraHeaderVo> convertHeaderToListOfHttpAdapterExtraHeaderVo(Header[] headers) { public static List<HttpAdapterExtraHeaderVo> convertHeaderToListOfHttpAdapterExtraHeaderVo(Header[] headers) {
Set<String> seenNames = new HashSet<>(); // Set<String> seenNames = new HashSet<>();
return Arrays.stream(headers) return Arrays.stream(headers)
// .filter(header -> seenNames.add(header.getName())) // 중복된 이름을 스킵 // .filter(header -> seenNames.add(header.getName())) // 중복된 이름을 스킵
.map(header -> new HttpAdapterExtraHeaderVo(header.getName(), header.getValue())) .map(header -> new HttpAdapterExtraHeaderVo(header.getName(), header.getValue()))
@@ -0,0 +1,448 @@
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.JSONParser;
import org.json.simple.parser.ParseException;
public class JSONUtils {
private JSONUtils() {
throw new IllegalStateException("Utility class");
}
public static Object parseJsonGeneric(Object jsonData) {
if (jsonData == null) {
return new JSONObject();
}
if (jsonData instanceof JSONObject) {
return (JSONObject) jsonData;
} else if (jsonData instanceof String) {
if (StringUtils.isBlank((String) jsonData)) {
return new JSONObject();
}
try {
JSONParser parser = new JSONParser();
Object parsedObj = parser.parse((String) jsonData);
return parsedObj;
} catch (ParseException e) {
e.printStackTrace();
return null;
}
} else
throw new IllegalArgumentException("not support json type. " + jsonData.getClass());
}
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;
}
}

Some files were not shown because too many files have changed in this diff Show More