SSO 로그인 대응

This commit is contained in:
Rinjae
2026-01-13 20:32:00 +09:00
parent 7ae5958b7c
commit 6e17ec76ab
4 changed files with 322 additions and 46 deletions
+1
View File
@@ -245,3 +245,4 @@ eapim-admin-kjb/
.claude
*.report.md
*.claude.md
tmpclaude-*-cwd
@@ -205,6 +205,11 @@
<div style="text-align: right !important;" class="form-group">
<a style="font-size: 12px !important;" href="#pwdChgModal" data-toggle="modal" >Password Change</a>
</div>
<!-- SSO 로그인 버튼 -->
<div class="form-group">
<input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3"
value="SSO Login" onclick="location.href='<c:url value="/sso/login.do"/>'">
</div>
<p style="color:red;"><%=resultMsg %></p>
</form>
@@ -118,5 +118,20 @@ public class UserInfoService extends
return repository.findAll(predicate, pageable);
}
/**
* 대소문자 구분 없이 사용자 ID로 검색
* @param userId 사용자 ID
* @return 사용자 정보 (Optional)
*/
public Optional<UserInfo> findByUserIdIgnoreCase(String userId) {
QUserInfo qUserInfo = QUserInfo.userInfo;
UserInfo result = getJPAQueryFactory()
.selectFrom(qUserInfo)
.where(qUserInfo.userid.equalsIgnoreCase(userId))
.fetchOne();
return Optional.ofNullable(result);
}
}
@@ -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,37 +43,47 @@ 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;
// SSO 검증 실패 시 에러 메시지 설정 및 로그인 페이지로 이동
session.setAttribute("resultMsg", "SSO 로그인을 실패 하였습니다. 다른 로그인 방식을 시도 하세요");
return "redirect:/loginForm.do";
}
if (StringUtils.isEmpty(ssoId)) {
throw new IllegalStateException("KJB-SSO - ssoId 획득 실패");
session.setAttribute("resultMsg", "SSO 인증 정보를 획득하지 못했습니다.");
return "redirect:/loginForm.do";
}
// 기본 정보
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");
// 필수 필드 검증
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<UserInfo> 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<UserRole> userRoles = userRoleService.findByUserId(userInfo.getUserid());
List<String> 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);
}
}