로그인시 비밀번호 n회 실패시 계정 잠금처리

This commit is contained in:
eastargh
2026-07-29 10:14:50 +09:00
parent 2d414dc31b
commit a4614ca7fd
8 changed files with 111 additions and 21 deletions
@@ -41,8 +41,8 @@ import lombok.RequiredArgsConstructor;
@Transactional(transactionManager = "transactionManagerForEMS")
@RequiredArgsConstructor
public class UserManService extends BaseService {
private static final String USER_STATUS_NORMAL = "1";
private final UserInfoService userInfoService;
private final UserRoleService userRoleService;
@@ -100,7 +100,8 @@ public class UserManService extends BaseService {
UserInfo userInfo = userUIMapper.toEntity(userUI);
userInfo.setRoleidnfiname(CommonConstants.DEPT_DEVELOPER);
userInfo.setStatus(USER_STATUS_NORMAL);
String subfix = monitoringContext.getStringProperty(MonitoringContext.RMS_PASSWORD_INIT_SUBFIX, "@!");
try {
@@ -133,6 +134,12 @@ public class UserManService extends BaseService {
public void update(UserUI userUI) {
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
userUIMapper.updateToEntity(userUI, userInfo);
// 계정상태를 정상으로 저장하면 로그인 실패횟수 초기화
if (USER_STATUS_NORMAL.equals(userInfo.getStatus())) {
userInfo.setLoginfailcount(0);
}
userInfoService.save(userInfo);
}
@@ -148,6 +155,11 @@ public class UserManService extends BaseService {
UserInfo userInfo = userInfoService.getById(userUI.getUserId());
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256, userUI.getUserId()+subfix));
userUIMapper.updateToEntity(userUI, userInfo);
// 비밀번호 초기화 시 계정상태를 정상으로 변경하고 로그인 실패횟수 초기화
userInfo.setLoginfailcount(0);
userInfo.setStatus(USER_STATUS_NORMAL);
userInfoService.save(userInfo);
}
@@ -265,6 +265,9 @@ public interface MonitoringContext {
//비밀번호 초기화 접미사
public static final String RMS_PASSWORD_INIT_SUBFIX = "rms.password.init.subfix";
// 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용)
public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count";
@@ -64,6 +64,9 @@ public class MainController implements InterceptorSkipController {
private static final String REDIRECT_LOGIN_URL = "redirect:/emergency.jsp";
private static final String REDIRECT_CHANGE_PASSWORD_URL = "redirect:/emergency.jsp";
private static final String USER_STATUS_LOCKED = "2";
private static final int DEFAULT_MAX_LOGIN_FAIL_COUNT = 5;
private final LocaleMessage localeMessage;
private final MonitoringContext monitoringContext;
@@ -160,7 +163,7 @@ public class MainController implements InterceptorSkipController {
session.removeAttribute(RESULT_MSG);
return LoginResponseDto.builder()
.success(false)
.errorMessage(errorMsg != null ? errorMsg : "로그인에 실패했습니다.")
.errorMessage(toAlertMessage(errorMsg != null ? errorMsg : "로그인에 실패했습니다."))
.build();
}
@@ -345,10 +348,20 @@ public class MainController implements InterceptorSkipController {
//logger.info("사용자 계정 상태: " + userInfo.getStatus());
if ( userInfo.getStatus() != null && "2".equals(userInfo.getStatus()) ) {
setLoginFailure(request, session, "login.checkstatus1",
"login.checkstatus2");
return null;
if ( USER_STATUS_LOCKED.equals(userInfo.getStatus()) ) {
int maxLoginFailCount = getMaxLoginFailCount();
boolean lockedByFailCount = maxLoginFailCount > 0
&& userInfo.getLoginfailcount() != null
&& userInfo.getLoginfailcount() >= maxLoginFailCount;
if (lockedByFailCount) {
setLoginFailure(request, session, "login.accountlocked1",
"login.accountlocked2", String.valueOf(maxLoginFailCount));
} else {
setLoginFailure(request, session, "login.checkstatus1",
"login.checkstatus2");
}
return null;
}
//logger.info("사용자 ip: " + userInfo.getAllowip());
@@ -374,27 +387,29 @@ public class MainController implements InterceptorSkipController {
/* 기존로직 - LDAP 사용 x */
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256, dto.getPassword()))) {
// password 다름.
int failCount = increaseLoginFailCount(userInfo);
setLoginFailure(request, session, "login.loginfail1",
"login.loginfail2");
"login.loginfail2", failCount);
return null;
}
} else {
String password = dto.getPassword();
password = password.replace("#","#");
password = password.replace("&","&");
/* LDAP */
//if (!authenticate(userInfo.getUserid(), dto.getPassword())) {
if (!authenticate(userInfo.getUserid(), password)) {
// password 다름.
int failCount = increaseLoginFailCount(userInfo);
setLoginFailure(request, session, "login.loginfail1",
"login.loginfail2");
"login.loginfail2", failCount);
return null;
}
}
session = request.getSession(true);
session.setAttribute("userId", dto.getUserId());
@@ -432,7 +447,28 @@ public class MainController implements InterceptorSkipController {
return userInfo;
}
// LDAP
// 최대 로그인 실패 허용횟수 조회 (rms.password.fail.count, 0이면 계정 잠금 미적용)
private int getMaxLoginFailCount() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT);
}
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
private int increaseLoginFailCount(UserInfo userInfo) {
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
userInfo.setLoginfailcount(failCount);
int maxLoginFailCount = getMaxLoginFailCount();
if (maxLoginFailCount > 0 && failCount >= maxLoginFailCount) {
userInfo.setStatus(USER_STATUS_LOCKED);
logger.warn("로그인 " + maxLoginFailCount + "회 이상 실패로 계정 잠금 처리: " + userInfo.getUserid());
}
userInfoService.save(userInfo);
return failCount;
}
// LDAP
public boolean authenticate(String userId, String password) {
String LDAP_URL = monitoringPropertyService.getPrpty2ValById("Monitoring", "ldap.domain");
String NETBIOS = monitoringPropertyService.getPrpty2ValById("Monitoring", "ldap.netbios");
@@ -496,6 +532,7 @@ public class MainController implements InterceptorSkipController {
String lastLoginIp = IpUtil.getClientIp(request);
userInfo.setLastloginyms(LocalDateTime.now());
userInfo.setLastloginip(lastLoginIp);
userInfo.setLoginfailcount(0);
userInfoService.save(userInfo);
}
@@ -556,12 +593,32 @@ public class MainController implements InterceptorSkipController {
return "redirect:/common/errors/errorLogon.jsp";
}
// HTML 개행 태그(<BR/> 등)를 JS alert용 개행문자로 변환
private String toAlertMessage(String message) {
return message.replaceAll("(?i)<br\\s*/?>", "\n");
}
// 로그인 실패 시 로그 기록, 세션에 실패 메시지 설정
private void setLoginFailure(HttpServletRequest request,
HttpSession session, String logKey, String msgKey) {
HttpSession session, String logKey, String msgKey,
String... msgArgs) {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage.getString(logKey));
session.setAttribute(RESULT_MSG, localeMessage.getString(msgKey));
session.setAttribute(RESULT_MSG, localeMessage.getString(msgKey, msgArgs));
}
// 로그인 실패 시 로그 기록, 세션에 실패 메시지(실패 횟수 포함) 설정
private void setLoginFailure(HttpServletRequest request,
HttpSession session, String logKey, String msgKey,
int failCount) {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage.getString(logKey));
String msg = localeMessage.getString(msgKey);
int maxLoginFailCount = getMaxLoginFailCount();
if (maxLoginFailCount > 0) {
msg += " (" + failCount + "/" + maxLoginFailCount + ")";
}
session.setAttribute(RESULT_MSG, msg);
}
// 서비스 타입 검증
@@ -1200,6 +1200,8 @@ login.failcountover2 = You have had three password failures. <B
login.grantfail1 = Failed to initialize user credential information.
login.grantfail2 = Logon Error:<BR>Failed to initialize user credential information.
login.loginfail1 = Login failed. Please check your login information.
login.accountlocked1 = Account locked due to {0} or more login failures. Please contact your administrator.
login.accountlocked2 = Account locked due to {0} or more login failures.<BR/> Please contact your administrator.
login.loginfail2 = Login failed.<BR/>Please check your login information.
login.logonaccount = Logon account
login.logout = Logout
@@ -1346,6 +1346,8 @@ login.failcountover2 = You have had three password failures. <B
login.grantfail1 = Failed to initialize user credential information.
login.grantfail2 = Logon Error:<BR>Failed to initialize user credential information.
login.loginfail1 = Login failed. Please check your login information.
login.accountlocked1 = Account locked due to {0} or more login failures. Please contact your administrator.
login.accountlocked2 = Account locked due to {0} or more login failures.<BR/> Please contact your administrator.
login.loginfail2 = Login failed.<BR/>Please check your login information.
login.logonaccount = Logon account
login.logout = Logout
@@ -1426,6 +1426,8 @@ login.loginfail1 = \uC0AC\uC6A9\uC790ID \uB610\uB294 \uBE44
login.loginfail2 = \uC0AC\uC6A9\uC790ID \uB610\uB294 \uBE44\uBC00\uBC88\uD638 \uC785\uB825 \uC2E4\uD328\uC785\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.checkstatus1 = \uC7A5\uAE30(90\uC77C)\uBBF8\uC0AC\uC6A9 \uACC4\uC815\uC785\uB2C8\uB2E4. \uB2F4\uB2F9\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.
login.checkstatus2 = \uC7A5\uAE30(90\uC77C)\uBBF8\uC0AC\uC6A9 \uACC4\uC815\uC785\uB2C8\uB2E4.<BR/> \uB2F4\uB2F9\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.
login.accountlocked1 = \uB85C\uADF8\uC778 {0}\uD68C \uC774\uC0C1 \uC2E4\uD328\uB85C \uACC4\uC815\uC774 \uC7A0\uACBC\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.
login.accountlocked2 = \uB85C\uADF8\uC778 {0}\uD68C \uC774\uC0C1 \uC2E4\uD328\uB85C \uACC4\uC815\uC774 \uC7A0\uACBC\uC2B5\uB2C8\uB2E4.<BR/> \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.
login.logonaccount = \uB85C\uADF8\uC628\uACC4\uC815
login.logout = \uB85C\uADF8\uC544\uC6C3
login.password = \uBE44\uBC00\uBC88\uD638