From e32acfa215d42040bb5a5d24bd3d2116cb60dbe9 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 11 Aug 2026 19:10:39 +0900 Subject: [PATCH] =?UTF-8?q?UserLog=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API=20User-Agent=20=EC=B2=98=EB=A6=AC=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20-=20UserLog=EC=97=90=20userAgent(500=EC=9E=90)?= =?UTF-8?q?=EC=99=80=20duplicate(Boolean)=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/=EC=8B=A4=ED=8C=A8/?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=8B=9C=20User-Agent=EC=99=80=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EC=97=AC=EB=B6=80=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?-=20TwoFactorService/Controller=20Request=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC=20=EB=B0=8F=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/twofactor/TwoFactorController.java | 3 +- .../apps/auth/twofactor/TwoFactorService.java | 18 ++++++---- .../apps/login/service/LoginFinalizer.java | 7 +++- .../user/service/PortalUserLogService.java | 35 +++++++++++++------ .../PortalAuthenticationFailureHandler.java | 3 +- 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java b/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java index e26ad47..46f9391 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorController.java @@ -41,8 +41,9 @@ public class TwoFactorController { public TwoFactorSendResponse send(@RequestParam String channel, @RequestParam(required = false) String purpose, @RequestParam(required = false, defaultValue = "false") boolean force, + HttpServletRequest request, HttpSession session) { - return twoFactorService.send(session, channel, purpose, force); + return twoFactorService.send(request, session, channel, purpose, force); } /** 인증번호 검증 */ diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java b/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java index 43d87c6..aa1e6b5 100644 --- a/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java +++ b/src/main/java/com/eactive/apim/portal/apps/auth/twofactor/TwoFactorService.java @@ -115,7 +115,8 @@ public class TwoFactorService { // SEND // ========================================================================= - public TwoFactorSendResponse send(HttpSession session, String channel, String purpose, boolean force) { + public TwoFactorSendResponse send(HttpServletRequest request, HttpSession session, String channel, + String purpose, boolean force) { TwoFactorSendResponse res = new TwoFactorSendResponse(); TwoFactorContext.Mode mode = resolveMode(session); @@ -156,7 +157,7 @@ public class TwoFactorService { res.setMessage("진행 중인 다른 인증 절차가 있습니다. 강제 종료 후 진행하시겠습니까?"); return res; } - discardContext(session, ctx); // 강제 종료(감사 기록 포함) + discardContext(request, session, ctx); // 강제 종료(감사 기록 포함) } else if (ctx.getStartedAt() != null && ctx.getStartedAt().plusSeconds(RESEND_THROTTLE_SECONDS).isAfter(LocalDateTime.now())) { res.setValid(false); @@ -280,7 +281,7 @@ public class TwoFactorService { String stateError = revalidateLoginState(user); if (stateError != null) { userLogService.logFailure(loginId, request.getRemoteAddr(), session.getId(), - LoginFailureReason.ACCOUNT_DISABLED); + LoginFailureReason.ACCOUNT_DISABLED, request.getHeader("User-Agent")); clearPending(session); res.setValid(false); res.setTerminated(true); @@ -316,7 +317,8 @@ public class TwoFactorService { if (ctx != null && ctx.getMode() == TwoFactorContext.Mode.LOGIN) { String loginId = (String) session.getAttribute(ATTR_PENDING_LOGIN_ID); - userLogService.logFailure(loginId, request.getRemoteAddr(), session.getId(), failureReason); + userLogService.logFailure(loginId, request.getRemoteAddr(), session.getId(), failureReason, + request.getHeader("User-Agent")); } if (ctx != null && ctx.getRecipient() != null) { authNumberStorage.deleteAuthNumber(ctx.getRecipient()); @@ -456,10 +458,11 @@ public class TwoFactorService { } /** 강제 종료: 인증번호 삭제 + (로그인 컨텍스트면) 취소 감사 기록 */ - private void discardContext(HttpSession session, TwoFactorContext ctx) { + private void discardContext(HttpServletRequest request, HttpSession session, TwoFactorContext ctx) { if (ctx.getMode() == TwoFactorContext.Mode.LOGIN) { String loginId = (String) session.getAttribute(ATTR_PENDING_LOGIN_ID); - userLogService.logFailure(loginId, "-", session.getId(), LoginFailureReason.TWO_FACTOR_CANCELLED); + userLogService.logFailure(loginId, request.getRemoteAddr(), session.getId(), + LoginFailureReason.TWO_FACTOR_CANCELLED, request.getHeader("User-Agent")); } if (ctx.getRecipient() != null) { authNumberStorage.deleteAuthNumber(ctx.getRecipient()); @@ -472,7 +475,8 @@ public class TwoFactorService { LoginFailureReason reason, HttpServletRequest request) { if (ctx.getMode() == TwoFactorContext.Mode.LOGIN) { String loginId = (String) session.getAttribute(ATTR_PENDING_LOGIN_ID); - userLogService.logFailure(loginId, request.getRemoteAddr(), session.getId(), reason); + userLogService.logFailure(loginId, request.getRemoteAddr(), session.getId(), reason, + request.getHeader("User-Agent")); clearPending(session); } if (ctx.getRecipient() != null) { diff --git a/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java b/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java index b6d3978..2ca023b 100644 --- a/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java +++ b/src/main/java/com/eactive/apim/portal/apps/login/service/LoginFinalizer.java @@ -92,7 +92,12 @@ public class LoginFinalizer { String ip = request.getRemoteAddr(); String sessionId = request.getSession().getId(); - userLogService.logSuccess(rawUsername, ip, sessionId, loginType); + // 다른 기기/브라우저의 활성 세션이 남아 있는 상태의 로그인인지(아래 forceLogoutOtherSessions 로 + // 기존 세션이 끊기는 건인지) 기록한다. 현재 세션 등록(registerSession)은 이 뒤에 일어나므로 + // 여기서 조회되는 활성 세션은 항상 "다른 곳"의 세션이다. + boolean duplicate = userSessionService.getActiveSession(normalizedUsername).isPresent(); + userLogService.logSuccess(rawUsername, ip, sessionId, loginType, request.getHeader("User-Agent"), + duplicate); String contextPath = request.getContextPath(); HttpSession session = request.getSession(); diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserLogService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserLogService.java index b214538..13abf39 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserLogService.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/PortalUserLogService.java @@ -12,17 +12,22 @@ import org.springframework.transaction.annotation.Transactional; @Transactional public class PortalUserLogService { + /** PTL_USER_LOG.USER_AGENT 컬럼 길이. 초과분은 잘라서 저장한다(초과 시 ORA-12899 로 로그인 실패). */ + private static final int USER_AGENT_MAX_LENGTH = 500; + private final UserLogRepository userLogRepository; public PortalUserLogService(UserLogRepository userLogRepository) { this.userLogRepository = userLogRepository; } - public void logSuccess(String userId, String ip, String sessionId) { - logSuccess(userId, ip, sessionId, LoginType.NORMAL); - } - - public void logSuccess(String userId, String ip, String sessionId, LoginType loginType) { + /** + * 로그인 성공 기록. + * + * @param duplicate 로그인 시점에 다른 활성 세션이 남아 있어 기존 세션이 강제 해제된 건이면 true + */ + public void logSuccess(String userId, String ip, String sessionId, LoginType loginType, String userAgent, + boolean duplicate) { UserLog log = new UserLog(); log.setLoginId(userId); log.setLoginTime(LocalDateTime.now()); @@ -30,15 +35,13 @@ public class PortalUserLogService { log.setSessionId(sessionId); log.setSuccess(true); log.setLoginType(loginType != null ? loginType.name() : null); + log.setUserAgent(truncateUserAgent(userAgent)); + log.setDuplicate(duplicate); userLogRepository.save(log); } - public void logFailure(String userId, String ip, String sessionId) { - logFailure(userId, ip, sessionId, LoginFailureReason.UNKNOWN); - } - - public void logFailure(String userId, String ip, String sessionId, LoginFailureReason reason) { + public void logFailure(String userId, String ip, String sessionId, LoginFailureReason reason, String userAgent) { UserLog log = new UserLog(); log.setLoginId(userId); log.setLoginTime(LocalDateTime.now()); @@ -46,19 +49,29 @@ public class PortalUserLogService { log.setSessionId(sessionId); log.setSuccess(false); log.setFailureReason(reason != null ? reason.name() : LoginFailureReason.UNKNOWN.name()); + log.setUserAgent(truncateUserAgent(userAgent)); userLogRepository.save(log); } @Transactional - public void logLogout(String userId, String ip, String sessionId) { + public void logLogout(String userId, String ip, String sessionId, String userAgent) { UserLog log = new UserLog(); log.setLoginId(userId); log.setLogoutTime(LocalDateTime.now()); log.setIp(ip); log.setSessionId(sessionId); log.setSuccess(true); + log.setUserAgent(truncateUserAgent(userAgent)); userLogRepository.save(log); } + + private String truncateUserAgent(String userAgent) { + if (userAgent == null) { + return null; + } + return userAgent.length() <= USER_AGENT_MAX_LENGTH + ? userAgent : userAgent.substring(0, USER_AGENT_MAX_LENGTH); + } } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java index 7da961f..f2aa349 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java @@ -97,7 +97,8 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure } } - userLogService.logFailure(username, ip, sessionId, resolveFailureReason(exception)); + userLogService.logFailure(username, ip, sessionId, resolveFailureReason(exception), + request.getHeader("User-Agent")); // 로그인 실패 시 세션 정보 로깅 logLoginFailure(request, username, exception);