diff --git a/.gitignore b/.gitignore index 2adda4b..41a9225 100644 --- a/.gitignore +++ b/.gitignore @@ -244,4 +244,5 @@ gradle-gf63.sh eapim-admin-kjb/ .claude *.report.md -*.claude.md \ No newline at end of file +*.claude.md +tmpclaude-*-cwd diff --git a/WebContent/jsp/common/screen/emergency.jsp b/WebContent/jsp/common/screen/emergency.jsp index 5ac9ffa..4d747d2 100644 --- a/WebContent/jsp/common/screen/emergency.jsp +++ b/WebContent/jsp/common/screen/emergency.jsp @@ -205,6 +205,11 @@
Password Change
+ +
+ '"> +

<%=resultMsg %>

diff --git a/src/main/java/com/eactive/eai/rms/data/entity/man/user/UserInfoService.java b/src/main/java/com/eactive/eai/rms/data/entity/man/user/UserInfoService.java index 3610129..6254ea8 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/man/user/UserInfoService.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/man/user/UserInfoService.java @@ -118,5 +118,20 @@ public class UserInfoService extends return repository.findAll(predicate, pageable); } + /** + * 대소문자 구분 없이 사용자 ID로 검색 + * @param userId 사용자 ID + * @return 사용자 정보 (Optional) + */ + public Optional findByUserIdIgnoreCase(String userId) { + QUserInfo qUserInfo = QUserInfo.userInfo; + + UserInfo result = getJPAQueryFactory() + .selectFrom(qUserInfo) + .where(qUserInfo.userid.equalsIgnoreCase(userId)) + .fetchOne(); + + return Optional.ofNullable(result); + } } \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java b/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java index 3a8ca01..594d1bb 100644 --- a/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java +++ b/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java @@ -1,7 +1,22 @@ package com.eactive.eai.rms.ext.kjb.sso; +import com.eactive.apim.portal.user.entity.UserInfo; +import com.eactive.eai.common.seed.Seed; +import com.eactive.eai.rms.common.context.MonitoringContext; +import com.eactive.eai.rms.common.datasource.DataSourceType; +import com.eactive.eai.rms.common.datasource.DataSourceTypeManager; import com.eactive.eai.rms.common.interceptor.InterceptorSkipController; +import com.eactive.eai.rms.common.login.LoginVo; +import com.eactive.eai.rms.common.login.SessionManager; +import com.eactive.eai.rms.common.util.CommonConstants; +import com.eactive.eai.rms.common.util.IpUtil; import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService; +import com.eactive.eai.rms.data.entity.man.user.UserInfoService; +import com.eactive.eai.rms.data.entity.man.user.UserRole; +import com.eactive.eai.rms.data.entity.man.user.UserRoleId; +import com.eactive.eai.rms.data.entity.man.user.UserRoleService; +import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService; +import com.eactive.eai.rms.onl.apim.masking.MaskingUtils; import com.eactive.ext.kjb.common.KjbProperty; import com.eactive.ext.kjb.sso.KjbSsoModule; import com.eactive.ext.kjb.util.KjbPropertyInjector; @@ -14,7 +29,12 @@ import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; import java.util.Properties; +import java.util.stream.Collectors; @Slf4j @Controller @@ -23,36 +43,46 @@ public class SsoController implements InterceptorSkipController { private final KjbSsoModule ssoModule; private final KjbProperty prop; + private final UserInfoService userInfoService; + private final UserRoleService userRoleService; + private final UserServiceTypeService userServiceTypeService; + private final MonitoringContext monitoringContext; - - public SsoController(MonitoringPropertyService monitoringPropertyService) { + public SsoController(MonitoringPropertyService monitoringPropertyService, + UserInfoService userInfoService, + UserRoleService userRoleService, + UserServiceTypeService userServiceTypeService, + MonitoringContext monitoringContext) { KjbPropertyInjector kjbPropertyInjector = new KjbPropertyInjector(monitoringPropertyService, PROP_GORUP_ID); prop = kjbPropertyInjector.inject(new KjbProperty()); this.ssoModule = KjbSsoModule.getInstance(prop); + this.userInfoService = userInfoService; + this.userRoleService = userRoleService; + this.userServiceTypeService = userServiceTypeService; + this.monitoringContext = monitoringContext; } @RequestMapping(value = "/sso/login.do") public String login(HttpServletRequest req, HttpServletResponse res, - @RequestParam(name = "UURL", required = false) String uurl) throws Exception { + @RequestParam(name = "UURL", required = false) String uurl) throws Exception { + HttpSession session = req.getSession(); String ssoId = ssoModule.getSsoId(req); log.debug("ssoId : {}", ssoId); log.debug("uurl : {}", uurl); - log.debug("KjbProperty : \n{}", prop.toJsonString(true)); - - // 디버깅용 쿠키 목록 출력 - Cookie[] cookies = req.getCookies(); - if (cookies == null || cookies.length == 0) { - log.debug("No cookies"); - } else { - for (Cookie c: cookies) { - log.debug("Cookie[{}] : {}, Domain={}, Path={}, Max-Age={}, Secure={}, Version={}", - c.getName(), c.getValue(), c.getDomain(), c.getPath(), c.getMaxAge(), c.getSecure(), c.getVersion()); + if (log.isDebugEnabled()) { + Cookie[] cookies = req.getCookies(); + if (cookies == null || cookies.length == 0) { + log.debug("No cookies"); + } else { + for (Cookie c : cookies) { + log.debug("Cookie[{}] : Domain={}, Path={}", + c.getName(), c.getDomain(), c.getPath()); + } } } - if (StringUtils.isEmpty(uurl)) { uurl = ssoModule.getAscpUrl(); @@ -62,45 +92,270 @@ public class SsoController implements InterceptorSkipController { ssoModule.goLoginPage(res, uurl); return null; } else { - + // EAM 세션 검증 String retCode = ssoModule.getEamSessionCheck(req, res); log.debug("retCode : {}", retCode); log.debug("Property - sso.ignore_validation = {}", prop.isSsoIgnoreValidation()); if (!prop.isSsoIgnoreValidation() && !"0".equalsIgnoreCase(retCode)) { - - ssoModule.goErrorPage(res, Integer.parseInt(retCode)); - return null; - } - - if (StringUtils.isEmpty(ssoId)) { - throw new IllegalStateException("KJB-SSO - ssoId 획득 실패"); - } - - - // 기본 정보 - Properties info = ssoModule.getUserInfos(ssoId); - if (info != null) { - log.debug("info - {}", info.keySet()); - info.keySet().forEach( k -> { - log.debug("{} : {}", k, info.get(k)); - }); - } else { - log.debug("User info is null"); - } - - // 확장 정보 - Properties extendedInfo = ssoModule.getUserExFields(ssoId); - if (extendedInfo != null) { - log.debug("extendedInfo - {}", info.keySet()); - extendedInfo.keySet().forEach( k -> { - log.debug("{} : {}", k, extendedInfo.get(k)); - }); - } else { - log.debug("User Extended info is null"); + // SSO 검증 실패 시 에러 메시지 설정 및 로그인 페이지로 이동 + session.setAttribute("resultMsg", "SSO 로그인을 실패 하였습니다. 다른 로그인 방식을 시도 하세요"); + return "redirect:/loginForm.do"; } + if (StringUtils.isEmpty(ssoId)) { + session.setAttribute("resultMsg", "SSO 인증 정보를 획득하지 못했습니다."); + return "redirect:/loginForm.do"; + } + + // 기본 정보 + Properties info = ssoModule.getUserInfos(ssoId); + // 확장 정보 + Properties extendedInfo = ssoModule.getUserExFields(ssoId); + + // 필수 필드 검증 + try { + validateRequiredFields(info, extendedInfo); + } catch (IllegalStateException e) { + log.warn("SSO 필수 필드 검증 실패: {}", e.getMessage()); + session.setAttribute("resultMsg", e.getMessage()); + return "redirect:/loginForm.do"; + } + + // 사용자 정보 로깅 (개인정보 마스킹 처리) + String userId = info.getProperty("USERID"); + String name = info.getProperty("NAME"); + String email = info.getProperty("EMAIL"); + String cellPhoneNo = extendedInfo != null ? extendedInfo.getProperty("CELLPHONENO") : null; + + log.info("SSO 로그인 시도 - userId: {}", userId); + log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}", + userId, + MaskingUtils.maskName(name), + maskPhoneNumber(cellPhoneNo), + maskEmail(email)); + + // 사용자 생성 또는 업데이트 + UserInfo userInfo = findOrCreateUser(info, extendedInfo); + + // 로그인 세션 설정 + setupLoginSession(req, userInfo); + + log.info("SSO 로그인 성공 - userId: {}", userId); return "redirect:/main.do"; } } + + /** + * 필수 필드 검증 + */ + private void validateRequiredFields(Properties info, Properties extendedInfo) throws IllegalStateException { + validateField(info, "USERID", "사용자ID"); + validateField(info, "NAME", "사용자명"); + validateField(extendedInfo, "CELLPHONENO", "휴대폰번호"); + } + + private void validateField(Properties props, String key, String fieldName) { + String value = props != null ? props.getProperty(key) : null; + if (StringUtils.isEmpty(value)) { + throw new IllegalStateException(fieldName + " - SSO 서버로 부터 전달 받지 못했습니다."); + } + } + + /** + * 사용자 조회 또는 생성 + */ + private UserInfo findOrCreateUser(Properties info, Properties extendedInfo) throws Exception { + String userId = info.getProperty("USERID"); + + // 대소문자 구분 없이 검색 + Optional existingUser = userInfoService.findByUserIdIgnoreCase(userId); + + if (existingUser.isPresent()) { + return updateUserIfNeeded(existingUser.get(), info, extendedInfo); + } else { + return createNewUser(info, extendedInfo); + } + } + + /** + * 신규 사용자 생성 + */ + private UserInfo createNewUser(Properties info, Properties extendedInfo) throws Exception { + String userId = info.getProperty("USERID"); + String name = info.getProperty("NAME"); + String email = info.getProperty("EMAIL"); + String cellPhoneNo = extendedInfo != null ? extendedInfo.getProperty("CELLPHONENO") : null; + String positionName = extendedInfo != null ? extendedInfo.getProperty("LEGACYPOSITIONNAME") : null; + String deptName = extendedInfo != null ? extendedInfo.getProperty("LEGACYDEPTNAME") : null; + + UserInfo userInfo = new UserInfo(); + userInfo.setUserid(userId); + userInfo.setUsername(name); + userInfo.setPassword(Seed.encrypt("!" + userId)); + userInfo.setCphnno(cleanPhoneNumber(cellPhoneNo)); + userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER); + + // 부가 필드 설정 (값이 있을 경우만) + if (StringUtils.isNotEmpty(email)) { + userInfo.setEmad(email); + } + if (StringUtils.isNotEmpty(positionName)) { + userInfo.setJobtlname(positionName); + } + if (StringUtils.isNotEmpty(deptName)) { + userInfo.setDvsnname(deptName); + } + + userInfoService.save(userInfo); + + // 기본 역할 할당 + String roleId = monitoringContext.getStringProperty( + MonitoringContext.USER_DEFAULT_RULE, "newbie"); + UserRole userRole = new UserRole(); + userRole.setId(new UserRoleId(userId, roleId)); + userRoleService.save(userRole); + + // 서비스 타입 할당 + DataSourceTypeManager.getDynamicDataSourceTypes().stream() + .map(DataSourceType::getName) + .forEach(type -> userServiceTypeService.insertUserServiceType(userId, type)); + + log.info("SSO 신규 사용자 생성 완료 - userId: {}", userId); + return userInfo; + } + + /** + * 기존 사용자 정보 업데이트 (변경된 필드만) + */ + private UserInfo updateUserIfNeeded(UserInfo userInfo, Properties info, Properties extendedInfo) { + boolean updated = false; + String userId = userInfo.getUserid(); + + String name = info.getProperty("NAME"); + String email = info.getProperty("EMAIL"); + String cellPhoneNo = extendedInfo != null ? extendedInfo.getProperty("CELLPHONENO") : null; + String positionName = extendedInfo != null ? extendedInfo.getProperty("LEGACYPOSITIONNAME") : null; + String deptName = extendedInfo != null ? extendedInfo.getProperty("LEGACYDEPTNAME") : null; + + // 사용자명 업데이트 + if (StringUtils.isNotEmpty(name) && !name.equals(userInfo.getUsername())) { + userInfo.setUsername(name); + updated = true; + } + + // 휴대폰번호 업데이트 + String cleanedPhone = cleanPhoneNumber(cellPhoneNo); + if (StringUtils.isNotEmpty(cleanedPhone) && !cleanedPhone.equals(userInfo.getCphnno())) { + userInfo.setCphnno(cleanedPhone); + updated = true; + } + + // 이메일 업데이트 + if (StringUtils.isNotEmpty(email) && !email.equals(userInfo.getEmad())) { + userInfo.setEmad(email); + updated = true; + } + + // 직위명 업데이트 + if (StringUtils.isNotEmpty(positionName) && !positionName.equals(userInfo.getJobtlname())) { + userInfo.setJobtlname(positionName); + updated = true; + } + + // 부서명 업데이트 + if (StringUtils.isNotEmpty(deptName) && !deptName.equals(userInfo.getDvsnname())) { + userInfo.setDvsnname(deptName); + updated = true; + } + + if (updated) { + userInfoService.save(userInfo); + log.info("SSO 사용자 정보 업데이트 완료 - userId: {}", userId); + } + + return userInfo; + } + + /** + * 로그인 세션 설정 + */ + private void setupLoginSession(HttpServletRequest request, UserInfo userInfo) { + HttpSession session = request.getSession(true); + + LoginVo dto = new LoginVo(); + dto.setUserId(userInfo.getUserid()); + dto.setUserName(userInfo.getUsername()); + dto.setDepartment(userInfo.getDvsnname()); + + // 역할 설정 + List userRoles = userRoleService.findByUserId(userInfo.getUserid()); + List roles = userRoles.stream() + .map(r -> r.getId().getRoleId()) + .collect(Collectors.toList()); + dto.setRoleList(roles); + + // 서비스타입 설정 (검증 없이 전체 허용) + dto.setUseServiceTypes(CommonConstants.USER_SERVICETYPE_NO_CHECK); + + // 세션 등록 + SessionManager.forceRegisterUserSession(request, dto); + session.setAttribute(CommonConstants.LOGIN, dto); + session.setAttribute("userId", userInfo.getUserid()); + session.setAttribute("dualLogin", "N"); + + // 대시보드 타이틀 설정 + String newTitle = monitoringContext.getStringProperty( + MonitoringContext.NEW_DASHBOARD_TITLE, "topLogo.gif"); + session.setAttribute(MonitoringContext.NEW_DASHBOARD_TITLE, newTitle); + + // 마지막 로그인 정보 업데이트 + userInfo.setLastloginyms(LocalDateTime.now()); + userInfo.setLastloginip(IpUtil.getClientIp(request)); + userInfoService.save(userInfo); + + // 세션에 마지막 로그인 정보 저장 + session.setAttribute("LastLoginYms", userInfo.getLastloginyms() + .format(com.eactive.eai.data.converter.LocalDateTimeFormatters.FORMATTER_YYYYMMDDHHMMSS_14)); + session.setAttribute("LastLoginIp", userInfo.getLastloginip()); + } + + /** + * 휴대폰번호에서 숫자만 추출 + */ + private String cleanPhoneNumber(String phone) { + if (StringUtils.isEmpty(phone)) { + return null; + } + return phone.replaceAll("[^0-9]", ""); + } + + /** + * 휴대폰번호 마스킹 (로그용) + */ + private String maskPhoneNumber(String phone) { + if (StringUtils.isEmpty(phone) || phone.length() < 4) { + return "****"; + } + // 010-1234-5678 → 010-****-5678 + String cleaned = phone.replaceAll("[^0-9]", ""); + if (cleaned.length() < 7) { + return "****"; + } + return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4); + } + + /** + * 이메일 마스킹 (로그용) + */ + private String maskEmail(String email) { + if (StringUtils.isEmpty(email) || !email.contains("@")) { + return "****"; + } + int atIndex = email.indexOf("@"); + if (atIndex <= 2) { + return "**" + email.substring(atIndex); + } + return email.substring(0, 2) + "**" + email.substring(atIndex); + } }