diff --git a/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java b/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java index 649f1a2..417f684 100644 --- a/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java +++ b/src/main/java/com/eactive/apim/portal/apps/apis/controller/ApiController.java @@ -8,6 +8,7 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch; import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO; import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService; import com.eactive.apim.portal.common.exception.NotFoundException; +import com.eactive.apim.portal.common.util.SecurityUtil; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -68,6 +69,10 @@ public class ApiController { @GetMapping("/testbed/api") public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) { + // 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도. + if (!SecurityUtil.isAuthenticated()) { + return "redirect:/login?reason=auth"; + } String selectedApiServiceName = "API 서비스 선택"; String selectedServiceId = ""; boolean idExists = false; @@ -93,6 +98,10 @@ public class ApiController { @GetMapping("/testbed") public String testbedByApiService(@RequestParam(value = "id", required = false) String id, Model model) { + // 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도. + if (!SecurityUtil.isAuthenticated()) { + return "redirect:/login?reason=auth"; + } String selectedApiServiceName = "API 서비스 선택"; boolean idExists = false; diff --git a/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java b/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java index 980c66f..ba0da58 100644 --- a/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java +++ b/src/main/java/com/eactive/apim/portal/apps/session/service/UserSessionService.java @@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.session.service; import com.eactive.apim.portal.apps.session.entity.UserSession; import com.eactive.apim.portal.apps.session.repository.UserSessionRepository; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -65,7 +66,8 @@ public class UserSessionService { .forceLogout("N") .build(); userSessionRepository.save(session); - log.info("세션 등록 - loginId: {}, sessionId: {}, ip: {}", loginId, sessionId, ipAddress); + log.info("세션 등록 - loginId: {}, sessionId: {}, ip: {}", + StringMaskingUtil.maskLoginId(loginId), StringMaskingUtil.maskToken(sessionId), StringMaskingUtil.maskIpAddress(ipAddress)); } /** @@ -75,7 +77,7 @@ public class UserSessionService { public void forceLogoutOtherSessions(String loginId, String currentSessionId) { int count = userSessionRepository.forceLogoutOtherSessions(loginId, currentSessionId); if (count > 0) { - log.info("강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", loginId, count); + log.info("강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", StringMaskingUtil.maskLoginId(loginId), count); } } @@ -87,7 +89,7 @@ public class UserSessionService { public void forceLogoutAllSessions(String loginId) { int count = userSessionRepository.forceLogoutAllSessions(loginId); if (count > 0) { - log.info("전체 강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", loginId, count); + log.info("전체 강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", StringMaskingUtil.maskLoginId(loginId), count); } } @@ -108,7 +110,7 @@ public class UserSessionService { public void removeSession(String sessionId) { if (userSessionRepository.existsById(sessionId)) { userSessionRepository.deleteById(sessionId); - log.debug("세션 삭제 - sessionId: {}", sessionId); + log.debug("세션 삭제 - sessionId: {}", StringMaskingUtil.maskToken(sessionId)); } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/controller/UserManRestController.java b/src/main/java/com/eactive/apim/portal/apps/user/controller/UserManRestController.java index 418ab16..3af6d6c 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/controller/UserManRestController.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/controller/UserManRestController.java @@ -74,11 +74,11 @@ public class UserManRestController { return new ResponseDTO(200, "SUCCESS", "변경되었습니다. 확인 버튼을 누르시면 계정을 로그아웃 합니다."); } - // 관리자 -> 이용자 변경 (본인 제외) + // 관리자 -> 개발자 변경 (본인 제외) @PostMapping("/revoke-manager") public ResponseDTO revokeManager(@RequestBody PortalUserDTO user) { userManFacade.revokeManager(SecurityUtil.getPortalAuthenticatedUser(), user.getId()); - return new ResponseDTO(200, "SUCCESS", "이용자로 변경되었습니다."); + return new ResponseDTO(200, "SUCCESS", "개발자로 변경되었습니다."); } // 소속 제외 -> 개인이용자로 전환 diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java index 66ce1bf..6d16ca1 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserFacadeImpl.java @@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.user.facade; import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade; import com.eactive.apim.portal.apps.user.dto.PortalUserDTO; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper; import com.eactive.apim.portal.apps.user.service.PasswordService; import com.eactive.apim.portal.apps.user.service.PortalOrgService; @@ -71,7 +72,7 @@ public class UserFacadeImpl implements UserFacade { updateUserBasicInfo(user, portalUserDTO); portalUserService.updateUser(user); - log.info("사용자 정보 업데이트 완료: {}", user.getLoginId()); + log.info("사용자 정보 업데이트 완료: {}", StringMaskingUtil.maskLoginId(user.getLoginId())); } @Override @@ -98,7 +99,7 @@ public class UserFacadeImpl implements UserFacade { portalUserService.updateUser(user); portalOrgService.updateOrg(org); - log.info("법인 관리자 정보 업데이트 완료: {}", user.getLoginId()); + log.info("법인 관리자 정보 업데이트 완료: {}", StringMaskingUtil.maskLoginId(user.getLoginId())); } // 사용자 기본 업데이트 @@ -136,7 +137,7 @@ public class UserFacadeImpl implements UserFacade { // 법인 관리자 탈퇴 제한 if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_CORP_MANAGER) { if(portalUserService.checkOrgHasOtherUsers(user.getPortalOrg())){ - throw new IllegalArgumentException("법인 관리자권한을 다른 이용자에게 위임하신 후 탈퇴가 가능합니다."); + throw new IllegalArgumentException("법인 관리자권한을 다른 개발자에게 위임하신 후 탈퇴가 가능합니다."); } } @@ -147,7 +148,7 @@ public class UserFacadeImpl implements UserFacade { messageRequestFacade.deleteUserMessage(user.getUserName(),user.getLoginId()); portalUserService.deleteUser(user); - log.info("회원 탈퇴 처리 완료: {}", user.getLoginId()); + log.info("회원 탈퇴 처리 완료: {}", StringMaskingUtil.maskLoginId(user.getLoginId())); } // 사용자 정보 업데이트 유효성 확인 @@ -215,6 +216,6 @@ public class UserFacadeImpl implements UserFacade { user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE); portalUserService.save(user); - log.info("사용자 이메일 인증 완료 - 활성화: {}", email); + log.info("사용자 이메일 인증 완료 - 활성화: {}", StringMaskingUtil.maskEmail(email)); } } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java index ae663dc..2bb75d0 100644 --- a/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java +++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/UserManFacade.java @@ -22,6 +22,8 @@ import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus; import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.apps.user.service.PortalUserAuthService; +import com.eactive.apim.portal.apps.user.service.UserRoleHistoryService; +import com.eactive.apim.portal.apps.user.service.UserRoleHistoryService.ChangeType; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; import com.eactive.apim.portal.template.service.MessageHandlerService; @@ -59,6 +61,7 @@ public class UserManFacade { private final CellPhoneValidator cellPhoneValidator; private final UserSessionService userSessionService; private final PortalUserAuthService portalUserAuthService; + private final UserRoleHistoryService userRoleHistoryService; public Page getUsers(PortalOrgDTO userOrg, Pageable pageable) { return portalUserRepository @@ -134,7 +137,7 @@ public class UserManFacade { if (existingUser.isPresent()) { PortalUser user = existingUser.get(); if (user.getPortalOrg() != null && !user.getRoleCode().equals(RoleCode.ROLE_USER)) { - throw new IllegalArgumentException("기관에 등록된 이용자 입니다."); + throw new IllegalArgumentException("기관에 등록된 개발자 입니다."); } } @@ -240,6 +243,9 @@ public class UserManFacade { PortalUser currentUser = portalUserRepository.findById(portalAuthenticatedUser.getId()) .orElseThrow(() -> new NotFoundException("사용자를 찾을 수 없습니다. ")); + RoleCode targetBefore = targetUser.getRoleCode(); + RoleCode currentBefore = currentUser.getRoleCode(); + // 3. Assign role code ROLE_CORP_MANAGER to target User targetUser.setRoleCode(RoleCode.ROLE_CORP_MANAGER); @@ -249,6 +255,10 @@ public class UserManFacade { portalUserRepository.save(targetUser); portalUserRepository.save(currentUser); + // 역할 변경 감사 이력: 대상 위임 + 본인 회수 + userRoleHistoryService.record(targetUser.getLoginId(), targetBefore, RoleCode.ROLE_CORP_MANAGER, ChangeType.MANAGER_ASSIGN); + userRoleHistoryService.record(currentUser.getLoginId(), currentBefore, RoleCode.ROLE_CORP_USER, ChangeType.MANAGER_REVOKE); + // 권한 변경 즉시 반영: 대상(타 세션)은 강제 로그아웃, 본인(현재 세션)은 in-place 재인증 userSessionService.forceLogoutAllSessions(targetUser.getLoginId()); portalUserAuthService.reloadCurrentAuthentication(); @@ -264,7 +274,7 @@ public class UserManFacade { } /** - * 법인 관리자 권한 회수 (관리자 → 이용자). + * 법인 관리자 권한 회수 (관리자 → 개발자). * 대상은 본인이 아닌 같은 기관의 ROLE_CORP_MANAGER 여야 한다. */ public void revokeManager(PortalAuthenticatedUser admin, String targetUserId) { @@ -279,12 +289,16 @@ public class UserManFacade { throw new IllegalArgumentException("본인의 권한은 변경할 수 없습니다."); } if (targetUser.getRoleCode() != RoleCode.ROLE_CORP_MANAGER) { - throw new IllegalArgumentException("관리자만 이용자로 변경할 수 있습니다."); + throw new IllegalArgumentException("관리자만 개발자로 변경할 수 있습니다."); } + RoleCode revokeBefore = targetUser.getRoleCode(); targetUser.setRoleCode(RoleCode.ROLE_CORP_USER); portalUserRepository.save(targetUser); + // 역할 변경 감사 이력: 관리자 → 개발자 + userRoleHistoryService.record(targetUser.getLoginId(), revokeBefore, RoleCode.ROLE_CORP_USER, ChangeType.MANAGER_REVOKE); + // 권한 회수를 대상 사용자의 활성 세션에 반영 (강제 로그아웃 → 재로그인 시 새 권한) userSessionService.forceLogoutAllSessions(targetUser.getLoginId()); } @@ -305,10 +319,14 @@ public class UserManFacade { throw new IllegalArgumentException("본인은 소속에서 제외할 수 없습니다."); } + RoleCode removeBefore = targetUser.getRoleCode(); targetUser.setPortalOrg(null); targetUser.setRoleCode(RoleCode.ROLE_USER); portalUserRepository.save(targetUser); + // 역할 변경 감사 이력: 소속 제외 → 개인 전환 + userRoleHistoryService.record(targetUser.getLoginId(), removeBefore, RoleCode.ROLE_USER, ChangeType.ORG_REMOVE); + // 소속 제외를 대상 사용자의 활성 세션에 반영 (강제 로그아웃 → 재로그인 시 새 권한) userSessionService.forceLogoutAllSessions(targetUser.getLoginId()); } diff --git a/src/main/java/com/eactive/apim/portal/apps/user/service/UserRoleHistoryService.java b/src/main/java/com/eactive/apim/portal/apps/user/service/UserRoleHistoryService.java new file mode 100644 index 0000000..b6e74ca --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/apps/user/service/UserRoleHistoryService.java @@ -0,0 +1,62 @@ +package com.eactive.apim.portal.apps.user.service; + +import com.eactive.apim.portal.common.util.SecurityUtil; +import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode; +import com.eactive.apim.portal.portaluser.entity.UserRoleHistory; +import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * 사용자 역할(권한) 변경 감사 이력 기록 서비스. + * + *

법인 관리자 위임/회수, 소속 제외 등 역할 변경 이벤트를 {@code ptl_user_role_history} 에 남긴다. + * 변경 수행자(changedBy)는 현재 인증된 관리자의 loginId 로 기록한다.

+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserRoleHistoryService { + + /** 역할 변경 유형. */ + public enum ChangeType { + MANAGER_ASSIGN, // 관리자 권한 위임 + MANAGER_REVOKE, // 관리자 권한 회수 + ORG_REMOVE // 소속 제외(개인으로 전환) + } + + private final UserRoleHistoryRepository userRoleHistoryRepository; + + /** + * 역할 변경 이력을 기록한다. 감사 목적이므로 실패해도 본 트랜잭션을 롤백시키지 않도록 + * 호출부에서 예외를 전파하지 않는다(내부에서 로깅만). + */ + @Transactional + public void record(String targetLoginId, RoleCode before, RoleCode after, ChangeType changeType) { + try { + String actor = SecurityUtil.getCurrentLoginId(); + if (actor == null || actor.isEmpty()) { + actor = "SYSTEM"; + } + LocalDateTime now = LocalDateTime.now(); + + UserRoleHistory history = new UserRoleHistory(); + history.setUserId(targetLoginId); + history.setBeforeRole(before != null ? before.name() : null); + history.setAfterRole(after != null ? after.name() : null); + history.setChangeType(changeType.name()); + history.setChangedBy(actor); + history.setChangeDate(now); + history.setCreatedBy(actor); + history.setCreatedDate(now); + + userRoleHistoryRepository.save(history); + } catch (Exception e) { + log.error("역할 변경 이력 기록 실패 - target: {}, type: {}", targetLoginId, changeType, e); + } + } +} diff --git a/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java b/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java index 8f3d8f4..96537c2 100644 --- a/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java +++ b/src/main/java/com/eactive/apim/portal/common/exception/PortalGlobalExceptionHandler.java @@ -8,6 +8,7 @@ import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import com.eactive.apim.portal.common.util.SecurityUtil; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.config.PortalProperties; import com.eactive.apim.portal.file.exception.InvalidFileException; import lombok.RequiredArgsConstructor; @@ -50,16 +51,16 @@ public class PortalGlobalExceptionHandler { @ExceptionHandler(value = UserNotLoginException.class) public ModelAndView handleUserNotLoginException(HttpServletRequest request, UserNotLoginException ex) { - return new ModelAndView("redirect:/login"); + return new ModelAndView("redirect:/login?reason=auth"); } @ExceptionHandler(value = AccessDeniedException.class) public ModelAndView handleAccessDeniedException(HttpServletRequest request, AccessDeniedException ex) { // 미로그인 사용자는 로그인 페이지로 유도, 로그인 상태에서의 권한 부족은 오류 안내 페이지로 표시한다. if (!SecurityUtil.isAuthenticated()) { - return new ModelAndView("redirect:/login"); + return new ModelAndView("redirect:/login?reason=auth"); } - log.warn("접근 권한 없음: loginId={}, uri={}", SecurityUtil.getCurrentLoginId(), request.getRequestURI()); + log.warn("접근 권한 없음: loginId={}, uri={}", StringMaskingUtil.maskLoginId(SecurityUtil.getCurrentLoginId()), request.getRequestURI()); ModelAndView modelAndView = new ModelAndView("error"); modelAndView.addObject("errorTitle", "페이지 접근 권한이 없습니다."); modelAndView.addObject("errorDescription", "해당 페이지를 이용할 수 있는 권한이 없는 계정입니다.\n권한이 필요한 경우 관리자에게 문의해 주세요."); diff --git a/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java b/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java index a56cdc5..c539cbc 100644 --- a/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java +++ b/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java @@ -1,5 +1,6 @@ package com.eactive.apim.portal.common.migration; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; @@ -177,7 +178,8 @@ public class LegacyEncryptionMigrationController { || "::1".equals(remote); boolean viaProxy = request.getHeader("X-Forwarded-For") != null; if (!localAddr || viaProxy) { - log.warn("[마이그레이션] 비로컬 접근 차단 remoteAddr={} xff={}", remote, request.getHeader("X-Forwarded-For")); + log.warn("[마이그레이션] 비로컬 접근 차단 remoteAddr={} xff={}", + StringMaskingUtil.maskIpAddress(remote), StringMaskingUtil.maskIpAddress(request.getHeader("X-Forwarded-For"))); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "localhost(127.0.0.1) 직접 호출만 허용됩니다."); } } diff --git a/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java b/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java index 857122b..a52af16 100644 --- a/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java +++ b/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java @@ -80,6 +80,98 @@ public class StringMaskingUtil { return number; } + /** + * 로그인 ID 마스킹 처리 (로그 출력용). + * 이메일 형식이면 {@link #maskEmail(String)} 규칙을, 그 외(스태프 등 비이메일 ID)는 부분 마스킹을 적용한다. + */ + public static String maskLoginId(String loginId) { + if (!isValidString(loginId)) { + return loginId; + } + if (loginId.contains("@")) { + return maskEmail(loginId); + } + int len = loginId.length(); + if (len <= 2) { + return stars(len); + } + if (len <= 4) { + return loginId.charAt(0) + stars(len - 1); + } + // 5자 이상: 앞2 + 마스킹 + 뒤1 + return loginId.substring(0, 2) + stars(len - 3) + loginId.charAt(len - 1); + } + + /** + * IP 주소 마스킹 처리 (로그 출력용). 첫 옥텟만 남기고 나머지를 마스킹한다. + *
127.0.0.1 → 127.***.***.***
+ * IPv4 형식이 아니면 원본을 반환한다(멱등). + */ + public static String maskIpAddress(String ip) { + if (!isValidString(ip)) { + return ip; + } + String[] parts = ip.split("\\."); + if (parts.length != 4) { + return ip; + } + return parts[0] + ".***.***.***"; + } + + /** + * 토큰/세션ID 등 식별자 마스킹 처리 (로그 출력용). 앞 4자 + {@code ***} + 뒤 4자만 노출한다. + * 길이가 짧으면(9자 미만) 전체를 마스킹한다. + */ + public static String maskToken(String token) { + if (!isValidString(token)) { + return token; + } + int len = token.length(); + if (len < 9) { + return stars(len); + } + return token.substring(0, 4) + "***" + token.substring(len - 4); + } + + // 로그에 값을 그대로 남기면 안 되는 민감 헤더(소문자 비교) + private static final java.util.Set SENSITIVE_HEADERS = new java.util.HashSet<>(Arrays.asList( + "cookie", "set-cookie", "authorization", "proxy-authorization", + "x-auth-token", "x-csrf-token", "x-xsrf-token", "x-api-key")); + + private static final String REDACTED = "***REDACTED***"; + + /** + * 요청/응답 헤더 로깅 시 민감 헤더(Cookie/Authorization 등)의 값을 {@code ***REDACTED***} 로 치환한다. + * 그 외 헤더는 원본 값을 반환한다. + */ + public static String maskHeaderValue(String headerName, String headerValue) { + if (headerName != null && SENSITIVE_HEADERS.contains(headerName.toLowerCase())) { + return REDACTED; + } + return headerValue; + } + + // 세션 속성 이름에 포함되면 값을 리댁트할 키워드(소문자 부분일치) + private static final String[] SENSITIVE_ATTRIBUTE_KEYWORDS = { + "token", "secret", "password", "credential", "csrf", + "security_context", "loginid", "authentication"}; + + /** + * 세션 속성 로깅 시 민감 속성(SPRING_SECURITY_CONTEXT, token, loginId 등)의 값을 리댁트한다. + * 속성 이름에 민감 키워드가 부분일치하면 {@code ***REDACTED***} 를 반환한다. + */ + public static String maskAttributeValue(String attrName, String value) { + if (attrName != null) { + String lower = attrName.toLowerCase(); + for (String kw : SENSITIVE_ATTRIBUTE_KEYWORDS) { + if (lower.contains(kw)) { + return REDACTED; + } + } + } + return value; + } + // 기존 메서드 오버로딩 (하위 호환성) public static String maskName(String name) { return maskName(name, null, null); 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 acf4c30..9330878 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationFailureHandler.java @@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.login.constants.LoginConstants; import com.eactive.apim.portal.apps.user.service.PortalUserLogService; import com.eactive.apim.portal.common.exception.UserNotFoundException; import com.eactive.apim.portal.common.util.HttpRequestUtil; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.common.util.StringRepeatUtil; import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.portaluser.repository.PortalUserRepository; @@ -83,7 +84,7 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure } catch (UserNotFoundException e) { - logger.error("{} login try {}", username, e.getMessage()); + logger.error("{} login try {}", StringMaskingUtil.maskLoginId(username), e.getMessage()); } } @@ -105,18 +106,18 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); logMessage.append("USER LOGIN FAILURE\n"); logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); - logMessage.append("Username: ").append(username).append("\n"); - logMessage.append("Session ID: ").append(request.getSession().getId()).append("\n"); + logMessage.append("Username: ").append(StringMaskingUtil.maskLoginId(username)).append("\n"); + logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(request.getSession().getId())).append("\n"); logMessage.append("Failed At: ").append(LocalDateTime.now().format(formatter)).append("\n"); logMessage.append("Failure Reason: ").append(exception.getLocalizedMessage()).append("\n"); logMessage.append("\n"); logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); logMessage.append("REQUEST INFORMATION\n"); logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); - logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n"); + logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n"); logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n"); logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n"); - logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n"); + logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n"); logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n"); logMessage.append("Request Method: ").append(request.getMethod()).append("\n"); logMessage.append("Request URI: ").append(request.getRequestURI()).append("\n"); @@ -133,7 +134,7 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure java.util.Enumeration headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { String headerValue = headerValues.nextElement(); - logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue)); + logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue))); } } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java index d6fcc75..7896b0d 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalAuthenticationSuccessHandler.java @@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository; import com.eactive.apim.portal.apps.user.service.PortalUserLogService; import com.eactive.apim.portal.common.util.HttpRequestUtil; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.common.util.PhoneNumberUtil; import com.eactive.apim.portal.common.util.StringRepeatUtil; import com.eactive.apim.portal.invitation.entity.UserInvitation; @@ -150,17 +151,17 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); logMessage.append("USER LOGIN SUCCESS\n"); logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); - logMessage.append("Username: ").append(username).append("\n"); - logMessage.append("Session ID: ").append(session.getId()).append("\n"); + logMessage.append("Username: ").append(StringMaskingUtil.maskLoginId(username)).append("\n"); + logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n"); logMessage.append("Login At: ").append(LocalDateTime.now().format(formatter)).append("\n"); logMessage.append("\n"); logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); logMessage.append("REQUEST INFORMATION\n"); logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); - logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n"); + logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n"); logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n"); logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n"); - logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n"); + logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n"); logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n"); logMessage.append("Request Method: ").append(request.getMethod()).append("\n"); logMessage.append("Request URI: ").append(request.getRequestURI()).append("\n"); @@ -177,7 +178,7 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess java.util.Enumeration headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { String headerValue = headerValues.nextElement(); - logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue)); + logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue))); } } diff --git a/src/main/java/com/eactive/apim/portal/config/PortalLogoutSuccessHandler.java b/src/main/java/com/eactive/apim/portal/config/PortalLogoutSuccessHandler.java index d06b7c3..74cd57e 100644 --- a/src/main/java/com/eactive/apim/portal/config/PortalLogoutSuccessHandler.java +++ b/src/main/java/com/eactive/apim/portal/config/PortalLogoutSuccessHandler.java @@ -2,6 +2,7 @@ package com.eactive.apim.portal.config; import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.common.util.HttpRequestUtil; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.common.util.StringRepeatUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,11 +57,11 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); logMessage.append("USER LOGOUT\n"); logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); - logMessage.append("Session ID: ").append(session.getId()).append("\n"); + logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n"); logMessage.append("Logout At: ").append(LocalDateTime.now().format(formatter)).append("\n"); if (authentication != null) { - logMessage.append("Username: ").append(authentication.getName()).append("\n"); + logMessage.append("Username: ").append(StringMaskingUtil.maskLoginId(authentication.getName())).append("\n"); logMessage.append("Authenticated: ").append(authentication.isAuthenticated()).append("\n"); } @@ -68,10 +69,10 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); logMessage.append("REQUEST INFORMATION\n"); logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); - logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n"); + logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n"); logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n"); logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n"); - logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n"); + logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n"); logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n"); logMessage.append("Request Method: ").append(request.getMethod()).append("\n"); logMessage.append("Request URI: ").append(request.getRequestURI()).append("\n"); @@ -88,7 +89,7 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH java.util.Enumeration headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { String headerValue = headerValues.nextElement(); - logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue)); + logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue))); } } @@ -102,7 +103,7 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH while (attributeNames.hasMoreElements()) { String attrName = attributeNames.nextElement(); Object attrValue = session.getAttribute(attrName); - String valueStr = attrValue != null ? attrValue.toString() : "null"; + String valueStr = StringMaskingUtil.maskAttributeValue(attrName, attrValue != null ? attrValue.toString() : "null"); if (valueStr.length() > 100) { valueStr = valueStr.substring(0, 97) + "..."; } diff --git a/src/main/java/com/eactive/apim/portal/config/SessionLoggingListener.java b/src/main/java/com/eactive/apim/portal/config/SessionLoggingListener.java index 91154df..fe80883 100644 --- a/src/main/java/com/eactive/apim/portal/config/SessionLoggingListener.java +++ b/src/main/java/com/eactive/apim/portal/config/SessionLoggingListener.java @@ -1,6 +1,7 @@ package com.eactive.apim.portal.config; import com.eactive.apim.portal.common.util.HttpRequestUtil; +import com.eactive.apim.portal.common.util.StringMaskingUtil; import com.eactive.apim.portal.common.util.StringRepeatUtil; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; @@ -48,7 +49,7 @@ public class SessionLoggingListener implements HttpSessionListener { logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); logMessage.append("NEW HTTP SESSION CREATED\n"); logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); - logMessage.append("Session ID: ").append(session.getId()).append("\n"); + logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n"); logMessage.append("Created At: ").append(LocalDateTime.now().format(formatter)).append("\n"); logMessage.append("Max Inactive Interval: ").append(session.getMaxInactiveInterval()).append(" seconds\n"); @@ -57,10 +58,10 @@ public class SessionLoggingListener implements HttpSessionListener { logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); logMessage.append("REQUEST INFORMATION\n"); logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n"); - logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n"); + logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n"); logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n"); logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n"); - logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n"); + logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n"); logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n"); logMessage.append("Remote Port: ").append(request.getRemotePort()).append("\n"); logMessage.append("Request Method: ").append(request.getMethod()).append("\n"); @@ -84,7 +85,7 @@ public class SessionLoggingListener implements HttpSessionListener { Enumeration headerValues = request.getHeaders(headerName); while (headerValues.hasMoreElements()) { String headerValue = headerValues.nextElement(); - logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue)); + logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue))); } } } else { @@ -106,7 +107,7 @@ public class SessionLoggingListener implements HttpSessionListener { logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); logMessage.append("HTTP SESSION DESTROYED\n"); logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n"); - logMessage.append("Session ID: ").append(session.getId()).append("\n"); + logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n"); logMessage.append("Destroyed At: ").append(LocalDateTime.now().format(formatter)).append("\n"); logMessage.append("Max Inactive Interval: ").append(session.getMaxInactiveInterval()).append(" seconds\n"); @@ -121,7 +122,8 @@ public class SessionLoggingListener implements HttpSessionListener { while (attributeNames.hasMoreElements()) { String attrName = attributeNames.nextElement(); Object attrValue = session.getAttribute(attrName); - logMessage.append(String.format(" %-30s : %s\n", attrName, attrValue)); + logMessage.append(String.format(" %-30s : %s\n", attrName, + StringMaskingUtil.maskAttributeValue(attrName, attrValue != null ? attrValue.toString() : "null"))); } } catch (Exception e) { logMessage.append(" Unable to retrieve session attributes\n"); diff --git a/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java b/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java index 07e3255..1eb949d 100644 --- a/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java +++ b/src/main/java/com/eactive/apim/portal/djb/webhook/controller/WebhookController.java @@ -40,12 +40,12 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; @Slf4j @Controller @RequestMapping("/webhook") -@Secured("ROLE_API_KEY_REQUEST") +@Secured("ROLE_WEBHOOK") @RequiredArgsConstructor @SessionAttributes({"webhookRegistration", "webhookModification"}) public class WebhookController { - // 클래스 기본: 법인 관리자(ROLE_API_KEY_REQUEST)만 신청/수정/삭제/Secret 관리 가능. - // 조회(index)만 메서드 레벨에서 ROLE_APP 으로 완화(같은 기관 일반 사용자도 열람). + // Webhook 관리는 법인 관리자 전용(ROLE_WEBHOOK). 조회/신청/수정/삭제/Secret 관리 모두 관리자만 가능. + // (개발자 ROLE_CORP_USER 의 앱 권한 확장과 분리하기 위해 ROLE_API_KEY_REQUEST 에서 별도 authority 로 격리) private static final int TOTAL_STEPS = 3; @@ -66,7 +66,7 @@ public class WebhookController { // ============================ 조회 ============================ - @Secured("ROLE_APP") + @Secured("ROLE_WEBHOOK") @GetMapping public ModelAndView index() { String orgId = currentOrgId(); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c926b0e..9c9fdf6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -224,12 +224,15 @@ portal: - ROLE_INQUIRY - ROLE_ACCOUNT ROLE_CORP_USER: + - ROLE_API_KEY_REQUEST + - ROLE_API_KEY_REQUEST_VIEW - ROLE_INQUIRY - ROLE_APP - ROLE_ACCOUNT ROLE_CORP_MANAGER: - ROLE_API_KEY_REQUEST - ROLE_API_KEY_REQUEST_VIEW + - ROLE_WEBHOOK - ROLE_INQUIRY - ROLE_APP - ROLE_ACCOUNT @@ -355,10 +358,10 @@ page: name: "이메일 인증" path: "/mypage/verification-email" user_list: - name: "이용자 관리" + name: "개발자 관리" path: "/users" user_detail: - name: "이용자 정보" + name: "개발자 정보" path: "/users/detail" apikey: name: "앱 관리" diff --git a/src/main/resources/message/apim/message_en.properties b/src/main/resources/message/apim/message_en.properties index 0db3ecc..a716e1c 100644 --- a/src/main/resources/message/apim/message_en.properties +++ b/src/main/resources/message/apim/message_en.properties @@ -82,6 +82,6 @@ status=Status table.reger=Registrar zip=Zip Code ROLE_CORP_MANAGER=Manager -ROLE_CORP_USER=User +ROLE_CORP_USER=Developer ROLE_USER=Individual login.passLengthShort=Password must be over 8 characters. diff --git a/src/main/resources/message/apim/message_ko.properties b/src/main/resources/message/apim/message_ko.properties index b6209c7..58d42cc 100644 --- a/src/main/resources/message/apim/message_ko.properties +++ b/src/main/resources/message/apim/message_ko.properties @@ -82,6 +82,6 @@ status=\uC0C1\uD0DC table.reger=\uB4F1\uB85D\uC790 zip=\uC6B0\uD3B8\uBC88\uD638 ROLE_CORP_MANAGER=\uAD00\uB9AC\uC790 -ROLE_CORP_USER=\uC774\uC6A9\uC790 +ROLE_CORP_USER=\uAC1C\uBC1C\uC790 ROLE_USER=\uAC1C\uC778\uC0AC\uC6A9\uC790 ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\uCD5C\uB300 \uB85C\uADF8\uC778 \uD5C8\uC6A9 \uC5F0\uACB0\uC744 \uCD08\uACFC\uD558\uC600\uC2B5\uB2C8\uB2E4. diff --git a/src/main/resources/templates/views/apps/login/login.html b/src/main/resources/templates/views/apps/login/login.html index 9b82315..60b54ac 100644 --- a/src/main/resources/templates/views/apps/login/login.html +++ b/src/main/resources/templates/views/apps/login/login.html @@ -27,6 +27,10 @@ 다른 기기 또는 브라우저에서 로그인되어 현재 세션이 종료되었습니다. +