Compare commits
4 Commits
11b439aaf1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c8f7bfe1b1 | |||
| 64bf89582a | |||
| ac5e617b37 | |||
| 1c48dd834a |
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import com.eactive.eai.adapter.http.client.impl.HttpClient5AdapterServiceRest;
|
||||||
|
import com.eactive.eai.adapter.http.filter.CryptoFilterPathConfig;
|
||||||
|
import com.eactive.eai.common.util.JacksonUtil;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 어댑터그룹별 암복호화 JSON 경로를 PropManager "CryptoFilter" 그룹에서 읽는 송신 암복호화 필터.
|
||||||
|
*
|
||||||
|
* 외부기관마다 경로만 다른 필터 클래스를 만드는 대신 이 필터 + 프로퍼티 설정으로 대체한다.
|
||||||
|
* 경로 키와 기본값은 {@link CryptoFilterPathConfig} 참조.
|
||||||
|
* OutCryptoFilter 는 경로를 tempProp 에서 읽으므로 경로는 tempProp 에 반영한다.
|
||||||
|
*
|
||||||
|
* doPreFilter : 송신 요청 암호화. HEADER_GROUP 으로 지정된 헤더 그룹은 암호화 대상에서 빼고 암호화 후 다시 붙인다.
|
||||||
|
* doPostFilter : 수신 응답 복호화
|
||||||
|
*
|
||||||
|
* 필터 타입 등록 (FQCN):
|
||||||
|
* com.eactive.eai.adapter.http.client.impl.filter.ConfigurableEncFieldOutCryptoFilter
|
||||||
|
*/
|
||||||
|
public class ConfigurableEncFieldOutCryptoFilter implements HttpClientAdapterFilter {
|
||||||
|
|
||||||
|
private final OutCryptoFilter filter = new OutCryptoFilter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||||
|
Properties tempProp) throws Exception {
|
||||||
|
String headerGroupName = prop.getProperty(HttpClient5AdapterServiceRest.HEADER_GROUP);
|
||||||
|
|
||||||
|
ObjectNode root = (ObjectNode) JacksonUtil.readTree(message);
|
||||||
|
JsonNode header = null;
|
||||||
|
if (headerGroupName != null) {
|
||||||
|
header = root.get(headerGroupName);
|
||||||
|
root.remove(headerGroupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object enc = filter.doPreFilter(adptGrpName, adptName, prop, root,
|
||||||
|
CryptoFilterPathConfig.apply(adptGrpName, tempProp));
|
||||||
|
ObjectNode encJson = (ObjectNode) JacksonUtil.readTree(enc);
|
||||||
|
if (header != null) {
|
||||||
|
encJson.set(headerGroupName, header);
|
||||||
|
}
|
||||||
|
return encJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||||
|
Properties tempProp) throws Exception {
|
||||||
|
return filter.doPostFilter(adptGrpName, adptName, prop, message,
|
||||||
|
CryptoFilterPathConfig.apply(adptGrpName, tempProp));
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import com.eactive.eai.adapter.http.dynamic.impl.HttpAdapterServiceRest;
|
||||||
|
import com.eactive.eai.adapter.http.filter.CryptoFilterPathConfig;
|
||||||
|
import com.eactive.eai.common.util.JacksonUtil;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 어댑터그룹별 암복호화 JSON 경로를 PropManager "CryptoFilter" 그룹에서 읽는 수신 암복호화 필터.
|
||||||
|
*
|
||||||
|
* 외부기관마다 경로만 다른 필터 클래스를 만드는 대신 이 필터 + 프로퍼티 설정으로 대체한다.
|
||||||
|
* 경로 키와 기본값은 {@link CryptoFilterPathConfig} 참조.
|
||||||
|
*
|
||||||
|
* doPreFilter : 수신 요청 복호화
|
||||||
|
* doPostFilter : 송신 응답 암호화. HEADER_GROUP 으로 지정된 헤더 그룹은 암호화 대상에서 빼고 암호화 후 다시 붙인다.
|
||||||
|
*
|
||||||
|
* 필터 타입 등록 (FQCN):
|
||||||
|
* com.eactive.eai.adapter.http.dynamic.filter.ConfigurableEncFieldInCryptoFilter
|
||||||
|
*/
|
||||||
|
public class ConfigurableEncFieldInCryptoFilter implements HttpAdapterFilter {
|
||||||
|
|
||||||
|
private final InCryptoFilter filter = new InCryptoFilter();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||||
|
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||||
|
return filter.doPreFilter(adptGrpName, adptName, message, CryptoFilterPathConfig.apply(adptGrpName, prop),
|
||||||
|
request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||||
|
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||||
|
try {
|
||||||
|
ObjectNode root = (ObjectNode) JacksonUtil.readTree(resultMessage);
|
||||||
|
String headerGroupName = prop.getProperty(HttpAdapterServiceRest.HEADER_GROUP);
|
||||||
|
JsonNode header = null;
|
||||||
|
if (headerGroupName != null) {
|
||||||
|
header = root.get(headerGroupName);
|
||||||
|
root.remove(headerGroupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object enc = filter.doPostFilter(adptGrpName, adptName, root,
|
||||||
|
CryptoFilterPathConfig.apply(adptGrpName, prop), request, response);
|
||||||
|
ObjectNode encJson = (ObjectNode) JacksonUtil.readTree(enc);
|
||||||
|
if (header != null) {
|
||||||
|
encJson.set(headerGroupName, header);
|
||||||
|
}
|
||||||
|
return encJson;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FilterCryptoException("암호화 오류", InCryptoFilter.ERROR_ENC_FAIL,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR.value(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.eactive.eai.adapter.http.filter;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.property.PropManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설정 기반 암복호화 필터의 JSON 경로를 어댑터그룹별로 PropManager 에서 읽어 반영한다.
|
||||||
|
*
|
||||||
|
* 프로퍼티 그룹 "CryptoFilter", 키는 "{어댑터그룹명}.{CRYPTO_*_PATH}".
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* {어댑터그룹명}.CRYPTO_DEC_FROM_PATH = /encryptedData (기본값) 복호화할 암호문 필드
|
||||||
|
* {어댑터그룹명}.CRYPTO_DEC_TO_PATH = / (기본값) 복호화 결과 반영 위치. "/" 면 암호문 필드를 지우고 body 에 병합
|
||||||
|
* {어댑터그룹명}.CRYPTO_ENC_FROM_PATH = / (기본값) 암호화 대상. "/" 면 body 전체
|
||||||
|
* {어댑터그룹명}.CRYPTO_ENC_TO_PATH = /encryptedData (기본값) 암호문을 넣을 필드
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* 필터 인스턴스는 팩토리에 캐시되어 모든 어댑터그룹이 공유하므로 설정은 호출마다 읽는다.
|
||||||
|
*
|
||||||
|
* @see com.eactive.eai.adapter.http.dynamic.filter.ConfigurableEncFieldInCryptoFilter
|
||||||
|
* @see com.eactive.eai.adapter.http.client.impl.filter.ConfigurableEncFieldOutCryptoFilter
|
||||||
|
*/
|
||||||
|
public final class CryptoFilterPathConfig {
|
||||||
|
|
||||||
|
public static final String PROP_GROUP = "CryptoFilter";
|
||||||
|
|
||||||
|
static final String DEFAULT_DEC_FROM_PATH = "/encryptedData";
|
||||||
|
static final String DEFAULT_DEC_TO_PATH = AbstractCryptoFilter.PATH_ROOT;
|
||||||
|
static final String DEFAULT_ENC_FROM_PATH = AbstractCryptoFilter.PATH_ROOT;
|
||||||
|
static final String DEFAULT_ENC_TO_PATH = "/encryptedData";
|
||||||
|
|
||||||
|
private static final String[] PATH_KEYS = { AbstractCryptoFilter.PROP_DEC_FROM_PATH,
|
||||||
|
AbstractCryptoFilter.PROP_DEC_TO_PATH, AbstractCryptoFilter.PROP_ENC_FROM_PATH,
|
||||||
|
AbstractCryptoFilter.PROP_ENC_TO_PATH };
|
||||||
|
|
||||||
|
private CryptoFilterPathConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* target 에 경로 프로퍼티 4개를 반영한다. (수신 필터는 prop, 송신 필터는 tempProp)
|
||||||
|
* 기본값을 먼저 넣고 CryptoFilter 그룹에 설정된 값으로 덮어쓴다. 값이 비어 있는 키는 기본값을 유지한다.
|
||||||
|
*
|
||||||
|
* @param adptGrpName 어댑터그룹명
|
||||||
|
* @param target 경로를 반영할 Properties
|
||||||
|
* @return target
|
||||||
|
*/
|
||||||
|
public static Properties apply(String adptGrpName, Properties target) {
|
||||||
|
target.setProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH, DEFAULT_DEC_FROM_PATH);
|
||||||
|
target.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, DEFAULT_DEC_TO_PATH);
|
||||||
|
target.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, DEFAULT_ENC_FROM_PATH);
|
||||||
|
target.setProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH, DEFAULT_ENC_TO_PATH);
|
||||||
|
|
||||||
|
// 그룹이 없을 때 getProperties() 는 예외를 던지므로 먼저 확인한다.
|
||||||
|
PropManager propManager = PropManager.getInstance();
|
||||||
|
if (StringUtils.isBlank(adptGrpName) || !propManager.isContainProperties(PROP_GROUP)) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String key : PATH_KEYS) {
|
||||||
|
String value = propManager.getProperty(PROP_GROUP, adptGrpName + "." + key, null);
|
||||||
|
if (StringUtils.isNotBlank(value)) {
|
||||||
|
target.setProperty(key, value.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -258,7 +258,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
if (cached == null) {
|
if (cached == null) {
|
||||||
logger.debug("Token not exists for adapter group: {}", adapterGroupName);
|
logger.debug("Token not exists for adapter group: {}", adapterGroupName);
|
||||||
sessionManager.getOutboundAccessToken(adapterGroupName,
|
sessionManager.getOutboundAccessToken(adapterGroupName,
|
||||||
token -> issueToken(credential));
|
token -> issueToken(credential, TokenIssueHistory.TRIGGER_SCHEDULE));
|
||||||
|
|
||||||
} else if (cached.getExpiration() == null) {
|
} else if (cached.getExpiration() == null) {
|
||||||
// 만료시각이 없는 토큰은 갱신 시점을 판단할 수 없다.
|
// 만료시각이 없는 토큰은 갱신 시점을 판단할 수 없다.
|
||||||
@@ -272,7 +272,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
logger.debug("Token expires before next schedule : {}, expiration date: {}",
|
logger.debug("Token expires before next schedule : {}, expiration date: {}",
|
||||||
adapterGroupName, cached.getExpiration());
|
adapterGroupName, cached.getExpiration());
|
||||||
sessionManager.reissueOutboundAccessToken(adapterGroupName, cached.getAccessToken(),
|
sessionManager.reissueOutboundAccessToken(adapterGroupName, cached.getAccessToken(),
|
||||||
intervalTime, token -> issueToken(credential));
|
intervalTime, token -> issueToken(credential, TokenIssueHistory.TRIGGER_SCHEDULE));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -300,7 +300,10 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo) {
|
/**
|
||||||
|
* @param trigger 발급 이력에 남길 호출 경로 (TokenIssueHistory.TRIGGER_*)
|
||||||
|
*/
|
||||||
|
private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo, String trigger) {
|
||||||
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
|
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
|
||||||
AccessTokenVO accessToken = null;
|
AccessTokenVO accessToken = null;
|
||||||
try {
|
try {
|
||||||
@@ -335,12 +338,12 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
|
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
|
||||||
outboundOAuthCredentialVo);
|
outboundOAuthCredentialVo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, type, startTime, null,
|
recordIssueHistory(adapterGroupName, trigger, type, startTime, null,
|
||||||
toFailReason(e));
|
toFailReason(e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, type, startTime,
|
recordIssueHistory(adapterGroupName, trigger, type, startTime,
|
||||||
accessToken, null);
|
accessToken, null);
|
||||||
|
|
||||||
// 발급에 실패했는데도 빈 토큰을 반환하는 구현체가 있다. 그대로 캐시되면
|
// 발급에 실패했는데도 빈 토큰을 반환하는 구현체가 있다. 그대로 캐시되면
|
||||||
@@ -354,12 +357,12 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
return accessToken;
|
return accessToken;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, type,
|
recordIssueHistory(adapterGroupName, trigger, type,
|
||||||
System.currentTimeMillis(), null, "토큰 발급 구현체를 찾을 수 없음");
|
System.currentTimeMillis(), null, "토큰 발급 구현체를 찾을 수 없음");
|
||||||
logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
|
logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, null,
|
recordIssueHistory(adapterGroupName, trigger, null,
|
||||||
System.currentTimeMillis(), null, "어댑터 설정을 찾을 수 없음");
|
System.currentTimeMillis(), null, "어댑터 설정을 찾을 수 없음");
|
||||||
logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName);
|
logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName);
|
||||||
}
|
}
|
||||||
@@ -429,7 +432,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
* 3. 주의사항 : accessToken 은 앞 UNMASKED_TOKEN_LENGTH 자만 남겨 보관한다.
|
* 3. 주의사항 : accessToken 은 앞 UNMASKED_TOKEN_LENGTH 자만 남겨 보관한다.
|
||||||
*
|
*
|
||||||
* @param adapterGroupName 어댑터그룹명
|
* @param adapterGroupName 어댑터그룹명
|
||||||
* @param trigger SCHEDULE / RETRY
|
* @param trigger SCHEDULE / RETRY / TRANSACTION
|
||||||
* @param serviceClass 사용한 발급 구현체 클래스명
|
* @param serviceClass 사용한 발급 구현체 클래스명
|
||||||
* @param startTime 발급 시도 시각
|
* @param startTime 발급 시도 시각
|
||||||
* @param accessToken 발급된 토큰. 실패 시 null.
|
* @param accessToken 발급된 토큰. 실패 시 null.
|
||||||
@@ -553,7 +556,8 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
|||||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos
|
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos
|
||||||
.get(adapterGroupName);
|
.get(adapterGroupName);
|
||||||
|
|
||||||
return issueToken(outboundOAuthCredentialVo);
|
// 스케줄러가 아니라 거래 스레드가 토큰이 없거나 만료된 것을 보고 발급하는 경로다.
|
||||||
|
return issueToken(outboundOAuthCredentialVo, TokenIssueHistory.TRIGGER_TRANSACTION);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,18 @@ public class TokenIssueHistory {
|
|||||||
/** 스케줄러의 주기 발급 */
|
/** 스케줄러의 주기 발급 */
|
||||||
public static final String TRIGGER_SCHEDULE = "SCHEDULE";
|
public static final String TRIGGER_SCHEDULE = "SCHEDULE";
|
||||||
|
|
||||||
/** 거래 중 재발급 */
|
/** 거래 중 재발급 (기관이 토큰을 거부한 경우) */
|
||||||
public static final String TRIGGER_RETRY = "RETRY";
|
public static final String TRIGGER_RETRY = "RETRY";
|
||||||
|
|
||||||
|
/** 거래 중 발급 (캐시에 토큰이 없거나 이미 만료된 경우) */
|
||||||
|
public static final String TRIGGER_TRANSACTION = "TRANSACTION";
|
||||||
|
|
||||||
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
/** 발급을 시도한 시각 */
|
/** 발급을 시도한 시각 */
|
||||||
private final String issuedAt;
|
private final String issuedAt;
|
||||||
|
|
||||||
/** SCHEDULE | RETRY */
|
/** SCHEDULE | RETRY | TRANSACTION */
|
||||||
private final String trigger;
|
private final String trigger;
|
||||||
|
|
||||||
/** 사용한 발급 구현체 클래스명 */
|
/** 사용한 발급 구현체 클래스명 */
|
||||||
|
|||||||
@@ -77,11 +77,29 @@ import ch.qos.logback.classic.Level;
|
|||||||
public class SessionManagerForIgnite extends SessionManager {
|
public class SessionManagerForIgnite extends SessionManager {
|
||||||
|
|
||||||
|
|
||||||
/** 아웃바운드 토큰 분산락 대기 시간(초). 스케줄러 경로에서 사용한다. */
|
/** 아웃바운드 토큰 분산락 대기 시간(ms) 설정 키 (RMIInfo 그룹) */
|
||||||
private static final int TOKEN_LOCK_WAIT_SECONDS = 60;
|
public static final String OUTBOUND_TOKEN_LOCK_WAIT_MILLIS = "outboundTokenLockWaitMillis";
|
||||||
|
/** 거래 중 재발급(기관 거부) 분산락 대기 시간(ms) 설정 키 (RMIInfo 그룹) */
|
||||||
|
public static final String OUTBOUND_TOKEN_REISSUE_LOCK_WAIT_MILLIS = "outboundTokenReissueLockWaitMillis";
|
||||||
|
/** 아웃바운드 토큰 primary 읽기 트랜잭션 타임아웃(ms) 설정 키 (RMIInfo 그룹) */
|
||||||
|
public static final String OUTBOUND_TOKEN_READ_TX_TIMEOUT_MILLIS = "outboundTokenReadTxTimeoutMillis";
|
||||||
|
|
||||||
/** 거래 중 재발급은 거래 스레드를 잡으므로 스케줄러 경로보다 짧게 기다린다. */
|
/**
|
||||||
private static final int REISSUE_LOCK_WAIT_SECONDS = 10;
|
* 아웃바운드 토큰 분산락 대기 시간(ms). RMIInfo.outboundTokenLockWaitMillis
|
||||||
|
* 토큰이 없거나 만료일 때 거래 스레드도 여기서 기다리므로 길게 잡지 않는다.
|
||||||
|
* (스케줄러는 락을 못 잡으면 다른 노드가 발급 중이라는 뜻이라 오래 기다릴 필요가 없다)
|
||||||
|
*/
|
||||||
|
private long tokenLockWaitMillis = 15000L;
|
||||||
|
|
||||||
|
/** 거래 중 재발급(기관 거부) 분산락 대기 시간(ms). RMIInfo.outboundTokenReissueLockWaitMillis */
|
||||||
|
private long reissueLockWaitMillis = 10000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* readFromPrimary 트랜잭션 타임아웃(ms). RMIInfo.outboundTokenReadTxTimeoutMillis
|
||||||
|
* 비관적 트랜잭션 읽기는 키 락을 잡으므로 다른 노드가 lock(key) 로 발급 중이면 발급이 끝날 때까지 기다린다.
|
||||||
|
* 타임아웃이 없으면 이 대기는 락 대기 시간(tryLock)의 제한을 받지 않는다.
|
||||||
|
*/
|
||||||
|
private long readTxTimeoutMillis = 3000L;
|
||||||
// evictMaster Instance - single socket 관련
|
// evictMaster Instance - single socket 관련
|
||||||
private static IgniteCache<String, String> evictMasterCache = null;
|
private static IgniteCache<String, String> evictMasterCache = null;
|
||||||
// Socket Session cache 정보
|
// Socket Session cache 정보
|
||||||
@@ -219,6 +237,15 @@ public class SessionManagerForIgnite extends SessionManager {
|
|||||||
cacheStoreMaxSize = 10000;
|
cacheStoreMaxSize = 10000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenLockWaitMillis = parseMillis(prpty, OUTBOUND_TOKEN_LOCK_WAIT_MILLIS, tokenLockWaitMillis, 1000L);
|
||||||
|
reissueLockWaitMillis = parseMillis(prpty, OUTBOUND_TOKEN_REISSUE_LOCK_WAIT_MILLIS, reissueLockWaitMillis, 1000L);
|
||||||
|
readTxTimeoutMillis = parseMillis(prpty, OUTBOUND_TOKEN_READ_TX_TIMEOUT_MILLIS, readTxTimeoutMillis, 100L);
|
||||||
|
|
||||||
|
if (logger.isWarn()) {
|
||||||
|
logger.warn(">> OutboundToken tokenLockWaitMillis = " + tokenLockWaitMillis + ", reissueLockWaitMillis = "
|
||||||
|
+ reissueLockWaitMillis + ", readTxTimeoutMillis = " + readTxTimeoutMillis);
|
||||||
|
}
|
||||||
|
|
||||||
IgniteConfiguration config = new IgniteConfiguration();
|
IgniteConfiguration config = new IgniteConfiguration();
|
||||||
|
|
||||||
@@ -449,7 +476,28 @@ public class SessionManagerForIgnite extends SessionManager {
|
|||||||
cacheWebSocketTimeout = manager.cache(WEBSOCKER_CAHCE_NAME);
|
cacheWebSocketTimeout = manager.cache(WEBSOCKER_CAHCE_NAME);
|
||||||
cacheTerminal = manager.cache(TERMINAL_CAHCE_NAME);
|
cacheTerminal = manager.cache(TERMINAL_CAHCE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 밀리초 설정값을 읽는다. 없거나 숫자가 아니거나 최소값보다 작으면 기본값을 쓴다.
|
||||||
|
*/
|
||||||
|
private long parseMillis(Properties prpty, String key, long defaultValue, long minValue) {
|
||||||
|
String value = prpty == null ? null : prpty.getProperty(key);
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long millis = Long.parseLong(value.trim());
|
||||||
|
if (millis < minValue) {
|
||||||
|
logger.warn(">> " + key + " = " + value + " is less than " + minValue + ". use default " + defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return millis;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
logger.warn(">> " + key + " = " + value + " is not a number. use default " + defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs,
|
private CacheConfiguration initCache(IgniteConfiguration config, int expiryDurationSecs,
|
||||||
String bootstrapAsynchronously, String name, int maxsize, boolean useCacheWriteModeAsync,
|
String bootstrapAsynchronously, String name, int maxsize, boolean useCacheWriteModeAsync,
|
||||||
boolean transactionMode) {
|
boolean transactionMode) {
|
||||||
@@ -848,17 +896,20 @@ public class SessionManagerForIgnite extends SessionManager {
|
|||||||
* 비관적 트랜잭션 안에서 읽으면 primary 의 값을 보장받는다. 트랜잭션은 읽기 직후 바로 닫는다.
|
* 비관적 트랜잭션 안에서 읽으면 primary 의 값을 보장받는다. 트랜잭션은 읽기 직후 바로 닫는다.
|
||||||
* (발급 HTTP 호출 구간까지 열어두면 장기 트랜잭션이 되어 파티션 맵 교환을 막는다)
|
* (발급 HTTP 호출 구간까지 열어두면 장기 트랜잭션이 되어 파티션 맵 교환을 막는다)
|
||||||
*
|
*
|
||||||
|
* 다른 노드가 발급 중이라 키 락을 기다리게 되면 readTxTimeoutMillis 후 포기하고 일반 get() 으로 읽는다.
|
||||||
|
*
|
||||||
* @param key 어댑터그룹명
|
* @param key 어댑터그룹명
|
||||||
* @return 캐시에 있는 토큰. 없으면 null.
|
* @return 캐시에 있는 토큰. 없으면 null.
|
||||||
*/
|
*/
|
||||||
private AccessTokenVO readFromPrimary(String key) {
|
private AccessTokenVO readFromPrimary(String key) {
|
||||||
try (Transaction tx = manager.transactions().txStart(TransactionConcurrency.PESSIMISTIC,
|
try (Transaction tx = manager.transactions().txStart(TransactionConcurrency.PESSIMISTIC,
|
||||||
TransactionIsolation.REPEATABLE_READ)) {
|
TransactionIsolation.REPEATABLE_READ, readTxTimeoutMillis, 1)) {
|
||||||
AccessTokenVO token = cacheOutBoundAccessToken.get(key);
|
AccessTokenVO token = cacheOutBoundAccessToken.get(key);
|
||||||
tx.commit();
|
tx.commit();
|
||||||
return token;
|
return token;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.error("occuring exception in readFromPrimary. key=" + key, e);
|
// 다른 노드가 발급 중이라 트랜잭션 타임아웃으로 빠지는 것은 예상된 경우라 WARN 으로 남긴다.
|
||||||
|
logger.warn("occuring exception in readFromPrimary. fallback to get(). key=" + key, e);
|
||||||
return cacheOutBoundAccessToken.get(key);
|
return cacheOutBoundAccessToken.get(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,7 +958,7 @@ public class SessionManagerForIgnite extends SessionManager {
|
|||||||
boolean acquired = false;
|
boolean acquired = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
acquired = lock.tryLock(REISSUE_LOCK_WAIT_SECONDS, TimeUnit.SECONDS);
|
acquired = lock.tryLock(reissueLockWaitMillis, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
if (!acquired) {
|
if (!acquired) {
|
||||||
logger.warn("reissueOutboundAccessToken lock timeout. key=" + key);
|
logger.warn("reissueOutboundAccessToken lock timeout. key=" + key);
|
||||||
@@ -1039,12 +1090,13 @@ public class SessionManagerForIgnite extends SessionManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock");
|
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock");
|
||||||
acquired = lock.tryLock(TOKEN_LOCK_WAIT_SECONDS, TimeUnit.SECONDS);
|
acquired = lock.tryLock(tokenLockWaitMillis, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
if ( acquired ) {
|
if ( acquired ) {
|
||||||
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock success=>"+acquired);
|
logger.debug("calling func getOutboundAccessToken = distributed ignite trylock success=>"+acquired);
|
||||||
// 3. 락 획득 후 다시 확인 (그 사이 다른 인스턴스가 갱신했을 수 있음)
|
// 3. 락 획득 후 다시 확인 (그 사이 다른 인스턴스가 갱신했을 수 있음)
|
||||||
token = cacheOutBoundAccessToken.get(key);
|
// near 캐시 사본은 무효화가 늦을 수 있어 get() 으로는 방금 발급된 토큰을 놓치고 다시 발급할 수 있다.
|
||||||
|
token = readFromPrimary(key);
|
||||||
|
|
||||||
logger.debug(String.format("getting token in ignite=%s", token));
|
logger.debug(String.format("getting token in ignite=%s", token));
|
||||||
|
|
||||||
@@ -1094,7 +1146,7 @@ public class SessionManagerForIgnite extends SessionManager {
|
|||||||
try {
|
try {
|
||||||
|
|
||||||
logger.debug("calling func removeOutboundAccessToken = distributed ignite trylock");
|
logger.debug("calling func removeOutboundAccessToken = distributed ignite trylock");
|
||||||
acquired = lock.tryLock(TOKEN_LOCK_WAIT_SECONDS, TimeUnit.SECONDS);
|
acquired = lock.tryLock(tokenLockWaitMillis, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
if( acquired ) {
|
if( acquired ) {
|
||||||
if (logger.isDebug()) {
|
if (logger.isDebug()) {
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ public class OAuthTokenStatusDTO {
|
|||||||
String serverName;
|
String serverName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 발급 이력 (최근 순, 최대 10건).
|
* 토큰 발급 이력 (최근 순).
|
||||||
*
|
*
|
||||||
* 이 노드에서 일어난 발급만 담기며 재기동 시 사라진다.
|
* 이 노드에서 일어난 발급만 담기며 재기동 시 사라진다.
|
||||||
* 목록 조회에는 담지 않고 어댑터그룹 단건 조회에서만 채운다.
|
* 단건 조회는 보관 중인 전체(최대 10건), 목록 조회는 최근 1건만 담는다.
|
||||||
*/
|
*/
|
||||||
List<TokenIssueHistory> issueHistory;
|
List<TokenIssueHistory> issueHistory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.eactive.eai.adapter.AdapterPropManager;
|
|||||||
import com.eactive.eai.adapter.AdapterVO;
|
import com.eactive.eai.adapter.AdapterVO;
|
||||||
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
||||||
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
||||||
|
import com.eactive.eai.common.authoutbound.TokenIssueHistory;
|
||||||
import com.eactive.eai.common.server.EAIServerManager;
|
import com.eactive.eai.common.server.EAIServerManager;
|
||||||
import com.eactive.eai.common.util.Logger;
|
import com.eactive.eai.common.util.Logger;
|
||||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||||
@@ -45,6 +46,9 @@ public class OAuthTokenStatusService {
|
|||||||
/** accessToken 중 노출할 앞자리 수 */
|
/** accessToken 중 노출할 앞자리 수 */
|
||||||
private static final int UNMASKED_LENGTH = 8;
|
private static final int UNMASKED_LENGTH = 8;
|
||||||
|
|
||||||
|
/** 목록 조회에 담을 발급 이력 건수 */
|
||||||
|
private static final int LIST_HISTORY_SIZE = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 등록된 모든 어댑터그룹의 토큰 현황을 반환한다.
|
* 등록된 모든 어댑터그룹의 토큰 현황을 반환한다.
|
||||||
*
|
*
|
||||||
@@ -53,39 +57,38 @@ public class OAuthTokenStatusService {
|
|||||||
public List<OAuthTokenStatusDTO> getStatusList() {
|
public List<OAuthTokenStatusDTO> getStatusList() {
|
||||||
List<OAuthTokenStatusDTO> result = new ArrayList<OAuthTokenStatusDTO>();
|
List<OAuthTokenStatusDTO> result = new ArrayList<OAuthTokenStatusDTO>();
|
||||||
for (String adapterGroupName : AccessTokenManagerByDB.getInstance().getRegisteredAdapterGroupNames()) {
|
for (String adapterGroupName : AccessTokenManagerByDB.getInstance().getRegisteredAdapterGroupNames()) {
|
||||||
// 목록에는 발급 이력을 담지 않는다. 그룹이 많으면 응답이 지나치게 커진다.
|
// 목록에는 최근 1건만 담는다. 전체를 담으면 그룹 수만큼 응답이 커진다.
|
||||||
result.add(getStatus(adapterGroupName, false));
|
result.add(getStatus(adapterGroupName, LIST_HISTORY_SIZE));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 어댑터그룹 하나의 토큰 현황을 발급 이력과 함께 반환한다.
|
* 어댑터그룹 하나의 토큰 현황을 발급 이력 전체와 함께 반환한다.
|
||||||
*
|
*
|
||||||
* @param adapterGroupName 어댑터그룹명
|
* @param adapterGroupName 어댑터그룹명
|
||||||
* @return 토큰 현황. 등록돼 있지 않으면 message 만 채워서 반환한다.
|
* @return 토큰 현황. 등록돼 있지 않으면 message 만 채워서 반환한다.
|
||||||
*/
|
*/
|
||||||
public OAuthTokenStatusDTO getStatus(String adapterGroupName) {
|
public OAuthTokenStatusDTO getStatus(String adapterGroupName) {
|
||||||
return getStatus(adapterGroupName, true);
|
return getStatus(adapterGroupName, Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 어댑터그룹 하나의 토큰 현황을 반환한다.
|
* 어댑터그룹 하나의 토큰 현황을 반환한다.
|
||||||
*
|
*
|
||||||
* @param adapterGroupName 어댑터그룹명
|
* @param adapterGroupName 어댑터그룹명
|
||||||
* @param includeHistory 토큰 발급 이력 포함 여부
|
* @param historyLimit 담을 발급 이력 건수 (최근 순)
|
||||||
* @return 토큰 현황. 등록돼 있지 않으면 message 만 채워서 반환한다.
|
* @return 토큰 현황. 등록돼 있지 않으면 message 만 채워서 반환한다.
|
||||||
*/
|
*/
|
||||||
private OAuthTokenStatusDTO getStatus(String adapterGroupName, boolean includeHistory) {
|
private OAuthTokenStatusDTO getStatus(String adapterGroupName, int historyLimit) {
|
||||||
OAuthTokenStatusDTO dto = new OAuthTokenStatusDTO();
|
OAuthTokenStatusDTO dto = new OAuthTokenStatusDTO();
|
||||||
dto.setAdapterGroupName(adapterGroupName);
|
dto.setAdapterGroupName(adapterGroupName);
|
||||||
|
|
||||||
AccessTokenManagerByDB manager = AccessTokenManagerByDB.getInstance();
|
AccessTokenManagerByDB manager = AccessTokenManagerByDB.getInstance();
|
||||||
|
|
||||||
if (includeHistory) {
|
// 이력은 노드 로컬이므로 어느 노드가 응답했는지 함께 담는다.
|
||||||
dto.setServerName(findLocalServerName());
|
dto.setServerName(findLocalServerName());
|
||||||
dto.setIssueHistory(manager.getIssueHistories(adapterGroupName));
|
dto.setIssueHistory(latest(manager.getIssueHistories(adapterGroupName), historyLimit));
|
||||||
}
|
|
||||||
OutboundOAuthCredentialVo credential = manager.getOutboundOAuthCredentialVo(adapterGroupName);
|
OutboundOAuthCredentialVo credential = manager.getOutboundOAuthCredentialVo(adapterGroupName);
|
||||||
|
|
||||||
if (credential == null) {
|
if (credential == null) {
|
||||||
@@ -175,6 +178,20 @@ public class OAuthTokenStatusService {
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발급 이력에서 최근 건만 잘라낸다. 이력은 이미 최근 순으로 정렬돼 있다.
|
||||||
|
*
|
||||||
|
* @param histories 발급 이력
|
||||||
|
* @param limit 담을 건수
|
||||||
|
* @return 최근 limit 건
|
||||||
|
*/
|
||||||
|
private List<TokenIssueHistory> latest(List<TokenIssueHistory> histories, int limit) {
|
||||||
|
if (histories.size() <= limit) {
|
||||||
|
return histories;
|
||||||
|
}
|
||||||
|
return new ArrayList<TokenIssueHistory>(histories.subList(0, limit));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 이 응답을 만든 서버명을 찾는다. 발급 이력이 노드 로컬이라 함께 알려준다.
|
* 이 응답을 만든 서버명을 찾는다. 발급 이력이 노드 로컬이라 함께 알려준다.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.eactive.eai.adapter.http.filter;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
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 org.mockito.Mockito;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.property.PropManager;
|
||||||
|
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CryptoFilterPathConfig 단위테스트
|
||||||
|
*
|
||||||
|
* PropManager 는 ApplicationContextProvider 의 context 에 mock 을 리플렉션으로 주입해 격리한다.
|
||||||
|
*/
|
||||||
|
class CryptoFilterPathConfigTest {
|
||||||
|
|
||||||
|
private static final String GROUP = "_TST_IN_RST_SyS";
|
||||||
|
|
||||||
|
private PropManager propManager;
|
||||||
|
|
||||||
|
private Field contextField;
|
||||||
|
|
||||||
|
private Object previousContext;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
propManager = Mockito.mock(PropManager.class);
|
||||||
|
|
||||||
|
ApplicationContext ctx = Mockito.mock(ApplicationContext.class);
|
||||||
|
Mockito.when(ctx.getBean(PropManager.class)).thenReturn(propManager);
|
||||||
|
|
||||||
|
contextField = ApplicationContextProvider.class.getDeclaredField("context");
|
||||||
|
contextField.setAccessible(true);
|
||||||
|
previousContext = contextField.get(null);
|
||||||
|
contextField.set(null, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
contextField.set(null, previousContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CryptoFilter 그룹이 있고 어댑터그룹의 key 에 value 가 설정된 상태로 만든다. */
|
||||||
|
private void configure(String adapterGroupName, String key, String value) {
|
||||||
|
Mockito.when(propManager.isContainProperties(CryptoFilterPathConfig.PROP_GROUP)).thenReturn(true);
|
||||||
|
Mockito.when(propManager.getProperty(CryptoFilterPathConfig.PROP_GROUP, adapterGroupName + "." + key, null))
|
||||||
|
.thenReturn(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertDefaults(Properties p) {
|
||||||
|
assertEquals("/encryptedData", p.getProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH));
|
||||||
|
assertEquals("/", p.getProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH));
|
||||||
|
assertEquals("/", p.getProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH));
|
||||||
|
assertEquals("/encryptedData", p.getProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("1. CryptoFilter 그룹이 없으면 4개 경로 모두 기본값")
|
||||||
|
void 그룹없음_기본값() {
|
||||||
|
Properties p = new Properties();
|
||||||
|
|
||||||
|
Properties result = CryptoFilterPathConfig.apply(GROUP, p);
|
||||||
|
|
||||||
|
assertSame(p, result);
|
||||||
|
assertDefaults(p);
|
||||||
|
Mockito.verify(propManager, Mockito.never()).getProperty(anyString(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("2. 그룹은 있지만 어댑터그룹 키가 없으면 기본값")
|
||||||
|
void 키없음_기본값() {
|
||||||
|
Mockito.when(propManager.isContainProperties(CryptoFilterPathConfig.PROP_GROUP)).thenReturn(true);
|
||||||
|
Properties p = new Properties();
|
||||||
|
|
||||||
|
CryptoFilterPathConfig.apply(GROUP, p);
|
||||||
|
|
||||||
|
assertDefaults(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("3. 4개 키가 모두 설정되면 모두 반영")
|
||||||
|
void 네개키_반영() {
|
||||||
|
configure(GROUP, AbstractCryptoFilter.PROP_DEC_FROM_PATH, "/encrypted_data");
|
||||||
|
configure(GROUP, AbstractCryptoFilter.PROP_DEC_TO_PATH, "/data");
|
||||||
|
configure(GROUP, AbstractCryptoFilter.PROP_ENC_FROM_PATH, "/data");
|
||||||
|
configure(GROUP, AbstractCryptoFilter.PROP_ENC_TO_PATH, "/encrypted_data");
|
||||||
|
Properties p = new Properties();
|
||||||
|
|
||||||
|
CryptoFilterPathConfig.apply(GROUP, p);
|
||||||
|
|
||||||
|
assertEquals("/encrypted_data", p.getProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH));
|
||||||
|
assertEquals("/data", p.getProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH));
|
||||||
|
assertEquals("/data", p.getProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH));
|
||||||
|
assertEquals("/encrypted_data", p.getProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("4. 빈 값은 무시하고 기본값 유지, 앞뒤 공백은 제거")
|
||||||
|
void 빈값무시_공백제거() {
|
||||||
|
configure(GROUP, AbstractCryptoFilter.PROP_DEC_FROM_PATH, " ");
|
||||||
|
configure(GROUP, AbstractCryptoFilter.PROP_ENC_TO_PATH, " /encrypted_data ");
|
||||||
|
Properties p = new Properties();
|
||||||
|
|
||||||
|
CryptoFilterPathConfig.apply(GROUP, p);
|
||||||
|
|
||||||
|
assertEquals("/encryptedData", p.getProperty(AbstractCryptoFilter.PROP_DEC_FROM_PATH));
|
||||||
|
assertEquals("/encrypted_data", p.getProperty(AbstractCryptoFilter.PROP_ENC_TO_PATH));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("5. 다른 어댑터그룹의 키는 반영하지 않는다")
|
||||||
|
void 다른그룹_미반영() {
|
||||||
|
configure("_OTHER_IN_RST_SyS", AbstractCryptoFilter.PROP_DEC_FROM_PATH, "/encrypted_data");
|
||||||
|
Properties p = new Properties();
|
||||||
|
|
||||||
|
CryptoFilterPathConfig.apply(GROUP, p);
|
||||||
|
|
||||||
|
assertDefaults(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("6. 어댑터그룹명이 없으면 PropManager 를 조회하지 않고 기본값")
|
||||||
|
void 그룹명없음_기본값() {
|
||||||
|
Mockito.when(propManager.isContainProperties(CryptoFilterPathConfig.PROP_GROUP)).thenReturn(true);
|
||||||
|
Properties p = new Properties();
|
||||||
|
|
||||||
|
CryptoFilterPathConfig.apply(null, p);
|
||||||
|
|
||||||
|
assertDefaults(p);
|
||||||
|
Mockito.verify(propManager, Mockito.never()).getProperty(anyString(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("7. 앞 단계에서 들어온 경로 값은 기본값/설정값으로 덮어쓴다")
|
||||||
|
void 기존값_덮어씀() {
|
||||||
|
Properties p = new Properties();
|
||||||
|
p.setProperty(AbstractCryptoFilter.PROP_DEC_TO_PATH, "/leftover");
|
||||||
|
p.setProperty(AbstractCryptoFilter.PROP_ENC_FROM_PATH, "/leftover");
|
||||||
|
|
||||||
|
CryptoFilterPathConfig.apply(GROUP, p);
|
||||||
|
|
||||||
|
assertDefaults(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
-3
@@ -58,6 +58,9 @@ class SessionManagerForIgniteReissueTest {
|
|||||||
/** readFromPrimary 가 여는 트랜잭션 */
|
/** readFromPrimary 가 여는 트랜잭션 */
|
||||||
private Transaction transaction;
|
private Transaction transaction;
|
||||||
|
|
||||||
|
/** readFromPrimary 가 트랜잭션을 여는 곳 (타임아웃 인자 검증용) */
|
||||||
|
private IgniteTransactions transactions;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
sessionManager = new SessionManagerForIgnite();
|
sessionManager = new SessionManagerForIgnite();
|
||||||
@@ -86,9 +89,9 @@ class SessionManagerForIgniteReissueTest {
|
|||||||
*/
|
*/
|
||||||
private void injectIgnite() throws Exception {
|
private void injectIgnite() throws Exception {
|
||||||
transaction = Mockito.mock(Transaction.class);
|
transaction = Mockito.mock(Transaction.class);
|
||||||
IgniteTransactions transactions = Mockito.mock(IgniteTransactions.class);
|
transactions = Mockito.mock(IgniteTransactions.class);
|
||||||
Mockito.when(transactions.txStart(any(TransactionConcurrency.class), any(TransactionIsolation.class)))
|
Mockito.when(transactions.txStart(any(TransactionConcurrency.class), any(TransactionIsolation.class),
|
||||||
.thenReturn(transaction);
|
Mockito.anyLong(), Mockito.anyInt())).thenReturn(transaction);
|
||||||
|
|
||||||
Ignite ignite = Mockito.mock(Ignite.class);
|
Ignite ignite = Mockito.mock(Ignite.class);
|
||||||
Mockito.when(ignite.transactions()).thenReturn(transactions);
|
Mockito.when(ignite.transactions()).thenReturn(transactions);
|
||||||
@@ -316,4 +319,48 @@ class SessionManagerForIgniteReissueTest {
|
|||||||
assertTrue(locks.containsKey("GRP-A") && locks.containsKey("GRP-B"), "그룹별로 락이 분리돼야 한다");
|
assertTrue(locks.containsKey("GRP-A") && locks.containsKey("GRP-B"), "그룹별로 락이 분리돼야 한다");
|
||||||
assertSame(locks.get("GRP-A"), locks.get("GRP-A"));
|
assertSame(locks.get("GRP-A"), locks.get("GRP-A"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("8. 트랜잭션 읽기에는 타임아웃을 준다 (발급 중인 노드의 락을 무한정 기다리지 않도록)")
|
||||||
|
void 트랜잭션읽기_타임아웃() {
|
||||||
|
store.put(GROUP, token("OLD"));
|
||||||
|
|
||||||
|
sessionManager.reissueOutboundAccessToken(GROUP, "OLD", 0L, supplier(new AtomicInteger(), "NEW"));
|
||||||
|
|
||||||
|
Mockito.verify(transactions, Mockito.times(2)).txStart(
|
||||||
|
Mockito.eq(TransactionConcurrency.PESSIMISTIC), Mockito.eq(TransactionIsolation.REPEATABLE_READ),
|
||||||
|
Mockito.longThat(timeout -> timeout > 0), Mockito.anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("9. getOutboundAccessToken 도 락 안의 재확인은 트랜잭션으로 읽는다")
|
||||||
|
void 최초발급_락안에서_트랜잭션읽기() {
|
||||||
|
AtomicInteger issued = new AtomicInteger();
|
||||||
|
|
||||||
|
AccessTokenVO result = sessionManager.getOutboundAccessToken(GROUP, supplier(issued, "NEW"));
|
||||||
|
|
||||||
|
assertEquals(1, issued.get());
|
||||||
|
assertEquals("NEW", result.getAccessToken());
|
||||||
|
Mockito.verify(transaction, Mockito.times(1)).commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("10. getOutboundAccessToken - 락 안에서 읽은 토큰이 유효하면 발급하지 않는다")
|
||||||
|
void 최초발급_락안에서_이미발급됨() {
|
||||||
|
// 락 밖 get() 은 옛 만료 토큰(near 캐시 사본), 락 안의 트랜잭션 읽기는 다른 노드가 방금 넣은 토큰을 돌려준다.
|
||||||
|
OAuth2AccessTokenVO expired = token("OLD");
|
||||||
|
expired.setExpiration(new Date(System.currentTimeMillis() - 1_000L));
|
||||||
|
store.put(GROUP, expired);
|
||||||
|
Mockito.when(transactions.txStart(any(TransactionConcurrency.class), any(TransactionIsolation.class),
|
||||||
|
Mockito.anyLong(), Mockito.anyInt())).thenAnswer(invocation -> {
|
||||||
|
store.put(GROUP, token("ALREADY-NEW"));
|
||||||
|
return transaction;
|
||||||
|
});
|
||||||
|
|
||||||
|
AtomicInteger issued = new AtomicInteger();
|
||||||
|
AccessTokenVO result = sessionManager.getOutboundAccessToken(GROUP, supplier(issued, "NEW"));
|
||||||
|
|
||||||
|
assertEquals(0, issued.get(), "락 안에서 확인한 토큰이 유효하면 발급하지 않아야 한다");
|
||||||
|
assertEquals("ALREADY-NEW", result.getAccessToken());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -23,6 +24,7 @@ import org.springframework.context.ApplicationContext;
|
|||||||
|
|
||||||
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
||||||
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
||||||
|
import com.eactive.eai.common.authoutbound.TokenIssueHistory;
|
||||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||||
|
|
||||||
@@ -64,6 +66,21 @@ class OAuthTokenStatusServiceTest {
|
|||||||
return credential;
|
return credential;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 최근 순으로 정렬된 발급 이력 count 건 (svc-0 이 최신).
|
||||||
|
*
|
||||||
|
* TokenIssueHistory 생성자는 package-private 이라 mock 으로 만든다.
|
||||||
|
*/
|
||||||
|
private List<TokenIssueHistory> histories(int count) {
|
||||||
|
List<TokenIssueHistory> result = new ArrayList<>();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
TokenIssueHistory history = Mockito.mock(TokenIssueHistory.class);
|
||||||
|
Mockito.when(history.getServiceClass()).thenReturn("svc-" + i);
|
||||||
|
result.add(history);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/** 만료까지 expiresInSec 남은 토큰 */
|
/** 만료까지 expiresInSec 남은 토큰 */
|
||||||
private OAuth2AccessTokenVO token(String accessToken, long expiresInSec) {
|
private OAuth2AccessTokenVO token(String accessToken, long expiresInSec) {
|
||||||
OAuth2AccessTokenVO vo = new OAuth2AccessTokenVO();
|
OAuth2AccessTokenVO vo = new OAuth2AccessTokenVO();
|
||||||
@@ -277,20 +294,35 @@ class OAuthTokenStatusServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("9-1. 단건 조회에는 발급 이력을 담고 목록에는 담지 않는다")
|
@DisplayName("9-1. 단건 조회는 이력 전체, 목록은 최근 1건만 담는다")
|
||||||
void 이력포함범위() {
|
void 이력포함범위() {
|
||||||
Set<String> groups = new LinkedHashSet<>(Arrays.asList(GROUP));
|
Set<String> groups = new LinkedHashSet<>(Arrays.asList(GROUP));
|
||||||
Mockito.when(mockManager.getRegisteredAdapterGroupNames()).thenReturn(groups);
|
Mockito.when(mockManager.getRegisteredAdapterGroupNames()).thenReturn(groups);
|
||||||
registerCredential(GROUP);
|
registerCredential(GROUP);
|
||||||
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijkl", 600));
|
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijkl", 600));
|
||||||
Mockito.when(mockManager.getIssueHistories(GROUP)).thenReturn(Collections.emptyList());
|
// histories() 안에서 mock 을 만들므로 when(...) 인자에 직접 넣으면 스터빙이 중첩된다.
|
||||||
|
List<TokenIssueHistory> histories = histories(3);
|
||||||
|
Mockito.when(mockManager.getIssueHistories(GROUP)).thenReturn(histories);
|
||||||
|
|
||||||
OAuthTokenStatusDTO single = service.getStatus(GROUP);
|
OAuthTokenStatusDTO single = service.getStatus(GROUP);
|
||||||
OAuthTokenStatusDTO fromList = service.getStatusList().get(0);
|
OAuthTokenStatusDTO fromList = service.getStatusList().get(0);
|
||||||
|
|
||||||
assertNotNull(single.getIssueHistory(), "단건 조회에는 이력이 있어야 한다");
|
assertEquals(3, single.getIssueHistory().size(), "단건 조회는 보관 중인 이력 전체");
|
||||||
assertNull(fromList.getIssueHistory(), "목록에는 이력을 담지 않는다");
|
assertEquals(1, fromList.getIssueHistory().size(), "목록은 최근 1건만");
|
||||||
Mockito.verify(mockManager, Mockito.times(1)).getIssueHistories(GROUP);
|
assertEquals("svc-0", fromList.getIssueHistory().get(0).getServiceClass(), "최신 건이어야 한다");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("9-2. 이력이 없으면 빈 목록 (null 아님)")
|
||||||
|
void 이력없으면_빈목록() {
|
||||||
|
registerCredential(GROUP);
|
||||||
|
Mockito.when(mockManager.peekAccessTokenVO(GROUP)).thenReturn(token("abcdefghijkl", 600));
|
||||||
|
Mockito.when(mockManager.getIssueHistories(GROUP)).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
OAuthTokenStatusDTO dto = service.getStatus(GROUP);
|
||||||
|
|
||||||
|
assertNotNull(dto.getIssueHistory());
|
||||||
|
assertTrue(dto.getIssueHistory().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user