취약점 점검 수정

This commit is contained in:
eastargh
2026-08-26 13:47:32 +09:00
parent 37817b15a9
commit 9df39e74bd
12 changed files with 325 additions and 51 deletions
+1 -1
View File
@@ -175,6 +175,6 @@
</jsp-config>
<session-config>
<session-timeout>60</session-timeout>
<session-timeout>10</session-timeout>
</session-config>
</web-app>
+1 -1
View File
@@ -145,6 +145,6 @@
</jsp-config>
<session-config>
<session-timeout>60</session-timeout>
<session-timeout>10</session-timeout>
</session-config>
</web-app>
+36 -6
View File
@@ -102,7 +102,7 @@
window.location.href = response.redirectUrl;
} else {
// 로그인 실패
alert(response.errorMessage || '로그인에 실패했습니다.');
alert('로그인에 실패했습니다');
}
},
error: function(xhr, status, error) {
@@ -262,7 +262,36 @@
return true;
}
// 비밀번호 변경 요청 (AJAX) - 서버 룰 검증 실패 시 모달을 닫지 않고 비밀번호 입력값만 초기화
function submitChangePassword() {
if (!checkPwd()) {
return false;
}
$.ajax({
url: '<c:url value="/changePassword.do"/>',
type: 'POST',
data: $('#modalLoginForm').serialize(),
dataType: 'json',
success: function(response) {
alert(response.message);
if (response.success) {
window.location.href = response.redirectUrl || '<c:url value="/emergency.jsp"/>';
} else {
$("input[name=changePassword]").val('');
$("input[name=confirmPassword]").val('');
$("input[name=changePassword]").trigger("focus");
}
},
error: function() {
alert("<%= localeMessage.getString("login.pwdchangefail") %>");
}
});
return false;
}
$(document).ready(function() {
$("input[name=userId]").keydown(function(event){
if ( event.which == 13 ) {
@@ -276,12 +305,12 @@
});
$("input[name=changePassword]").keydown(function(event){
if ( event.which == 13 ) {
$('#modalLoginForm').submit();
submitChangePassword();
}
});
$("input[name=confirmPassword]").keydown(function(event){
if ( event.which == 13 ) {
$('#modalLoginForm').submit();
submitChangePassword();
}
});
// Password Change 링크로 직접 열 때만 초기화 (JS에서 modal('show') 호출 시 relatedTarget 없음)
@@ -387,7 +416,7 @@
<span aria-hidden="true">X</span>
</button>
</div>
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return checkPwd()" novalidate>
<form id="modalLoginForm" action="<c:url value="/changePassword.do"/>" method="post" onsubmit="return submitChangePassword()" novalidate>
<div class="modal-body">
<div id="changeInitPassword">
<div class="form-group d-flex">
@@ -405,7 +434,8 @@
<input type="password" name="confirmPassword" class="form-control rounded-left" placeholder="<%= localeMessage.getString("login.placeholderConfirmationPassword") %>" autocomplete="off" required>
</div>
<span style="font-size:0.8em; color:red">
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상
※ 7글자 이상 & 영문/숫자/특수문자 2종류 이상<br/>
※ 연속된 숫자/문자(예: 123, abc, qwer), 동일 문자 3자 이상 반복 사용 불가
</span>
</div>
<div class="modal-footer">
@@ -269,7 +269,10 @@ public interface MonitoringContext {
// 최대 로그인 실패 허용횟수 (0이면 계정 잠금 미적용)
public static final String RMS_PASSWORD_FAIL_COUNT = "rms.password.fail.count";
// 비밀번호 변경 시 재사용을 금지할 최근 이력 개수
public static final String RMS_PASSWORD_HISTORY_COUNT = "rms.password.history.count";
// API 상태변화 스윙챗 발송여부
public static final String DJB_UMS_MESSENGER_APIMONITOR_ENABLED = "djb.ums.messenger.api-monitor.enabled";
@@ -0,0 +1,28 @@
package com.eactive.eai.rms.common.login;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChangePasswordResponseDto {
/**
* 비밀번호 변경 성공 여부
*/
private boolean success;
/**
* 사용자에게 alert로 표시할 메시지
*/
private String message;
/**
* 성공 시 이동할 URL
*/
private String redirectUrl;
}
@@ -47,6 +47,7 @@ import com.eactive.apim.portal.user.entity.UserInfo;
import com.eactive.eai.rms.data.entity.man.user.UserInfoService;
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistory;
import com.eactive.eai.rms.data.entity.man.user.UserLoginHistoryService;
import com.eactive.eai.rms.data.entity.man.user.AdminUserPasswordHistoryService;
import com.eactive.eai.rms.data.entity.man.user.UserRole;
import com.eactive.eai.rms.data.entity.man.user.UserRoleService;
import com.eactive.eai.rms.data.entity.man.user.UserServiceTypeService;
@@ -62,10 +63,10 @@ public class MainController implements InterceptorSkipController {
private static final String USER_NOT_FOUND_KEY_2 = "login.useridnotfound2";
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 static final int DEFAULT_PASSWORD_HISTORY_COUNT = 5;
private final LocaleMessage localeMessage;
private final MonitoringContext monitoringContext;
@@ -76,6 +77,7 @@ public class MainController implements InterceptorSkipController {
private final LoginService loginService;
private final MonitoringPropertyService monitoringPropertyService;
private final UserLoginHistoryService userLoginHistoryService;
private final AdminUserPasswordHistoryService userPasswordHistoryService;
private final SmsAuthService smsAuthService;
@Autowired
@@ -87,6 +89,7 @@ public class MainController implements InterceptorSkipController {
UserServiceTypeService userServiceTypeService,
MonitoringPropertyService monitoringPropertyService,
UserLoginHistoryService userLoginHistoryService,
AdminUserPasswordHistoryService userPasswordHistoryService,
SmsAuthService smsAuthService) {
this.localeMessage = localeMessage;
this.monitoringContext = monitoringContext;
@@ -96,6 +99,7 @@ public class MainController implements InterceptorSkipController {
this.userServiceTypeService = userServiceTypeService;
this.monitoringPropertyService = monitoringPropertyService;
this.userLoginHistoryService = userLoginHistoryService;
this.userPasswordHistoryService = userPasswordHistoryService;
this.smsAuthService = smsAuthService;
}
@@ -453,6 +457,12 @@ public class MainController implements InterceptorSkipController {
MonitoringContext.RMS_PASSWORD_FAIL_COUNT, DEFAULT_MAX_LOGIN_FAIL_COUNT);
}
// 비밀번호 재사용 금지 대상 최근 이력 개수 조회 (rms.password.history.count)
private int getPasswordHistoryCount() {
return monitoringContext.getIntProperty(
MonitoringContext.RMS_PASSWORD_HISTORY_COUNT, DEFAULT_PASSWORD_HISTORY_COUNT);
}
// 로그인 실패 횟수 증가, 임계치 도달 시 계정 잠금
private int increaseLoginFailCount(UserInfo userInfo) {
int failCount = (userInfo.getLoginfailcount() == null ? 0 : userInfo.getLoginfailcount()) + 1;
@@ -824,7 +834,8 @@ public class MainController implements InterceptorSkipController {
}
@RequestMapping("/changePassword.do")
public String changePassword(HttpServletRequest request,
@ResponseBody
public ChangePasswordResponseDto changePassword(HttpServletRequest request,
String changePassword, String confirmPassword,
String resetUserId, String resetPassword) {
HttpSession session = request.getSession();
@@ -842,9 +853,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString(USER_NOT_FOUND_KEY_1));
session.setAttribute(RESULT_MSG, localeMessage
.getString(USER_NOT_FOUND_KEY_1));
return REDIRECT_LOGIN_URL;
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString(USER_NOT_FOUND_KEY_1)))
.build();
}
UserInfo userInfo = userInfoOptional.get();
@@ -854,9 +866,10 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString("login.pwdmismatch1"));
session.setAttribute(RESULT_MSG, localeMessage
.getString("login.pwdmismatch2"));
return REDIRECT_CHANGE_PASSWORD_URL;
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdmismatch2")))
.build();
}
// 2. 비밀번호 Validation
@@ -864,48 +877,80 @@ public class MainController implements InterceptorSkipController {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString("login.pwdmismatch3"));
session.setAttribute(RESULT_MSG, localeMessage
.getString("login.pwdmismatch4"));
return REDIRECT_CHANGE_PASSWORD_URL;
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdmismatch4")))
.build();
}
// 3. 비밀번호 체크
if (!userInfo.getPassword().equals(DamoManager.getInstance().hash(DamoManager.SHA256,resetPassword))
|| !userInfo.getUserid().equals(resetUserId)) {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString("login.pwdmismatch5"));
session.setAttribute(RESULT_MSG, localeMessage
.getString("login.pwdmismatch6"));
return REDIRECT_CHANGE_PASSWORD_URL;
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdmismatch6")))
.build();
}
// 4. 비밀번호 변경
userInfo.setPassword(DamoManager.getInstance().hash(DamoManager.SHA256,changePassword));
// 4. 최근 사용한 비밀번호 재사용 금지
String newPasswordHash = DamoManager.getInstance().hash(DamoManager.SHA256, changePassword);
if (userPasswordHistoryService.isPasswordReused(resetUserId, newPasswordHash, getPasswordHistoryCount())) {
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "F",
localeMessage
.getString("login.pwdreused1"));
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdreused2")))
.build();
}
// 5. 비밀번호 변경
userInfo.setPassword(newPasswordHash);
userInfoService.save(userInfo);
// 6. 비밀번호 변경 이력 저장 (재사용 금지 검증용)
userPasswordHistoryService.recordPasswordChange(resetUserId, newPasswordHash);
UserAccessLogger.log(request, LOG_CATEGORY_LOGIN, "S", localeMessage
.getString("login.pwdchanged"));
session.setAttribute(RESULT_MSG, localeMessage
.getString("login.pwdchanged"));
logger.debug("\n\n\n\n===================" + localeMessage
.getString("login.pwdchanged"));
return REDIRECT_LOGIN_URL;
return ChangePasswordResponseDto.builder()
.success(true)
.message(toAlertMessage(localeMessage.getString("login.pwdchanged")))
.redirectUrl(request.getContextPath() + "/emergency.jsp")
.build();
} catch (Exception e) {
logger.error("changePassword Error ", e);
return "redirect:/common/errors/errorLogon.jsp";
return ChangePasswordResponseDto.builder()
.success(false)
.message(toAlertMessage(localeMessage.getString("login.pwdchangefail")))
.build();
}
}
private static final int PASSWORD_PATTERN_CHECK_LENGTH = 3;
private static final String[] KEYBOARD_SEQUENTIAL_ROWS = {
"1234567890",
"!@#$%^&*()",
"qwertyuiop",
"asdfghjkl",
"zxcvbnm"
};
private boolean isPasswordValid(String password) {
return password != null && password.getBytes().length == password
.length() && password.getBytes().length >= 7
&& isCombination(password)
&& !isRepeatPassword(password, 5)
&& !isSerialPassword(password, 5);
&& !isRepeatPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
&& !isSerialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH)
&& !isKeyboardSequentialPassword(password, PASSWORD_PATTERN_CHECK_LENGTH);
}
private boolean isRepeatPassword(String password, int count) {
@@ -930,21 +975,41 @@ public class MainController implements InterceptorSkipController {
}
private boolean isSerialPassword(String password, int count) {
int preData = 0;
int serialcount = 1;
int ascCount = 1;
int descCount = 1;
for (int i = 0; i < password.length(); i++) {
for (int i = 1; i < password.length(); i++) {
char prev = password.charAt(i - 1);
char curr = password.charAt(i);
ascCount = (curr == prev + 1) ? ascCount + 1 : 1;
descCount = (curr == prev - 1) ? descCount + 1 : 1;
if (ascCount >= count || descCount >= count) { return true; }
}
return false;
}
// 키보드 자판 배열상 연속된 문자열(qwer, asdf 등, 역순 포함) 사용 여부 체크
private boolean isKeyboardSequentialPassword(String password, int count) {
String lowerPassword = password.toLowerCase();
for (String row : KEYBOARD_SEQUENTIAL_ROWS) {
for (int i = 0; i <= row.length() - count; i++) {
String forward = row.substring(i, i + count);
String backward = new StringBuilder(forward).reverse().toString();
if (lowerPassword.contains(forward) || lowerPassword.contains(backward)) {
return true;
}
if (preData + 1 == password.charAt(i)) {
serialcount++;
} else {
serialcount = 1;
}
preData = password.charAt(i);
if (serialcount == count) { return true; }
}
return false;
@@ -0,0 +1,77 @@
package com.eactive.eai.rms.data.entity.man.user;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.GenericGenerator;
import com.eactive.eai.data.entity.AbstractEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
/**
* 관리콘솔(TSEAIRM02) 로그인 사용자의 비밀번호 변경 이력 (TSEAIRM08).
* 포털 API 소비자 비밀번호 이력({@code com.eactive.apim.portal.portaluser.entity.UserPasswordHistory})과는 별개 테이블/도메인이다.
*/
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = true)
@Entity
@Table(name = "TSEAIRM08")
@org.hibernate.annotations.Table(appliesTo = "TSEAIRM08", comment = "관리콘솔 사용자 비밀번호 변경 이력")
public class AdminUserPasswordHistory extends AbstractEntity<Integer> implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GenericGenerator(
name = "TSEAIRM08_GEN",
strategy = "com.eactive.eai.rms.data.jpa.SchemaPrefixedSequenceGenerator",
parameters = {
@org.hibernate.annotations.Parameter(name = "sequence_name", value = "SEQ_TSEAIRM08"),
@org.hibernate.annotations.Parameter(name = "initial_value", value = "1"),
@org.hibernate.annotations.Parameter(name = "increment_size", value = "1")
}
)
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "TSEAIRM08_GEN"
)
@Column(name = "ID", unique = true, nullable = false)
@Comment("비밀번호 변경 이력 식별 ID")
private int pwdHistId;
@Column(name = "USERID", length = 255, nullable = false)
@Comment("사용자 식별자(사번)")
private String userId;
@Column(name = "PASSWORD_HASH", length = 255, nullable = false)
@Comment("변경된 비밀번호 해시값")
private String passwordHash;
@Column(name = "CHANGE_DATE", length = 14)
@Comment("비밀번호 변경 일시 (yyyyMMddHHmmss)")
private String changeDate;
@Column(name = "CREATED_BY", length = 200)
@Comment("등록자 식별자")
private String createdBy;
@Column(name = "CREATED_DATE", length = 14)
@Comment("레코드 생성 일시 (yyyyMMddHHmmss)")
private String createdDate;
@Override
public @NonNull Integer getId() {
return pwdHistId;
}
}
@@ -0,0 +1,9 @@
package com.eactive.eai.rms.data.entity.man.user;
import com.eactive.eai.rms.data.EMSDataSource;
import com.eactive.eai.data.jpa.BaseRepository;
@EMSDataSource
public interface AdminUserPasswordHistoryRepository extends BaseRepository<AdminUserPasswordHistory, Integer> {
}
@@ -0,0 +1,53 @@
package com.eactive.eai.rms.data.entity.man.user;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
import com.eactive.eai.rms.data.jpa.AbstractEMSDataSerivce;
import com.querydsl.core.types.dsl.BooleanExpression;
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class AdminUserPasswordHistoryService
extends AbstractEMSDataSerivce<AdminUserPasswordHistory, Integer, AdminUserPasswordHistoryRepository> {
/** 사용자의 최근 비밀번호 이력을 최신순으로 historyCount건 조회 */
public List<AdminUserPasswordHistory> findRecent(String userId, int historyCount) {
BooleanExpression predicate = QAdminUserPasswordHistory.adminUserPasswordHistory.userId.eq(userId);
Pageable pageable = PageRequest.of(0, historyCount, Sort.by(Sort.Direction.DESC, "pwdHistId"));
return repository.findAll(predicate, pageable).getContent();
}
/** 최근 historyCount건 이내에 동일한 비밀번호(해시값)가 있었는지 확인 */
public boolean isPasswordReused(String userId, String newPasswordHash, int historyCount) {
if (historyCount <= 0) {
return false;
}
return findRecent(userId, historyCount).stream()
.anyMatch(history -> history.getPasswordHash().equals(newPasswordHash));
}
/** 비밀번호 변경 이력 저장 */
public void recordPasswordChange(String userId, String passwordHash) {
String now = LocalDateTime.now().format(LocalDateTimeFormatters.FORMATTER_YYYYMMDDHHMMSS_14);
AdminUserPasswordHistory history = new AdminUserPasswordHistory();
history.setUserId(userId);
history.setPasswordHash(passwordHash);
history.setChangeDate(now);
history.setCreatedBy(userId);
history.setCreatedDate(now);
save(history);
}
}
@@ -1215,9 +1215,12 @@ login.pwdchanged = Your password has been changed.
login.pwdmismatch1 = Your change password is different from your verification password.
login.pwdmismatch2 = Your change password is different from your verification password. <BR/> Please check your login information.
login.pwdmismatch3 = The change password does not match the rule.
login.pwdmismatch4 = The change password does not match the rule. <BR/> Please check your login information.
login.pwdmismatch4 = The change password does not match the rule.
login.pwdmismatch5 = Your password is different.
login.pwdmismatch6 = Your password is different. <BR/> Please check your login information.
login.pwdmismatch6 = Your password is different.
login.pwdreused1 = You cannot reuse a recently used password.
login.pwdreused2 = You cannot reuse a recently used password. <BR/> Please enter a different password.
login.pwdchangefail = An error occurred while changing the password.
login.pwdnotchanged1 = You did not change your password after resetting your password. Please change your password and login.
login.pwdnotchanged2 = You did not change your password after resetting your password.<BR/>Please change your password and login.
login.seedencriptfail = Seed encryption failed. <BR/> Please sign in again.
@@ -1360,9 +1360,12 @@ login.pwdchanged = Your password has been changed.
login.pwdmismatch1 = Your change password is different from your verification password.
login.pwdmismatch2 = Your change password is different from your verification password. <BR/> Please check your login information.
login.pwdmismatch3 = The change password does not match the rule.
login.pwdmismatch4 = The change password does not match the rule. <BR/> Please check your login information.
login.pwdmismatch4 = The change password does not match the rule.
login.pwdmismatch5 = Your password is different.
login.pwdmismatch6 = Your password is different. <BR/> Please check your login information.
login.pwdmismatch6 = Your password is different.
login.pwdreused1 = You cannot reuse a recently used password.
login.pwdreused2 = You cannot reuse a recently used password. <BR/> Please enter a different password.
login.pwdchangefail = An error occurred while changing the password.
login.pwdnotchanged1 = You did not change your password after resetting your password. Please change your password and login.
login.pwdnotchanged2 = You did not change your password after resetting your password.<BR/>Please change your password and login.
login.seedencriptfail = Seed encryption failed. <BR/> Please sign in again.
@@ -1440,9 +1440,12 @@ login.pwdchanged = \uBE44\uBC00\uBC88\uD638\uAC00 \uBCC0\uA
login.pwdmismatch1 = \uBCC0\uACBD \uBE44\uBC00\uBC88\uD638 \uC640 \uD655\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.
login.pwdmismatch2 = \uBCC0\uACBD \uBE44\uBC00\uBC88\uD638 \uC640 \uD655\uC778 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.pwdmismatch3 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4.
login.pwdmismatch4 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.pwdmismatch4 = \uBCC0\uACBD\uBE44\uBC00\uBC88\uD638\uAC00 \uB8F0\uC5D0 \uC548\uB9DE\uC2B5\uB2C8\uB2E4.
login.pwdmismatch5 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.
login.pwdmismatch6 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.pwdmismatch6 = \uBCC0\uACBD\uC804 \uBE44\uBC00\uBC88\uD638\uAC00 \uB2E4\uB985\uB2C8\uB2E4.
login.pwdreused1 = \uCD5C\uADFC \uC0AC\uC6A9\uD55C \uBE44\uBC00\uBC88\uD638\uB294 \uB2E4\uC2DC \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
login.pwdreused2 = \uCD5C\uADFC \uC0AC\uC6A9\uD55C \uBE44\uBC00\uBC88\uD638\uB294 \uB2E4\uC2DC \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.<BR/> \uB2E4\uB978 \uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD558\uC138\uC694.
login.pwdchangefail = \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.
login.pwdnotchanged1 = \uBE44\uBC00\uBC88\uD638 \uCD08\uAE30\uD654\uD6C4 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD\uC744 \uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\uBE44\uBC00\uBC88\uD638\uB97C \uBCC0\uACBD\uD558\uC2DC\uACE0 \uB85C\uADF8\uC778 \uD558\uC138\uC694
login.pwdnotchanged2 = \uBE44\uBC00\uBC88\uD638 \uCD08\uAE30\uD654\uD6C4 \uBE44\uBC00\uBC88\uD638 \uBCC0\uACBD\uC744 \uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\uBE44\uBC00\uBC88\uD638\uB97C \uBCC0\uACBD\uD558\uC2DC\uACE0 \uB85C\uADF8\uC778 \uD558\uC138\uC694.<BR/> \uB85C\uADF8\uC778\uC815\uBCF4\uB97C \uD655\uC778\uD558\uC138\uC694.
login.seedencriptfail = Seed \uC554\uD638\uD654\uC5D0 \uC2E4\uD328 \uD558\uC600\uC2B5\uB2C8\uB2E4.<BR/> \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694.