package com.eactive.eai.common.authoutbound; import com.eactive.eai.adapter.AdapterGroupVO; import com.eactive.eai.adapter.AdapterManager; import com.eactive.eai.adapter.AdapterPropManager; import com.eactive.eai.adapter.AdapterVO; import com.eactive.eai.authoutbound.OutboundOAuthCredentialDao; import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo; import com.eactive.eai.authoutbound.client.HttpClientAccessTokenServiceByDB; import com.eactive.eai.authoutbound.client.HttpClientAccessTokenServiceFactoryByDB; import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.lifecycle.Lifecycle; import com.eactive.eai.common.lifecycle.LifecycleException; import com.eactive.eai.common.lifecycle.LifecycleListener; import com.eactive.eai.common.lifecycle.LifecycleSupport; import com.eactive.eai.common.session.SessionManager; import com.eactive.eai.common.session.SessionManagerForIgnite; import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.Logger; import com.mchange.v1.cachedstore.CachedStore.Manager; import com.openbanking.eai.common.token.AccessTokenVO; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; import java.util.function.Function; import java.util.function.Supplier; /** * 1. 기능 : Access 토큰 정보를 DB로 부터 로딩하고 만료시 재발급 정보를 메모리에 관리하는 Manager 클래스 * 2. 처리 개요 : DB 프로퍼티 정보에 저장된 Access 토큰 정보를 메모리에 로딩하거나 재발급 관리한다. * * - * 3. 주의사항 * * @author : * @version : v 1.0.0 * @see : 관련 기능을 참조 * @since : * : */ @Component public class AccessTokenManagerByDB implements Lifecycle { /** * Default Logger */ static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); /** * AccessTokenManager Single Instance */ private static AccessTokenManagerByDB instance = new AccessTokenManagerByDB(); /** * LifecyleSupport object */ private LifecycleSupport lifecycle = new LifecycleSupport(this); /** * 기동 여부 */ private boolean started; /** * 인증 정보를 저장하는 Map */ private Map outboundOAuthCredentialVos; @Autowired OutboundOAuthCredentialDao outboundOAuthCredentialDao; private ScheduledExecutorService scheduler; private Map> scheduledTasks; private Set runningTasks; private static final long TASK_TIMEOUT_SECONDS = 30; private static final int TOKEN_SCHEDULER_THREAD_POOL_SIZE = 30; public AccessTokenManagerByDB() { scheduledTasks = new ConcurrentHashMap<>(); outboundOAuthCredentialVos = new HashMap<>(); runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<>()); scheduler = Executors.newScheduledThreadPool(TOKEN_SCHEDULER_THREAD_POOL_SIZE); } /** * 1. 기능 : AccessTokenManager Singleton Object를 반환하는 getter method * 2. 처리 개요 : AccessTokenManager Singleton Object를 반환한다. * - * 3. 주의사항 * * @param * @return * @exception **/ public static AccessTokenManagerByDB getInstance() { return ApplicationContextProvider.getContext().getBean(AccessTokenManagerByDB.class); } /** * 1. 기능 : AccessTokenManager의 초기화 여부를 반환하는 getter 메서드 * 2. 처리 개요 : AccessTokenManager의 초기화 여부를 반환한다. * 3. 주의사항 * * @return 초기화 여부 **/ public boolean isStarted() { return this.started; } public void start() throws LifecycleException { if (started) throw new LifecycleException("RECEAICPM201"); lifecycle.fireLifecycleEvent(STARTING_EVENT, this); try { init(); } catch(Exception e) { throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201")); } started = true; if (logger.isWarn()){ logger.warn("Service started successfully"); } lifecycle.fireLifecycleEvent(STARTED_EVENT, this); } private void init() throws Exception { List OutboundOAuthCredentialVos = outboundOAuthCredentialDao.getAllOutboundOAuthCredentials(); logger.info("Initializing with {} credential configurations", OutboundOAuthCredentialVos.size()); for (OutboundOAuthCredentialVo outboundOAuthCredentialVo : OutboundOAuthCredentialVos) { startToken(outboundOAuthCredentialVo); outboundOAuthCredentialVos.put(outboundOAuthCredentialVo.getAdapterGroupName(), outboundOAuthCredentialVo); } } public void stop() throws LifecycleException { if (!started) throw new LifecycleException("RECEAICPM203"); lifecycle.fireLifecycleEvent(STOPING_EVENT, this); // 모든 스케줄된 작업 중지 scheduledTasks.forEach((name, future) -> { future.cancel(true); logger.warn("Task stopped for adapter group: {}", name); }); scheduledTasks.clear(); // 실행 중인 작업 세트 클리어 runningTasks.clear(); // 스케줄러 종료 scheduler.shutdown(); try { if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) { scheduler.shutdownNow(); logger.warn("Forced shutdown of scheduler after timeout"); } } catch (InterruptedException e) { scheduler.shutdownNow(); logger.error("Scheduler shutdown interrupted", e); } started = false; if (logger.isWarn()){ logger.warn("Service stopped successfully"); } lifecycle.fireLifecycleEvent(STOPPED_EVENT, this); } public synchronized void refresh(OutboundOAuthCredentialVo outboundOAuthCredentialVo) throws Exception { String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName(); logger.info("Starting refresh for adapter group: {}", adapterGroupName); // 기존 스케줄러 작업 취소 stopToken(adapterGroupName); // 인증 정보 갱신 outboundOAuthCredentialVos.put(adapterGroupName, outboundOAuthCredentialVo); if("N".equals(outboundOAuthCredentialVo.getUseYn())) { logger.info("Token task disabled for adapter group: {}", adapterGroupName); return; } // Y인 경우 새로운 스케줄러 작업 시작 logger.info("Restarting token task for adapter group: {}", adapterGroupName); startToken(outboundOAuthCredentialVo); } public void startToken(final OutboundOAuthCredentialVo outboundOAuthCredentialVo) { if (!"Y".equals(outboundOAuthCredentialVo.getUseYn())) { logger.debug("Token task not started - disabled for adapter group: {}", outboundOAuthCredentialVo.getAdapterGroupName()); return; } final String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName(); if (scheduledTasks.containsKey(adapterGroupName)) { logger.warn("Token task not started - already scheduled for adapter group: {}", adapterGroupName); return; } long interval = outboundOAuthCredentialVo.getIntervalSec() > 0 ? outboundOAuthCredentialVo.getIntervalSec() : 24 * 60 * 60; logger.info("Starting token task for adapter group: {} with interval: {} seconds", adapterGroupName, interval); Runnable periodicTask = () -> executeTokenTask(adapterGroupName, outboundOAuthCredentialVo); ScheduledFuture scheduledTask = scheduler.scheduleAtFixedRate( periodicTask, 0, // 즉시 첫 실행 interval, TimeUnit.SECONDS ); scheduledTasks.put(adapterGroupName, scheduledTask); } private void executeTokenTask(String adapterGroupName, OutboundOAuthCredentialVo credential) { if (!runningTasks.add(adapterGroupName)) { logger.info("Task skipped - already running for adapter group: {}", adapterGroupName); return; } logger.debug("Starting token task for adapter group: {}", adapterGroupName); try { Future future = scheduler.submit(() -> { try { logger.debug("Executing token issuance for adapter group: {}", adapterGroupName); SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function() { @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 { // 토큰이 아직 유효한 경우 logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration()); return accessToken; } } else { //토큰이 있지만 만료 시간이 없는 경우 logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName); return accessToken; } } }); logger.debug("Token issuance completed for adapter group: {}", adapterGroupName); } catch (Exception e) { logger.error("Token issuance failed for adapter group: {}", adapterGroupName, e); } }); try { future.get(TASK_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (TimeoutException e) { logger.error("Task timed out for adapter group: {}", adapterGroupName); future.cancel(true); } catch (Exception e) { logger.error("Task execution failed for adapter group: {}", adapterGroupName, e); } } finally { runningTasks.remove(adapterGroupName); logger.debug("Token task completed for adapter group: {}", adapterGroupName); } } private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo) { String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName(); AccessTokenVO accessToken = null; try { // boolean isExpired = true; // if (tokens.containsKey(adapterGroupName)) { // AccessTokenVO accessToken = tokens.get(adapterGroupName); // isExpired = accessToken.isExpired(); // } // logger.debug("Token status for adapter group: {}, expired: {}, isTokenExpiredWithinInterval: {}", adapterGroupName, isExpired, isTokenExpiredWithinInterval); // if (isExpired || isTokenExpiredWithinInterval) { logger.info("Issuing new token for adapter group: {}", adapterGroupName); // tokens.remove(adapterGroupName); AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName); if (gvo != null && gvo.getAdapters().hasNext()) { AdapterVO avo = gvo.getAdapters().next(); Properties properties = AdapterPropManager.getInstance().getProperties(avo.getPropGroupName()); String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE"); HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type); if(service != null) { logger.debug("Executing token service of type: {} for adapter group: {}", type, adapterGroupName); accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo); logger.info("New token issued successfully for adapter group: {}", adapterGroupName); return accessToken; } else { logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName); } } else { logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName); } } catch (Exception e) { logger.error("Task execution failed for adapter group: {}", adapterGroupName, e); } return accessToken; } public void stopToken(String adapterGroupName) { ScheduledFuture task = scheduledTasks.remove(adapterGroupName); if (task != null) { task.cancel(true); logger.warn("Token task stopped for adapter group: {}", adapterGroupName); } SessionManager.getInstance().removeOutboundAccessToken(adapterGroupName); logger.debug("Token removed for adapter group: {}", adapterGroupName); } public void refresh(String adapterGroupName) throws Exception { refresh(outboundOAuthCredentialDao.getOutboundOAuthCredential(adapterGroupName)); } /** * 1. 기능 : LifecycleListener를 등록하는 메서드 * 2. 처리 개요 : LifecycleListener를 등록한다. * 3. 주의사항 * * @param listener LifecycleEvent를 수신한 LifecycleListener **/ public void addLifecycleListener(LifecycleListener listener) { lifecycle.addLifecycleListener(listener); } /** * 1. 기능 : 등록된 LifecycleListener 리스트를 반환하는 메서드 * 2. 처리 개요 : 등록된 LifecycleListener 리스트를 반환하다. * 3. 주의사항 * * @return 등록된 LifecycleListener 리스트 **/ public LifecycleListener[] findLifecycleListeners() { return lifecycle.findLifecycleListeners(); } /** * 1. 기능 : 등록된 LifecycleListener를 삭제하는 메서드 * 2. 처리 개요 : 파라미터의 LifecycleListener를 삭제한다. * 3. 주의사항 * * @param listener 삭제할 LifecycleListener **/ public void removeLifecycleListener(LifecycleListener listener) { lifecycle.removeLifecycleListener(listener); } public boolean isOAuthCredentialRegistered(String adapterGroupName){ OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName); return outboundOAuthCredentialVo != null && "Y".equals(outboundOAuthCredentialVo.getUseYn()); } /** * 1. 기능 : Access 토큰 정보를 반환하는 메서드 * 2. 처리 개요 : Access 토큰 정보를 반환한다. * - * 3. 주의사항 * * @return Access 토큰 정보 **/ public AccessTokenVO getAccessTokenVO(String adapterGroupName) { AccessTokenVO accessToken = null; try { accessToken = SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function() { @Override public AccessTokenVO apply(AccessTokenVO t) { OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos .get(adapterGroupName); return issueToken(outboundOAuthCredentialVo); } }); } catch (Exception e) { if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. [" + adapterGroupName + "] ", e); } return accessToken; } /** * 1. 기능 : Access 토큰 값를 반환하는 메서드 * 2. 처리 개요 : Access 토큰 값를 반환한다. * - * 3. 주의사항 * * @return accessToken 값 **/ public String getAccessToken(String adapterGroupName) { String accessToken = null; try { accessToken = getAccessTokenVO(adapterGroupName).getAccessToken(); } catch(Exception e) { if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e); } return accessToken; } /** * 1. 기능 : Access 토큰를 삭제하는 메서드 * 2. 처리 개요 : Access 토큰를 삭제한다. * - * 3. 주의사항 * **/ public void removeAccessTokenVO(String adapterGroupName) { // 스케줄러 작업 중지 및 토큰 제거 stopToken(adapterGroupName); // OAuth 인증 정보 제거 outboundOAuthCredentialVos.remove(adapterGroupName); } /** * 1. 기능 : Access 토큰를 재발급하는 메서드 * 2. 처리 개요 : Access 토큰를 재발급한다. * - * 3. 주의사항 * **/ public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties, String oldToken) throws Exception { AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName); // 동시 요청이 발생할 경우 synchronized 처리를 했기때문에 토큰을 다시 한번 체크한다. if (accessToken == null || accessToken.isExpired() || StringUtils.equals(accessToken.getAccessToken(), oldToken)) { if (isOAuthCredentialRegistered(adapterGroupName) == false) { throw new Exception( "There is no OAuthCredentialRegistered information, or whether to use it is 'N'."); } String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE"); HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type); OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName); if (service != null) { accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo); } } return accessToken; } }