Compare commits

...

13 Commits

Author SHA1 Message Date
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
37 changed files with 2430 additions and 119 deletions
@@ -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'));",
"});"
]
}
}
]
}
]
}
]
}
@@ -57,7 +57,7 @@ public class HttpStatusException extends Exception {
public String getCode() {
if (StringUtils.isBlank(code)) {
return String.format("Http Status: %d", status);
return String.format("HttCd:%d", status);
} else {
return code;
}
@@ -11,7 +11,9 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
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.SSLConnectionSocketFactory;
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.HttpRequestInterceptor;
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.http.secure.HttpClient5SSLContextFactory;
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.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager;
@@ -416,8 +420,24 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
logProcessNo = 200;
}
Header[] headers = request.getHeaders();
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, request.getHeaders(), url, request.getMethod());
contextAdapterGroupName, contextAdapterName, headerVoList, url, request.getMethod(), 0);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
@@ -88,7 +88,14 @@ public class DeadlineAwareRetryExecutor {
if (!policy.shouldRetryOnStatus(request, status)) {
// 성공 또는 재시도 불필요 → 호출부에서 close 책임
log.debug("non-retryable response status={}, attempt={}", status, attempt + 1);
log.info("non-retryable response status={}, attempt={}", status, attempt + 1);
return response;
}
// ④ 마지막 시도였으면 종료
if (attempt >= policy.getMaxRetries()) {
// 최대 재처리에 도달하여, 재시도하지 않고, 응답 객체를 리턴한다.
log.info("최대 재시도 횟수 도달 response status={}, attempt={}", status, attempt + 1);
return response;
}
@@ -127,9 +134,6 @@ public class DeadlineAwareRetryExecutor {
}
}
// ④ 마지막 시도였으면 바로 종료
if (attempt >= policy.getMaxRetries()) break;
// ⑤ 백오프 대기 전 남은 시간 재확인
long remainingBeforeWait = totalDeadlineMs - (System.currentTimeMillis() - startTime);
if (remainingBeforeWait <= policy.getBackoffMs()) {
@@ -171,10 +175,6 @@ public class DeadlineAwareRetryExecutor {
log.info("RequestConfig = {}", config);
}
private boolean isSuccess(int status) {
return status >= 200 && status < 300;
}
private void closeQuietly(CloseableHttpResponse response) {
try {
response.close();
@@ -14,6 +14,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.hc.client5.http.classic.methods.HttpPost;
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.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHost;
@@ -135,7 +136,15 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
byte[] responseMessage = null;
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();
responseMessage = EntityUtils.toByteArray(response.getEntity());
responseString = new String(responseMessage, vo.getEncode());
@@ -6,6 +6,7 @@ import java.math.BigDecimal;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
@@ -65,7 +66,9 @@ import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.CommonLib;
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
import com.eactive.eai.common.util.JacksonUtil;
import com.eactive.eai.common.util.RestSendBodyLogUtils;
import com.eactive.eai.common.util.TxFileLogger;
import com.eactive.eai.common.util.XMLUtils;
import com.eactive.eai.util.JsonPathUtil;
@@ -363,23 +366,12 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
configureHttpClient(requestConfigBuilder, vo, method);
assignFilterHeaders(method, tempProp); // OutBound PreFilter에서 설정한 Header내용 설정.
String postBody = "";
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() + "]");
}
assignGetParam(dataObject, uri, method);
break;
case PUT:
case POST:
@@ -388,13 +380,14 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
//assignPostBody(dataContent, contentType, vo.getEncode(), method); // jwhong commnet
// KJBank API 추적정보를 Header에 제공. jwhong
// String inboundToken= assignRequestKJHeaders(method, prop, tempProp );
assignPostBody(dataObject, mimeType, charset, method, chunked);
postBody = assignPostBody(dataObject, mimeType, charset, method, chunked);
break;
default:
break;
}
if (logger.isDebugEnabled()) {
logger.debug("HttpClientAdapterServiceRest] Request URI : " + method.getRequestUri());
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") method.getParams()=["
+ method.getEntity() + "] ");
}
@@ -403,7 +396,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
// 전달 header 셋팅
// Adapter 레벨 header 보다 data로 넘어온 httpHeader를 우선 적용(header명이 같다면 덮어씀)
assignRequestHeaders(method, httpHeader, prop);
assignRequestHeaders(method, httpHeader, prop, tempProp);
int status = -1;
@@ -447,6 +440,11 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
}
if (RestSendBodyLogUtils.isBodyLoggingApi(tempProp.getProperty("API_SERVICE_CODE"))) {
String trimmedBody = RestSendBodyLogUtils.getBodyByMaxSize(postBody);
context.setAttribute(HttpAdapterExtraLogUtil.BODY_FIELD_NAME, trimmedBody);
}
// encrypt algorithm type 추출 from adapter property jwhong
// Properties properties = AdapterPropManager.getInstance().getProperties(vo.getAdapterName()); // jwhong
// String encryptAlgorithm = properties.getProperty("ENCRYPT_ALGORITHM"); // jwhong
@@ -716,6 +714,24 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
}
}
private void assignGetParam(Object dataObject, String uri, HttpUriRequestBase method)
throws Exception, URISyntaxException {
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() + "]");
}
}
private void assignOutboundPropertyMap(Properties tempProp, int status, String responseString,
Properties responseHeaderProp) {
Map<String, Object> map = new HashMap<String, Object>();
@@ -846,22 +862,22 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
}
}
protected void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
method.setHeader("Authorization",
String.format("%s", authorization));
protected void setAuthHeaders(HttpUriRequestBase method, String authorization, String authorizationHeaderName)
throws Exception {
if (StringUtils.isEmpty(authorizationHeaderName))
authorizationHeaderName = "Authorization";
method.setHeader(authorizationHeaderName, String.format("%s", authorization));
}
protected void setAuthHeaders(HttpUriRequestBase method, String authorization, String authorizationHeaderName) throws Exception {
method.setHeader(authorizationHeaderName,
String.format("%s", authorization));
}
protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken,
String authorizationHeaderName) throws Exception {
if (StringUtils.isEmpty(authorizationHeaderName))
authorizationHeaderName = "Authorization";
protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
method.setHeader("Authorization",
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
}
protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken, String authorizationHeaderName) throws Exception {
if (StringUtils.isEmpty(accessToken.getTokenType()))
method.setHeader(authorizationHeaderName, String.format("%s", accessToken.getAccessToken()));
else
method.setHeader(authorizationHeaderName,
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
}
@@ -896,7 +912,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
return false;
}
protected void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader, Properties prop) {
protected void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader, Properties prop, Properties tempProp) {
if (httpHeader != null) {
if ( httpHeader instanceof ObjectNode) {
ObjectNode objectNode = (ObjectNode) httpHeader;
@@ -65,7 +65,7 @@ public class RetryPolicy {
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_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;
}
}
@@ -85,4 +85,8 @@ public interface HttpAdapterServiceKey {
//응답 처리 용 표준 전문 오브젝트
static final String STANDARD_MESSAGE_OBJECT = "STANDARD_MESSAGE_OBJECT";
// 어댑터별 인증 키 헤더 이름
static final String ADAPTER_TOKEN_HEADER_NAME = "ADAPTER_TOKEN_HEADER_NAME";
static final String ADAPTER_APIKEY_HEADER_NAME = "ADAPTER_APIKEY_HEADER_NAME";
}
@@ -22,10 +22,10 @@ import com.eactive.eai.inbound.error.InboundErrorInfoVO;
import com.eactive.eai.inbound.error.InboundErrorKeys;
import com.eactive.eai.util.HexaConverter;
public class UnkownMessageLogUtils {
public class UnknownMessageLogUtils {
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
private UnkownMessageLogUtils() {
private UnknownMessageLogUtils() {
throw new IllegalStateException("Utility class");
}
@@ -49,7 +49,7 @@ public class UnkownMessageLogUtils {
StringBuffer sb = new StringBuffer();
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());
UnkownMessageLogUtils.logUnknownMessage(txId, adptGrpName, adptName,
UnknownMessageLogUtils.logUnknownMessage(txId, adptGrpName, adptName,
"", "", false, errCode, sb.toString(), System.currentTimeMillis(),
serverName, InboundErrorKeys.IN_UNKNOWN, message);
} catch (Exception ex) {
@@ -1,5 +1,26 @@
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.authserver.service.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.stdmessage.STDMessageManager;
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.RequestAction;
import com.eactive.eai.inbound.processor.Processor;
@@ -25,27 +45,6 @@ import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.util.IOUtils;
import com.nimbusds.jwt.SignedJWT;
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 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 {
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
@@ -124,7 +123,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
switch (eaiMessage.getAuthType()){
case "oauth":
try {
String token = extractBearerToken(request);
String token = extractBearerToken(request, prop);
SignedJWT signedJWT = SignedJWT.parse(token);
if (signedJWT.getJWTClaimsSet().getExpirationTime() == null) {
@@ -182,8 +181,23 @@ public class ApiAuthFilter implements HttpAdapterFilter {
}
break;
case "api_key":
String apiKeyName = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
String apiKey = request.getHeader(apiKeyName);
// 기관별로 API Key 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_TOKEN_HEADER_NAME)을 우선 사용하고
// 없으면 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){
// QueryString으로 전달된 access_token 파라미터 확인
String apiKeyParamName = PropManager.getInstance().getProperty("ApiConfig", "api.key.param.name", "x-api-key");
@@ -191,14 +205,14 @@ public class ApiAuthFilter implements HttpAdapterFilter {
if (StringUtils.isNotBlank(queryApiKey)) {
apiKey = queryApiKey.trim();
} 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);
isPassScope = true;
break;
case "ca":
String token = extractBearerToken(request);
String token = extractBearerToken(request, prop);
BearerTokenInfo bearerTokenInfo = SessionManager.getInstance().getCAToken(token);
if ( bearerTokenInfo != null ) {
if( bearerTokenInfo.isExpired() ) {
@@ -319,8 +333,13 @@ public class ApiAuthFilter implements HttpAdapterFilter {
return false;
}
public static String extractBearerToken(HttpServletRequest request) throws JwtAuthException {
String tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization");
public static String extractBearerToken(HttpServletRequest request, Properties prop) throws JwtAuthException {
// 기관별로 토큰 헤더명이 다를 수 있어, 어댑터별 설정(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);
if (StringUtils.isBlank(authorization)) {
// QueryString으로 전달된 access_token 파라미터 확인
@@ -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;
}
}
@@ -31,7 +31,7 @@ import com.eactive.eai.adapter.http.HttpMemoryLogger;
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.UnkownMessageLogUtils;
import com.eactive.eai.adapter.http.dynamic.UnknownMessageLogUtils;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.util.CommonLib;
@@ -213,6 +213,10 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
stopWatch.stop();
String apiInternalErrorCode = prop.getProperty("API_INTERNAL_ERROR_CODE", "");
if(StringUtils.isNotEmpty(apiInternalErrorCode))
response.setStatus(500);
byte[] responseBytes = null;
String responseData = "";
if (RESPONSE_TYPE_ASYNC.equals(responseType)){
@@ -337,10 +341,10 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
} catch (Exception e) {
try {
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes, encode) : null, prop, request, response,
UnknownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes, encode) : null, prop, request, response,
"RECEAIIRP202", e);
} catch (UnsupportedEncodingException e1) {
UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes) : null, prop, request, response,
UnknownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes) : null, prop, request, response,
"RECEAIIRP202", e);
}
if (traceLevel >= 3){
@@ -242,15 +242,16 @@ public class AccessTokenManagerByDB implements Lifecycle {
@Override
public AccessTokenVO apply(AccessTokenVO accessToken) {
long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000);
// // 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급
if (accessToken == null) {
logger.debug("Token not exists for adapter group: {}", adapterGroupName);
return issueToken(credential);
} else if(accessToken.getExpiration() != null) {
// 토큰이 있고 만료시간이 설정 된 경우
if(accessToken.getExpiration().before(new Date(intervalTime))){
// 다음 스케줄 전에 토큰이 만료되는 경우 재발급
logger.debug("Token expired : {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
return issueToken(credential);
} else {
// 토큰이 아직 유효한 경우
@@ -263,10 +264,8 @@ public class AccessTokenManagerByDB implements Lifecycle {
return accessToken;
}
}
});
logger.debug("Token issuance completed for adapter group: {}", adapterGroupName);
} catch (Exception e) {
logger.error("Token issuance failed for adapter group: {}", adapterGroupName, e);
@@ -1,9 +1,9 @@
package com.eactive.eai.common.exception;
import java.nio.charset.Charset;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONValue;
import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
@@ -60,6 +60,9 @@ public class ExceptionHandler {
}
public static EAIMessage handle(EAIMessage eaiMessage) {
Properties prop = eaiMessage.getCallProp();
prop.setProperty("API_INTERNAL_ERROR_CODE", eaiMessage.getRspErrCd());
String inAdapterGroupName = eaiMessage.getSngSysItfTp();
AdapterManager manager = AdapterManager.getInstance();
AdapterGroupVO group = manager.getAdapterGroupVO(inAdapterGroupName);
@@ -104,8 +104,9 @@ public class InflowControlDAO extends BaseDAO {
for (InflowControl inflowControl : inflowControls) {
InflowTargetVO vo = new InflowTargetVO();
vo.setName(inflowControl.getId().getName());
vo.setThreshold(inflowControl.getThreshold());
vo.setThresholdPerSecond(inflowControl.getThresholdpersecond());
vo.setThreshold(inflowControl.getThreshold() == null ? 0 : inflowControl.getThreshold());
vo.setThresholdPerSecond(
inflowControl.getThresholdpersecond() == null ? 0 : inflowControl.getThresholdpersecond());
vo.setThresholdTimeUnit(inflowControl.getThresholdtimeunit());
vo.setActivate(!"0".equals(inflowControl.getUseyn()));
targetList.put(vo.getName(), vo);
@@ -1,6 +1,7 @@
package com.eactive.eai.common.logger;
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 org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -10,6 +11,8 @@ import org.springframework.stereotype.Service;
@Service
public class HttpLoggingService {
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
@Autowired
private HttpAdapterExtraLogMapper mapper;
@@ -24,6 +27,7 @@ public class HttpLoggingService {
if (EAIDBLogControl.isEnable()) {
try {
dbLogger.save(httpAdapterExtraLog);
logger.debug("inserted to DB");
} catch(Exception e){
String message = e.getMessage();
// 오류 메시지 추가 - Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection
@@ -33,9 +37,11 @@ public class HttpLoggingService {
EAIDBLogControl.setEnable(false);
}
fileLogger.writeFileLog(httpAdapterExtraLog);
logger.debug("wrote to File");
}
} else {
fileLogger.writeFileLog(httpAdapterExtraLog);
logger.debug("wrote to File");
}
}
}
@@ -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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -153,6 +154,58 @@ public class CryptoModuleManager implements Lifecycle {
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) {
CryptoModuleConfigVO vo = configMap.get(cryptoName);
if (vo == null) {
@@ -246,6 +246,8 @@ public abstract class SessionManager implements Lifecycle {
public abstract CacheInfoVO getSocketCache();
public abstract CacheInfoVO getOutboundAccessTokenCache();
public abstract String getMasterInstId();
// Convert terminalId to userId
@@ -888,6 +888,11 @@ public class SessionManagerForEhcache extends SessionManager {
return convert(cacheSocket);
}
@Override
public CacheInfoVO getOutboundAccessTokenCache() {
throw new UnsupportedOperationException();
}
@Override
public void putWebSocketTimeout(String key, SessionVO value) {
cacheWebSocketTimeout.put(new Element(key, value));
@@ -819,6 +819,11 @@ public class SessionManagerForIgnite extends SessionManager {
return convert(cacheSocket);
}
@Override
public CacheInfoVO getOutboundAccessTokenCache() {
return convert(cacheOutBoundAccessToken);
}
@Override
public void putWebSocketTimeout(String key, SessionVO value) {
cacheWebSocketTimeout.put(key, value);
@@ -2,24 +2,25 @@ package com.eactive.eai.common.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.HttpAdapterExtraLogVo;
import com.eactive.eai.common.logger.HttpLoggingService;
import com.eactive.eai.common.logger.async.AsyncHttpLoggingPoolManager;
import com.eactive.eai.env.ElinkConfig;
import org.apache.hc.core5.http.Header;
import org.apache.commons.lang3.StringUtils;
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);
public static boolean isHttpHeaderMode() {
@@ -62,14 +63,19 @@ public class HttpAdapterExtraLogUtil {
httpAdapterExtraLogVo.setHttpMethod(httpMethod);
for (HttpAdapterExtraHeaderVo httpAdapterExtraHeaderVo : headerVoList) {
// String name = httpAdapterExtraHeaderVo.getName();
// if(StringUtils.isNotBlank(name) && "authorization".equals(name.toLowerCase())) {
// httpAdapterExtraHeaderVo.setValue("{hidden}");
// }
String value = httpAdapterExtraHeaderVo.getValue();
if (value == null) {
if(StringUtils.isNotBlank(value) && value.length() > MAX_HEADER_VALUE_SIZE) {
value = value.substring(0, 400) + "...";
httpAdapterExtraHeaderVo.setValue(value);
}else if(value == null){
httpAdapterExtraHeaderVo.setValue(" ");
}
}
httpAdapterExtraLogVo.setHeaderList(headerVoList);
httpAdapterExtraLogVo.setHttpStatus(httpStatus);
@@ -0,0 +1,49 @@
package com.eactive.eai.common.util;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
public class RestSendBodyLogUtils {
private static final String PROP_LOG_MAX_DATA_SIZE = "log.max.data.size";
private static final String PROP_GROUP = "RestSendBodyLog";
private RestSendBodyLogUtils() {
throw new IllegalStateException("Utility class");
}
/**
* API ID 별 어댑터 body 로그 남기는 지 여부
*
* @param apiId
* @return
*/
public static boolean isBodyLoggingApi(String apiId) {
if (StringUtils.isEmpty(apiId))
return false;
String logging = PropManager.getInstance().getProperty(PROP_GROUP, apiId, "N");
return StringUtils.equals("Y", logging);
}
/**
* body 로그에 남길 만큼 자르기
*
* @param body
* @return
*/
public static String getBodyByMaxSize(String body) {
String maxDataSize = PropManager.getInstance().getProperty(PROP_GROUP, PROP_LOG_MAX_DATA_SIZE);
int imaxDataSize = HttpAdapterExtraLogUtil.MAX_HEADER_VALUE_SIZE;
if (maxDataSize != null)
imaxDataSize = Integer.parseInt(maxDataSize);
if (body.length() > imaxDataSize)
return StringUtils.substring(body, 0, imaxDataSize) + "...";
return body;
}
}
@@ -0,0 +1,96 @@
package com.eactive.eai.manage.crypto;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
/**
* 암호화모듈(CryptoModuleManager) 진단 및 암복호화 테스트 API.
*
* 원본 암호키(encKey/decKey/ivHex)는 응답에 포함하지 않는다. 메타데이터와
* 키 도출 전략(KeyDerivationStrategy) 로드 상태만 노출한다.
*
* GET /manage/crypto/list → 등록된 전체 암호화모듈 진단정보
* GET /manage/crypto/{cryptoName} → 단일 암호화모듈 진단정보
* GET /manage/crypto/cache/dynamic-keys → 동적 키 캐시 키 목록 (원본 키 아님)
* POST /manage/crypto/{cryptoName}/test/encrypt → 테스트 암호화 (Base64 입출력)
* POST /manage/crypto/{cryptoName}/test/decrypt → 테스트 복호화 (Base64 입출력)
*/
@RestController
@RequestMapping("/manage/crypto")
public class CryptoModuleManageController {
@Autowired
private CryptoModuleManageService cryptoModuleManageService;
@GetMapping("/list")
public ResponseEntity<?> listAll() {
List<CryptoModuleDiagnosticDTO> list = cryptoModuleManageService.listAll();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("data", list);
result.put("count", list.size());
return ResponseEntity.ok(result);
}
@GetMapping("/{cryptoName}")
public ResponseEntity<?> get(@PathVariable String cryptoName) {
return respond(() -> cryptoModuleManageService.get(cryptoName));
}
@GetMapping("/cache/dynamic-keys")
public ResponseEntity<?> listDynamicCacheKeys() {
List<String> keys = cryptoModuleManageService.listDynamicCacheKeys();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("data", keys);
result.put("count", keys.size());
return ResponseEntity.ok(result);
}
@PostMapping("/{cryptoName}/test/encrypt")
public ResponseEntity<?> testEncrypt(@PathVariable String cryptoName, @RequestBody CryptoTestRequestDTO request) {
return respond(() -> {
String cipherTextBase64 = cryptoModuleManageService.testEncrypt(cryptoName,
request.getRuntimeContext(), request.getPlainTextBase64(), request.getAadBase64());
Map<String, Object> data = new HashMap<>();
data.put("cipherTextBase64", cipherTextBase64);
return data;
});
}
@PostMapping("/{cryptoName}/test/decrypt")
public ResponseEntity<?> testDecrypt(@PathVariable String cryptoName, @RequestBody CryptoTestRequestDTO request) {
return respond(() -> {
String plainTextBase64 = cryptoModuleManageService.testDecrypt(cryptoName,
request.getRuntimeContext(), request.getCipherTextBase64(), request.getAadBase64());
Map<String, Object> data = new HashMap<>();
data.put("plainTextBase64", plainTextBase64);
return data;
});
}
private ResponseEntity<?> respond(Callable<Object> action) {
Map<String, Object> result = new HashMap<>();
try {
result.put("success", true);
result.put("data", action.call());
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok(result);
}
}
@@ -0,0 +1,61 @@
package com.eactive.eai.manage.crypto;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
import com.eactive.eai.common.security.CryptoModuleExtension;
import com.eactive.eai.common.security.CryptoModuleManager;
/**
* CryptoModuleManager 진단 및 암복호화 테스트를 위임하는 서비스.
*
* createExtension(cryptoName, runtimeContext) 는 STATIC 키 방식인 경우 내부에서
* createExtension(cryptoName) 으로 위임하므로, 테스트 시 runtimeContext 유무와 무관하게
* 동일 메서드를 사용한다.
*/
@Service
public class CryptoModuleManageService {
public List<CryptoModuleDiagnosticDTO> listAll() {
return CryptoModuleManager.getInstance().describeAll();
}
public CryptoModuleDiagnosticDTO get(String cryptoName) {
return CryptoModuleManager.getInstance().describe(cryptoName);
}
public List<String> listDynamicCacheKeys() {
return CryptoModuleManager.getInstance().listDynamicCacheKeys();
}
public String testEncrypt(String cryptoName, Map<String, String> runtimeContext,
String plainTextBase64, String aadBase64) throws Exception {
byte[] plain = Base64.getDecoder().decode(plainTextBase64);
CryptoModuleExtension ext = CryptoModuleManager.getInstance()
.createExtension(cryptoName, runtimeContext(runtimeContext));
byte[] cipher = aadBase64 != null
? ext.encrypt(plain, Base64.getDecoder().decode(aadBase64))
: ext.encrypt(plain);
return Base64.getEncoder().encodeToString(cipher);
}
public String testDecrypt(String cryptoName, Map<String, String> runtimeContext,
String cipherTextBase64, String aadBase64) throws Exception {
byte[] cipherBytes = Base64.getDecoder().decode(cipherTextBase64);
CryptoModuleExtension ext = CryptoModuleManager.getInstance()
.createExtension(cryptoName, runtimeContext(runtimeContext));
byte[] plain = aadBase64 != null
? ext.decrypt(cipherBytes, Base64.getDecoder().decode(aadBase64))
: ext.decrypt(cipherBytes);
return Base64.getEncoder().encodeToString(plain);
}
private Map<String, String> runtimeContext(Map<String, String> runtimeContext) {
return runtimeContext != null ? runtimeContext : Collections.emptyMap();
}
}
@@ -0,0 +1,17 @@
package com.eactive.eai.manage.crypto;
import java.util.Map;
import lombok.Data;
/**
* 암복호화 테스트 요청 DTO. 평문/암문은 Base64로 주고받는다.
*/
@Data
public class CryptoTestRequestDTO {
String plainTextBase64;
String cipherTextBase64;
String aadBase64;
Map<String, String> runtimeContext;
}
@@ -0,0 +1,104 @@
package com.eactive.eai.manage.session;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.eactive.eai.common.session.CacheInfoVO;
/**
* SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 상태 모니터링 API.
*
* GET /manage/session/cache/status → 전체 캐시 요약 (구현체별 XML 텍스트, Ignite/Ehcache)
* GET /manage/session/cache/all → 개별 캐시(login/http-login/evict-master/terminal/convert-user-id/socket) 목록
* GET /manage/session/cache/login → 로그인(WebSocket) 캐시
* GET /manage/session/cache/http-login → HTTP 로그인 캐시
* GET /manage/session/cache/evict-master → 싱글 어댑터 evict master 캐시
* GET /manage/session/cache/terminal → 단말(ATM 등) 캐시
* GET /manage/session/cache/convert-user-id → 단말ID → 사용자ID 변환 캐시
* GET /manage/session/cache/socket → 소켓 세션 캐시
* GET /manage/session/cache/outbound-access-token → 아웃바운드 AccessTokenVO 캐시 (Ignite 백엔드에서만 지원)
* GET /manage/session/master-inst-id → 현재 evict master 인스턴스명
*/
@RestController
@RequestMapping("/manage/session")
public class SessionCacheManageController {
@Autowired
private SessionCacheManageService sessionCacheManageService;
@GetMapping("/cache/status")
public ResponseEntity<?> getCacheStatus() {
return ok(sessionCacheManageService.getCacheStatus());
}
@GetMapping("/master-inst-id")
public ResponseEntity<?> getMasterInstId() {
return ok(sessionCacheManageService.getMasterInstId());
}
@GetMapping("/cache/all")
public ResponseEntity<?> getAllCaches() {
List<CacheInfoVO> list = sessionCacheManageService.getAllCaches();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("data", list);
result.put("count", list.size());
return ResponseEntity.ok(result);
}
@GetMapping("/cache/login")
public ResponseEntity<?> getLoginCache() {
return ok(sessionCacheManageService.getLoginCache());
}
@GetMapping("/cache/http-login")
public ResponseEntity<?> getHttpLoginCache() {
return ok(sessionCacheManageService.getHttpLoginCache());
}
@GetMapping("/cache/evict-master")
public ResponseEntity<?> getEvictMasterCache() {
return ok(sessionCacheManageService.getEvictMasterCache());
}
@GetMapping("/cache/terminal")
public ResponseEntity<?> getTerminalCache() {
return ok(sessionCacheManageService.getTerminalCache());
}
@GetMapping("/cache/convert-user-id")
public ResponseEntity<?> getConvertUserIdCache() {
return ok(sessionCacheManageService.getConvertUserIdCache());
}
@GetMapping("/cache/socket")
public ResponseEntity<?> getSocketCache() {
return ok(sessionCacheManageService.getSocketCache());
}
@GetMapping("/cache/outbound-access-token")
public ResponseEntity<?> getOutboundAccessTokenCache() {
try {
return ok(sessionCacheManageService.getOutboundAccessTokenCache());
} catch (UnsupportedOperationException e) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("message", "현재 SessionManager 백엔드는 OutboundAccessToken 캐시를 지원하지 않습니다.");
return ResponseEntity.ok(result);
}
}
private ResponseEntity<?> ok(Object data) {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("data", data);
return ResponseEntity.ok(result);
}
}
@@ -0,0 +1,71 @@
package com.eactive.eai.manage.session;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.eactive.eai.common.session.CacheInfoVO;
import com.eactive.eai.common.session.SessionManager;
/**
* SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회를 위임하는 서비스.
*
* SessionManager.getInstance() 는 설정(session_manager.cacheType)에 따라 Ignite/Ehcache
* 구현체 중 하나의 싱글턴을 반환하며, 두 구현체 모두 아래 조회 메서드를 지원한다.
*/
@Service
public class SessionCacheManageService {
public String getCacheStatus() {
return SessionManager.getInstance().getCacheStatus();
}
public String getMasterInstId() {
return SessionManager.getInstance().getMasterInstId();
}
public CacheInfoVO getLoginCache() {
return SessionManager.getInstance().getLoginCache();
}
public CacheInfoVO getHttpLoginCache() {
return SessionManager.getInstance().getHttpLoginCache();
}
public CacheInfoVO getEvictMasterCache() {
return SessionManager.getInstance().getEvictMasterCache();
}
public CacheInfoVO getTerminalCache() {
return SessionManager.getInstance().getTerminalCache();
}
public CacheInfoVO getConvertUserIdCache() {
return SessionManager.getInstance().getConvertUserIdCache();
}
public CacheInfoVO getSocketCache() {
return SessionManager.getInstance().getSocketCache();
}
public CacheInfoVO getOutboundAccessTokenCache() {
return SessionManager.getInstance().getOutboundAccessTokenCache();
}
public List<CacheInfoVO> getAllCaches() {
List<CacheInfoVO> result = new ArrayList<>();
result.add(getLoginCache());
result.add(getHttpLoginCache());
result.add(getEvictMasterCache());
result.add(getTerminalCache());
result.add(getConvertUserIdCache());
result.add(getSocketCache());
try {
result.add(getOutboundAccessTokenCache());
} catch (UnsupportedOperationException e) {
// Ehcache 백엔드는 OutboundAccessToken 캐시를 지원하지 않는다.
}
return result;
}
}
@@ -98,6 +98,10 @@ public class RESTProcess extends HTTPProcess {
this.timeout = iTimeoutValue * 1000;
this.tempProp.put(INTERFACE_TIME_OUT, "" + this.timeout);
this.tempProp.put("API_SERVICE_CODE", this.reqEaiMsg.getEAISvcCd());
this.tempProp.put("OUT_REQ_EAI_MSG", this.reqEaiMsg);
this.tempProp.put("OUT_REQ_STD_MSG", this.reqEaiMsg.getStandardMessage());
try {
// API별 이용
String apiId = reqEaiMsg.getEAISvcCd();
@@ -56,6 +56,9 @@ public class OAuth2AccessTokenVO implements AccessTokenVO, Serializable
}
public void setTokenType(String tokenType) {
if(tokenType.equalsIgnoreCase(BEARER_TYPE))
this.tokenType = BEARER_TYPE;
else
this.tokenType = tokenType;
}
@@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -295,6 +296,97 @@ class CryptoModuleManagerTest {
"비활성화된 모듈은 configMap에서 제거되어야 한다");
}
// =========================================================================
// 5. 진단정보 — describe / describeAll / listDynamicCacheKeys
// =========================================================================
@Test
@DisplayName("5-1. describe() STATIC — keyDerivStrategy 없음, 원본 키 필드 없음")
void testDescribe_static_noStrategyInfo() throws Exception {
CryptoModuleConfig config = buildStaticConfig("DESC_STATIC", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
CryptoModuleDiagnosticDTO dto = manager.describe("DESC_STATIC");
assertEquals("DESC_STATIC", dto.getCryptoName());
assertEquals("STATIC", dto.getKeySourceType());
assertTrue(dto.isHasIv());
assertNull(dto.getKeyDerivStrategy(), "STATIC 모듈은 keyDerivStrategy가 없어야 한다");
assertFalse(dto.isStrategyLoaded(), "STATIC 모듈은 전략을 로드하지 않는다");
assertFalse(dto.toString().contains(ENC_KEY_HEX), "원본 키(hex)가 진단정보에 노출되면 안 된다");
}
@Test
@DisplayName("5-2. describe() DYNAMIC 정상 FQCN — strategyLoaded=true, strategyClassName 존재")
void testDescribe_dynamic_validFqcn_strategyLoaded() throws Exception {
CryptoModuleConfig config = buildDynamicConfig("DESC_DYN", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
CryptoModuleDiagnosticDTO dto = manager.describe("DESC_DYN");
assertEquals("DYNAMIC", dto.getKeySourceType());
assertEquals("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy",
dto.getKeyDerivStrategy());
assertTrue(dto.isStrategyLoaded());
assertEquals("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy",
dto.getStrategyClassName());
assertNull(dto.getStrategyLoadError());
}
@Test
@DisplayName("5-3. describe() DYNAMIC 잘못된 FQCN — strategyLoaded=false, strategyLoadError 존재")
void testDescribe_dynamic_invalidFqcn_strategyLoadFailed() throws Exception {
CryptoModuleConfig config = buildDynamicConfig("DESC_BAD_FQCN", "AES", "CBC", "PKCS5Padding");
config.setKeyDerivStrategy("com.example.NonExistentStrategy");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
CryptoModuleDiagnosticDTO dto = manager.describe("DESC_BAD_FQCN");
assertFalse(dto.isStrategyLoaded());
assertNotNull(dto.getStrategyLoadError());
}
@Test
@DisplayName("5-4. describe() 미등록 cryptoName — IllegalArgumentException")
void testDescribe_unknownName_throwsException() {
assertThrows(IllegalArgumentException.class, () -> manager.describe("UNKNOWN_MODULE"));
}
@Test
@DisplayName("5-5. describeAll() — 등록된 모듈 수만큼 반환")
void testDescribeAll_returnsAllRegisteredModules() throws Exception {
CryptoModuleConfig c1 = buildStaticConfig("DESC_ALL_1", "AES", "CBC", "PKCS5Padding");
CryptoModuleConfig c2 = buildDynamicConfig("DESC_ALL_2", "ARIA", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(c1, c2));
reload();
List<CryptoModuleDiagnosticDTO> all = manager.describeAll();
assertEquals(2, all.size());
assertTrue(all.stream().anyMatch(d -> "DESC_ALL_1".equals(d.getCryptoName())));
assertTrue(all.stream().anyMatch(d -> "DESC_ALL_2".equals(d.getCryptoName())));
}
@Test
@DisplayName("5-6. listDynamicCacheKeys() — DYNAMIC 키 생성 후 캐시키 노출 (원본 키 아님)")
void testListDynamicCacheKeys_afterCreateExtension_containsCacheKey() throws Exception {
CryptoModuleConfig config = buildDynamicConfig("DESC_CACHE_KEYS", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
manager.createExtension("DESC_CACHE_KEYS", runtimeCtx);
List<String> cacheKeys = manager.listDynamicCacheKeys();
assertEquals(1, cacheKeys.size());
assertEquals("DESC_CACHE_KEYS:clientKeyValue01", cacheKeys.get(0));
}
// =========================================================================
// 헬퍼
// =========================================================================
@@ -0,0 +1,189 @@
package com.eactive.eai.manage.crypto;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
/**
* CryptoModuleManageController 단위 테스트.
*
* <p>Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여
* 응답 구조(success, data, count, message)를 검증한다.
*/
class CryptoModuleManageControllerTest {
private CryptoModuleManageController controller;
private CryptoModuleManageService mockService;
@BeforeEach
void setUp() {
controller = new CryptoModuleManageController();
mockService = mock(CryptoModuleManageService.class);
ReflectionTestUtils.setField(controller, "cryptoModuleManageService", mockService);
}
// -------------------------------------------------------------------------
// Helper
// -------------------------------------------------------------------------
private CryptoModuleDiagnosticDTO makeDTO(String name, String keySourceType) {
CryptoModuleDiagnosticDTO dto = new CryptoModuleDiagnosticDTO();
dto.setCryptoName(name);
dto.setKeySourceType(keySourceType);
dto.setAlgType("AES");
dto.setCipherMode("CBC");
return dto;
}
@SuppressWarnings("unchecked")
private Map<String, Object> body(ResponseEntity<?> response) {
return (Map<String, Object>) response.getBody();
}
// =========================================================================
// 목록 조회 — listAll
// =========================================================================
@Test
@DisplayName("listAll() — 등록된 모듈 목록과 개수 반환")
void listAll_returnsListAndCount() {
List<CryptoModuleDiagnosticDTO> list = Arrays.asList(
makeDTO("MOD_A", "STATIC"),
makeDTO("MOD_B", "DYNAMIC")
);
when(mockService.listAll()).thenReturn(list);
Map<String, Object> body = body(controller.listAll());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(list, body.get("data"));
assertEquals(2, body.get("count"));
}
@Test
@DisplayName("listAll() — 빈 목록이면 count=0")
void listAll_emptyList_countZero() {
when(mockService.listAll()).thenReturn(Collections.emptyList());
Map<String, Object> body = body(controller.listAll());
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals(0, body.get("count"));
}
// =========================================================================
// 단일 조회 — get
// =========================================================================
@Test
@DisplayName("get() — 정상 조회 시 success=true, data 포함")
void get_found_successTrueAndDataPresent() {
CryptoModuleDiagnosticDTO dto = makeDTO("MOD_A", "STATIC");
when(mockService.get("MOD_A")).thenReturn(dto);
ResponseEntity<?> response = controller.get("MOD_A");
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(dto, body.get("data"));
assertFalse(body.containsKey("message"));
}
@Test
@DisplayName("get() — 미등록 cryptoName 예외 발생 시 success=false, message에 원인 포함")
void get_unknownName_successFalseWithMessage() {
when(mockService.get("GHOST")).thenThrow(new IllegalArgumentException("암호화모듈 미등록: GHOST"));
Map<String, Object> body = body(controller.get("GHOST"));
assertEquals(Boolean.FALSE, body.get("success"));
assertTrue(body.get("message").toString().contains("GHOST"));
assertFalse(body.containsKey("data"));
}
// =========================================================================
// 동적 키 캐시 목록 — listDynamicCacheKeys
// =========================================================================
@Test
@DisplayName("listDynamicCacheKeys() — 캐시키 목록과 개수 반환")
void listDynamicCacheKeys_returnsListAndCount() {
List<String> keys = Arrays.asList("MOD_A:ctx1", "MOD_A:ctx2");
when(mockService.listDynamicCacheKeys()).thenReturn(keys);
Map<String, Object> body = body(controller.listDynamicCacheKeys());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(keys, body.get("data"));
assertEquals(2, body.get("count"));
}
// =========================================================================
// 암복호화 테스트 — testEncrypt / testDecrypt
// =========================================================================
@Test
@DisplayName("testEncrypt() — 정상 암호화 시 success=true, cipherTextBase64 포함")
void testEncrypt_success_returnsCipherTextBase64() throws Exception {
CryptoTestRequestDTO request = new CryptoTestRequestDTO();
request.setPlainTextBase64("cGxhaW4=");
when(mockService.testEncrypt(eq("MOD_A"), isNull(), eq("cGxhaW4="), isNull()))
.thenReturn("Y2lwaGVy");
Map<String, Object> body = body(controller.testEncrypt("MOD_A", request));
assertEquals(Boolean.TRUE, body.get("success"));
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) body.get("data");
assertEquals("Y2lwaGVy", data.get("cipherTextBase64"));
}
@Test
@DisplayName("testEncrypt() — 서비스 예외 발생 시 success=false, message 포함")
void testEncrypt_serviceThrows_successFalseWithMessage() throws Exception {
CryptoTestRequestDTO request = new CryptoTestRequestDTO();
request.setPlainTextBase64("not-valid-base64!!");
when(mockService.testEncrypt(anyString(), any(), anyString(), any()))
.thenThrow(new IllegalArgumentException("Illegal base64 character"));
Map<String, Object> body = body(controller.testEncrypt("MOD_A", request));
assertEquals(Boolean.FALSE, body.get("success"));
assertNotNull(body.get("message"));
}
@Test
@DisplayName("testDecrypt() — runtimeContext 전달 시 서비스에 그대로 위임")
void testDecrypt_withRuntimeContext_delegatesToService() throws Exception {
Map<String, String> runtimeContext = new HashMap<>();
runtimeContext.put("X-Api-Enc-Key", "clientKeyValue01");
CryptoTestRequestDTO request = new CryptoTestRequestDTO();
request.setCipherTextBase64("Y2lwaGVy");
request.setRuntimeContext(runtimeContext);
when(mockService.testDecrypt("MOD_B", runtimeContext, "Y2lwaGVy", null))
.thenReturn("cGxhaW4=");
Map<String, Object> body = body(controller.testDecrypt("MOD_B", request));
assertEquals(Boolean.TRUE, body.get("success"));
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) body.get("data");
assertEquals("cGxhaW4=", data.get("plainTextBase64"));
verify(mockService).testDecrypt("MOD_B", runtimeContext, "Y2lwaGVy", null);
}
}
@@ -0,0 +1,291 @@
package com.eactive.eai.manage.crypto;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.CryptoModuleConfigVO;
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
import com.eactive.eai.common.security.CryptoModuleManager;
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
/**
* CryptoModuleManageService 단위 테스트.
*
* <p>DB / 실 Spring 컨텍스트 없이 Mock으로 동작한다. CryptoModuleManager는 실제 인스턴스를
* 사용하며(싱글턴 getInstance() 경유), HSM 및 DB 로더만 Mock 처리한다.
*/
class CryptoModuleManageServiceTest {
private static final byte[] KEY_128 = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
private static final byte[] IV_16 = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
private static final String ENC_KEY_HEX = toHex(KEY_128);
private static final String IV_HEX = toHex(IV_16);
private static GenericApplicationContext ctx;
private static CryptoModuleConfigLoader mockLoader;
private static HsmCryptoService mockHsmCryptoService;
private static CryptoModuleManager manager;
private final CryptoModuleManageService service = new CryptoModuleManageService();
@BeforeAll
static void setUpClass() throws Exception {
mockLoader = mock(CryptoModuleConfigLoader.class);
mockHsmCryptoService = mock(HsmCryptoService.class);
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
when(mockLoader.findAll()).thenReturn(Collections.emptyList());
Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
ctor.setAccessible(true);
manager = ctor.newInstance();
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
ctx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
manager.start();
}
@AfterAll
static void tearDownClass() {
if (ctx != null) ctx.close();
}
@BeforeEach
void resetMocks() throws Exception {
reset(mockLoader);
when(mockLoader.findAll()).thenReturn(Collections.emptyList());
reload();
}
// =========================================================================
// 1. 진단정보 위임 — listAll / get
// =========================================================================
@Test
@DisplayName("1-1. listAll() — CryptoModuleManager.describeAll() 위임")
void testListAll_delegatesToManager() throws Exception {
CryptoModuleConfig config = buildStaticConfig("SVC_LIST", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
List<CryptoModuleDiagnosticDTO> result = service.listAll();
assertEquals(1, result.size());
assertEquals("SVC_LIST", result.get(0).getCryptoName());
}
@Test
@DisplayName("1-2. get() — 미등록 cryptoName은 IllegalArgumentException 전파")
void testGet_unknownName_throwsException() {
assertThrows(IllegalArgumentException.class, () -> service.get("UNKNOWN_MODULE"));
}
// =========================================================================
// 2. 동적 키 캐시 목록 — listDynamicCacheKeys
// =========================================================================
@Test
@DisplayName("2-1. listDynamicCacheKeys() — DYNAMIC 암호화 테스트 후 캐시키 노출")
void testListDynamicCacheKeys_afterTestEncrypt_containsCacheKey() throws Exception {
CryptoModuleConfig config = buildDynamicConfig("SVC_CACHE_KEYS", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
String plainB64 = Base64.getEncoder().encodeToString("hello".getBytes(StandardCharsets.UTF_8));
service.testEncrypt("SVC_CACHE_KEYS", runtimeCtx, plainB64, null);
assertEquals(Collections.singletonList("SVC_CACHE_KEYS:clientKeyValue01"), service.listDynamicCacheKeys());
}
// =========================================================================
// 3. 암복호화 테스트 — testEncrypt / testDecrypt
// =========================================================================
@Test
@DisplayName("3-1. STATIC — 암호화 후 복호화 라운드트립 (Base64)")
void testEncryptDecrypt_static_roundTrip() throws Exception {
CryptoModuleConfig config = buildStaticConfig("SVC_STATIC_RT", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
String plainText = "테스트 평문";
String plainB64 = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
String cipherB64 = service.testEncrypt("SVC_STATIC_RT", null, plainB64, null);
assertNotNull(cipherB64);
assertNotEquals(plainB64, cipherB64);
String decryptedB64 = service.testDecrypt("SVC_STATIC_RT", null, cipherB64, null);
assertEquals(plainText, new String(Base64.getDecoder().decode(decryptedB64), StandardCharsets.UTF_8));
}
@Test
@DisplayName("3-2. DYNAMIC — runtimeContext 전달 시 암복호화 라운드트립 (Base64)")
void testEncryptDecrypt_dynamic_roundTrip() throws Exception {
CryptoModuleConfig config = buildDynamicConfig("SVC_DYNAMIC_RT", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
Map<String, String> runtimeCtx = new HashMap<>();
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
String plainText = "DYNAMIC 키 테스트";
String plainB64 = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
String cipherB64 = service.testEncrypt("SVC_DYNAMIC_RT", runtimeCtx, plainB64, null);
String decryptedB64 = service.testDecrypt("SVC_DYNAMIC_RT", runtimeCtx, cipherB64, null);
assertEquals(plainText, new String(Base64.getDecoder().decode(decryptedB64), StandardCharsets.UTF_8));
}
@Test
@DisplayName("3-3. testDecrypt() — 잘못된 Base64 입력은 예외 전파")
void testDecrypt_invalidBase64_throwsException() throws Exception {
CryptoModuleConfig config = buildStaticConfig("SVC_BAD_B64", "AES", "CBC", "PKCS5Padding");
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
reload();
assertThrows(IllegalArgumentException.class,
() -> service.testDecrypt("SVC_BAD_B64", null, "not-valid-base64!!", null));
}
@Test
@DisplayName("3-4. testEncrypt() — 미등록 cryptoName은 예외 전파")
void testEncrypt_unknownName_throwsException() {
String plainB64 = Base64.getEncoder().encodeToString("x".getBytes(StandardCharsets.UTF_8));
assertThrows(IllegalArgumentException.class,
() -> service.testEncrypt("UNKNOWN_MODULE", null, plainB64, null));
}
// =========================================================================
// 헬퍼
// =========================================================================
private void reload() throws Exception {
clearField("configMap");
clearField("dynamicKeyCache");
clearField("strategyCache");
Method loadAll = CryptoModuleManager.class.getDeclaredMethod("loadAll");
loadAll.setAccessible(true);
loadAll.invoke(manager);
reset(mockHsmCryptoService);
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
}
private void clearField(String fieldName) throws Exception {
Field field = CryptoModuleManager.class.getDeclaredField(fieldName);
field.setAccessible(true);
((Map<?, ?>) field.get(manager)).clear();
}
private CryptoModuleConfig buildStaticConfig(String name, String alg, String mode, String padding) {
CryptoModuleConfig c = new CryptoModuleConfig();
c.setCryptoId(java.util.UUID.randomUUID().toString());
c.setCryptoName(name);
c.setAlgType(alg);
c.setCipherMode(mode);
c.setPadding(padding);
c.setIvHex(IV_HEX);
c.setKeySourceType("STATIC");
c.setEncKeyHex(ENC_KEY_HEX);
c.setDecKeyHex(ENC_KEY_HEX);
c.setCacheYn("Y");
c.setCacheTtlSec(300);
c.setUseYn("Y");
return c;
}
private CryptoModuleConfig buildDynamicConfig(String name, String alg, String mode, String padding) {
CryptoModuleConfig c = new CryptoModuleConfig();
c.setCryptoId(java.util.UUID.randomUUID().toString());
c.setCryptoName(name);
c.setAlgType(alg);
c.setCipherMode(mode);
c.setPadding(padding);
c.setIvHex(IV_HEX);
c.setKeySourceType("DYNAMIC");
c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy");
c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\",\"contextKey\":\"X-Api-Enc-Key\",\"offset\":\"0\",\"length\":\"16\"}");
c.setCacheYn("Y");
c.setCacheTtlSec(300);
c.setUseYn("Y");
return c;
}
private static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
/** MapStruct APT 없이도 동작하는 인라인 매퍼 구현체 */
private static CryptoModuleConfigMapper testMapper() {
return new CryptoModuleConfigMapper() {
@Override
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
vo.setCryptoId(e.getCryptoId());
vo.setCryptoName(e.getCryptoName());
vo.setCryptoDesc(e.getCryptoDesc());
vo.setAlgType(e.getAlgType());
vo.setCipherMode(e.getCipherMode());
vo.setPadding(e.getPadding());
vo.setIvHex(e.getIvHex());
vo.setKeySourceType(e.getKeySourceType());
vo.setEncKeyHex(e.getEncKeyHex());
vo.setDecKeyHex(e.getDecKeyHex());
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
vo.setKeyDerivParams(e.getKeyDerivParams());
vo.setCacheYn(e.getCacheYn());
vo.setCacheTtlSec(e.getCacheTtlSec());
vo.setUseYn(e.getUseYn());
return vo;
}
@Override
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
return null;
}
};
}
}
@@ -0,0 +1,200 @@
package com.eactive.eai.manage.session;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.common.session.CacheInfoVO;
/**
* SessionCacheManageController 단위 테스트.
*
* <p>Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여
* 응답 구조(success, data, count, message)를 검증한다.
*/
class SessionCacheManageControllerTest {
private SessionCacheManageController controller;
private SessionCacheManageService mockService;
@BeforeEach
void setUp() {
controller = new SessionCacheManageController();
mockService = mock(SessionCacheManageService.class);
ReflectionTestUtils.setField(controller, "sessionCacheManageService", mockService);
}
// -------------------------------------------------------------------------
// Helper
// -------------------------------------------------------------------------
private CacheInfoVO makeCacheInfo(String name) {
CacheInfoVO vo = new CacheInfoVO();
vo.setName(name);
vo.setSize(3);
vo.setKeys(Arrays.asList("a", "b", "c"));
vo.setTimeToLive(60000);
return vo;
}
@SuppressWarnings("unchecked")
private Map<String, Object> body(ResponseEntity<?> response) {
return (Map<String, Object>) response.getBody();
}
// =========================================================================
// 캐시 요약 / 마스터 인스턴스
// =========================================================================
@Test
@DisplayName("getCacheStatus() — success=true, data에 원문 그대로 반환")
void getCacheStatus_returnsRawStatusString() {
when(mockService.getCacheStatus()).thenReturn("<Caches type=\"Ignite\"></Caches>");
Map<String, Object> body = body(controller.getCacheStatus());
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("<Caches type=\"Ignite\"></Caches>", body.get("data"));
}
@Test
@DisplayName("getMasterInstId() — success=true, data에 인스턴스명 반환")
void getMasterInstId_returnsInstanceName() {
when(mockService.getMasterInstId()).thenReturn("SERVER01");
Map<String, Object> body = body(controller.getMasterInstId());
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("SERVER01", body.get("data"));
}
// =========================================================================
// 개별 캐시 조회
// =========================================================================
@Test
@DisplayName("getLoginCache() — success=true, CacheInfoVO 반환")
void getLoginCache_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("LoginCache");
when(mockService.getLoginCache()).thenReturn(vo);
ResponseEntity<?> response = controller.getLoginCache();
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(vo, body.get("data"));
}
@Test
@DisplayName("getHttpLoginCache() — success=true")
void getHttpLoginCache_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("HttpLoginCache");
when(mockService.getHttpLoginCache()).thenReturn(vo);
assertSame(vo, body(controller.getHttpLoginCache()).get("data"));
}
@Test
@DisplayName("getEvictMasterCache() — success=true")
void getEvictMasterCache_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("EvictMasterCache");
when(mockService.getEvictMasterCache()).thenReturn(vo);
assertSame(vo, body(controller.getEvictMasterCache()).get("data"));
}
@Test
@DisplayName("getTerminalCache() — success=true")
void getTerminalCache_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("TerminalCache");
when(mockService.getTerminalCache()).thenReturn(vo);
assertSame(vo, body(controller.getTerminalCache()).get("data"));
}
@Test
@DisplayName("getConvertUserIdCache() — success=true")
void getConvertUserIdCache_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("ConvertUserIdCache");
when(mockService.getConvertUserIdCache()).thenReturn(vo);
assertSame(vo, body(controller.getConvertUserIdCache()).get("data"));
}
@Test
@DisplayName("getSocketCache() — success=true")
void getSocketCache_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("SocketCache");
when(mockService.getSocketCache()).thenReturn(vo);
assertSame(vo, body(controller.getSocketCache()).get("data"));
}
// =========================================================================
// 전체 캐시 목록
// =========================================================================
@Test
@DisplayName("getAllCaches() — 목록과 개수 반환")
void getAllCaches_returnsListAndCount() {
List<CacheInfoVO> list = Arrays.asList(makeCacheInfo("A"), makeCacheInfo("B"));
when(mockService.getAllCaches()).thenReturn(list);
Map<String, Object> body = body(controller.getAllCaches());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(list, body.get("data"));
assertEquals(2, body.get("count"));
}
@Test
@DisplayName("getAllCaches() — 빈 목록이면 count=0")
void getAllCaches_emptyList_countZero() {
when(mockService.getAllCaches()).thenReturn(Arrays.asList());
Map<String, Object> body = body(controller.getAllCaches());
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals(0, body.get("count"));
}
// =========================================================================
// OutboundAccessToken 캐시 — 지원/미지원 백엔드
// =========================================================================
@Test
@DisplayName("getOutboundAccessTokenCache() — 지원 시 success=true, CacheInfoVO 반환")
void getOutboundAccessTokenCache_supported_returnsCacheInfo() {
CacheInfoVO vo = makeCacheInfo("OutBoundAccessToken");
when(mockService.getOutboundAccessTokenCache()).thenReturn(vo);
Map<String, Object> body = body(controller.getOutboundAccessTokenCache());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(vo, body.get("data"));
}
@Test
@DisplayName("getOutboundAccessTokenCache() — Ehcache 등 미지원 백엔드는 500이 아니라 success=false로 응답")
void getOutboundAccessTokenCache_unsupported_successFalseNotServerError() {
when(mockService.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException());
ResponseEntity<?> response = controller.getOutboundAccessTokenCache();
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.FALSE, body.get("success"));
assertNotNull(body.get("message"));
assertFalse(body.containsKey("data"));
}
}
@@ -0,0 +1,185 @@
package com.eactive.eai.manage.session;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.eactive.eai.common.session.CacheInfoVO;
import com.eactive.eai.common.session.SessionManager;
/**
* SessionCacheManageService 단위 테스트.
*
* <p>SessionManager.getInstance()는 static singleton(private static instance 필드)이라
* 실제 Ignite/Ehcache를 기동하지 않고, 리플렉션으로 그 필드를 Mockito mock으로 교체하여 검증한다.
* 테스트가 끝나면 반드시 필드를 원복(null)하여 다른 테스트에 영향을 주지 않는다.
*/
class SessionCacheManageServiceTest {
private SessionManager mockSessionManager;
private final SessionCacheManageService service = new SessionCacheManageService();
@BeforeEach
void setUp() throws Exception {
mockSessionManager = mock(SessionManager.class);
setInstance(mockSessionManager);
}
@AfterEach
void tearDown() throws Exception {
setInstance(null);
}
private void setInstance(SessionManager manager) throws Exception {
Field field = SessionManager.class.getDeclaredField("instance");
field.setAccessible(true);
field.set(null, manager);
}
private CacheInfoVO makeCacheInfo(String name, int size) {
CacheInfoVO vo = new CacheInfoVO();
vo.setName(name);
vo.setSize(size);
vo.setKeys(Arrays.asList("k1", "k2"));
vo.setTimeToLive(60000);
return vo;
}
// =========================================================================
// 1. 단건 위임
// =========================================================================
@Test
@DisplayName("1-1. getCacheStatus() — SessionManager.getCacheStatus() 위임")
void testGetCacheStatus_delegates() {
when(mockSessionManager.getCacheStatus()).thenReturn("<Caches type=\"Ignite\"></Caches>");
assertEquals("<Caches type=\"Ignite\"></Caches>", service.getCacheStatus());
verify(mockSessionManager).getCacheStatus();
}
@Test
@DisplayName("1-2. getMasterInstId() — SessionManager.getMasterInstId() 위임")
void testGetMasterInstId_delegates() {
when(mockSessionManager.getMasterInstId()).thenReturn("SERVER01");
assertEquals("SERVER01", service.getMasterInstId());
}
@Test
@DisplayName("1-3. getLoginCache() — 위임")
void testGetLoginCache_delegates() {
CacheInfoVO vo = makeCacheInfo("LoginCache", 2);
when(mockSessionManager.getLoginCache()).thenReturn(vo);
assertSame(vo, service.getLoginCache());
}
@Test
@DisplayName("1-4. getHttpLoginCache() — 위임")
void testGetHttpLoginCache_delegates() {
CacheInfoVO vo = makeCacheInfo("HttpLoginCache", 1);
when(mockSessionManager.getHttpLoginCache()).thenReturn(vo);
assertSame(vo, service.getHttpLoginCache());
}
@Test
@DisplayName("1-5. getEvictMasterCache() — 위임")
void testGetEvictMasterCache_delegates() {
CacheInfoVO vo = makeCacheInfo("EvictMasterCache", 1);
when(mockSessionManager.getEvictMasterCache()).thenReturn(vo);
assertSame(vo, service.getEvictMasterCache());
}
@Test
@DisplayName("1-6. getTerminalCache() — 위임")
void testGetTerminalCache_delegates() {
CacheInfoVO vo = makeCacheInfo("TerminalCache", 5);
when(mockSessionManager.getTerminalCache()).thenReturn(vo);
assertSame(vo, service.getTerminalCache());
}
@Test
@DisplayName("1-7. getConvertUserIdCache() — 위임")
void testGetConvertUserIdCache_delegates() {
CacheInfoVO vo = makeCacheInfo("ConvertUserIdCache", 3);
when(mockSessionManager.getConvertUserIdCache()).thenReturn(vo);
assertSame(vo, service.getConvertUserIdCache());
}
@Test
@DisplayName("1-8. getSocketCache() — 위임")
void testGetSocketCache_delegates() {
CacheInfoVO vo = makeCacheInfo("SocketCache", 4);
when(mockSessionManager.getSocketCache()).thenReturn(vo);
assertSame(vo, service.getSocketCache());
}
@Test
@DisplayName("1-9. getOutboundAccessTokenCache() — 위임")
void testGetOutboundAccessTokenCache_delegates() {
CacheInfoVO vo = makeCacheInfo("OutBoundAccessToken", 7);
when(mockSessionManager.getOutboundAccessTokenCache()).thenReturn(vo);
assertSame(vo, service.getOutboundAccessTokenCache());
}
@Test
@DisplayName("1-10. getOutboundAccessTokenCache() — 미지원 백엔드는 예외 그대로 전파")
void testGetOutboundAccessTokenCache_unsupported_throwsException() {
when(mockSessionManager.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException());
assertThrows(UnsupportedOperationException.class, service::getOutboundAccessTokenCache);
}
// =========================================================================
// 2. getAllCaches() 집계
// =========================================================================
@Test
@DisplayName("2-1. getAllCaches() — OutboundAccessToken 포함 7개 캐시 반환")
void testGetAllCaches_outboundSupported_returnsSevenCaches() {
when(mockSessionManager.getLoginCache()).thenReturn(makeCacheInfo("LoginCache", 1));
when(mockSessionManager.getHttpLoginCache()).thenReturn(makeCacheInfo("HttpLoginCache", 1));
when(mockSessionManager.getEvictMasterCache()).thenReturn(makeCacheInfo("EvictMasterCache", 1));
when(mockSessionManager.getTerminalCache()).thenReturn(makeCacheInfo("TerminalCache", 1));
when(mockSessionManager.getConvertUserIdCache()).thenReturn(makeCacheInfo("ConvertUserIdCache", 1));
when(mockSessionManager.getSocketCache()).thenReturn(makeCacheInfo("SocketCache", 1));
when(mockSessionManager.getOutboundAccessTokenCache()).thenReturn(makeCacheInfo("OutBoundAccessToken", 1));
List<CacheInfoVO> all = service.getAllCaches();
assertEquals(7, all.size());
assertTrue(all.stream().anyMatch(c -> "OutBoundAccessToken".equals(c.getName())));
}
@Test
@DisplayName("2-2. getAllCaches() — OutboundAccessToken 미지원(Ehcache)이면 예외 없이 6개만 반환")
void testGetAllCaches_outboundUnsupported_excludedGracefully() {
when(mockSessionManager.getLoginCache()).thenReturn(makeCacheInfo("LoginCache", 1));
when(mockSessionManager.getHttpLoginCache()).thenReturn(makeCacheInfo("HttpLoginCache", 1));
when(mockSessionManager.getEvictMasterCache()).thenReturn(makeCacheInfo("EvictMasterCache", 1));
when(mockSessionManager.getTerminalCache()).thenReturn(makeCacheInfo("TerminalCache", 1));
when(mockSessionManager.getConvertUserIdCache()).thenReturn(makeCacheInfo("ConvertUserIdCache", 1));
when(mockSessionManager.getSocketCache()).thenReturn(makeCacheInfo("SocketCache", 1));
when(mockSessionManager.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException());
List<CacheInfoVO> all = assertDoesNotThrow(service::getAllCaches);
assertEquals(6, all.size());
assertTrue(all.stream().noneMatch(c -> "OutBoundAccessToken".equals(c.getName())));
}
}