로그인 시 이메일 인증 절차 페이지 완료

This commit is contained in:
Rinjae
2025-11-13 19:41:25 +09:00
parent 5ec8fd9919
commit aabb9d38a9
16 changed files with 1165 additions and 11 deletions
@@ -22,10 +22,10 @@ public class AuthNumberServiceImpl implements AuthNumberService {
private final AuthNumberGenerator generator;
private final MessageSender messageSender;
@Value("${auth-ttl:300}")
@Value("${portal.auth-ttl:300}")
private int authNumberExpirationTime;
@Value("${auth.resend.limit.seconds:30}")
@Value("${portal.auth.resend_limit_seconds:30}")
private int resendLimitSeconds;
@Autowired
@@ -46,6 +46,7 @@ public class AccountController {
private final UserFacade userFacade;
private final OrgRegisterFacade orgRegisterFacade;
private final AgreementsFacade agreementsFacade;
private final com.eactive.apim.portal.apps.user.facade.AuthFacade authFacade;
@PostMapping("/confirm_password")
@@ -326,4 +327,115 @@ public class AccountController {
return "redirect:/mypage";
}
}
@GetMapping("/mypage/verification-email")
public String showVerificationEmailPage(Model model) {
try {
// 현재 로그인한 사용자 정보 가져오기
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
if (currentUser == null) {
return "redirect:/login";
}
// 사용자 이메일 주소를 모델에 추가
model.addAttribute("email", currentUser.getUsername());
model.addAttribute("userId", currentUser.getId());
return "apps/mypage/verificationEmail";
} catch (Exception e) {
logger.error("이메일 인증 페이지 로드 실패", e);
return "redirect:/";
}
}
@PostMapping("/mypage/send-verification-code")
public ResponseEntity<ValidationResponse> sendVerificationCode(
@org.springframework.web.bind.annotation.RequestBody java.util.Map<String, String> request,
HttpSession session) {
try {
String email = request.get("email");
// 현재 로그인한 사용자의 이메일과 일치하는지 확인
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
if (currentUser == null || !currentUser.getUsername().equals(email)) {
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("유효하지 않은 요청입니다.");
return ResponseEntity.badRequest().body(response);
}
// 이전 세션 데이터 정리
session.removeAttribute("emailVerified");
// 이메일 인증 코드 발송
ValidationResponse response = authFacade.requestAuth(email, "EMAIL");
if (response.isValid()) {
session.setAttribute("verificationEmail", email);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("이메일 인증 코드 발송 실패", e);
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("인증 코드 발송에 실패했습니다.");
return ResponseEntity.status(500).body(response);
}
}
@PostMapping("/mypage/verify-email-code")
public ResponseEntity<ValidationResponse> verifyEmailCode(
@org.springframework.web.bind.annotation.RequestBody java.util.Map<String, String> request,
HttpSession session) {
try {
String email = request.get("email");
String code = request.get("code");
// 현재 로그인한 사용자의 이메일과 일치하는지 확인
PortalAuthenticatedUser currentUser = SecurityUtil.getPortalAuthenticatedUser();
if (currentUser == null || !currentUser.getUsername().equals(email)) {
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("유효하지 않은 요청입니다.");
return ResponseEntity.badRequest().body(response);
}
// 세션에 저장된 이메일과 일치하는지 확인
String sessionEmail = (String) session.getAttribute("verificationEmail");
if (sessionEmail == null || !sessionEmail.equals(email)) {
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("인증 요청된 이메일과 일치하지 않습니다.");
return ResponseEntity.badRequest().body(response);
}
// 인증 코드 확인
ValidationResponse response = authFacade.verifyAuthNumber(email, code);
if (response.isValid()) {
session.setAttribute("emailVerified", true);
// 사용자 상태를 ACTIVE로 변경
userFacade.activateUserByEmail(email);
// 이메일 인증 관련 세션 정보 제거
session.removeAttribute("emailVerificationRequired");
session.removeAttribute("redirectUrl");
logger.info("이메일 인증 완료: {}", email);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("이메일 인증 코드 확인 실패", e);
ValidationResponse response = new ValidationResponse();
response.setValid(false);
response.setMessage("인증 코드 확인에 실패했습니다.");
return ResponseEntity.status(500).body(response);
}
}
}
@@ -15,4 +15,6 @@ public interface UserFacade {
void updateCorporateManager(PortalUserDTO portalUserDTO);
void withdrawUser(String userId);
void activateUserByEmail(String email);
}
@@ -198,4 +198,23 @@ public class UserFacadeImpl implements UserFacade {
throw new IllegalArgumentException("새 비밀번호와 확인 비밀번호가 일치하지 않습니다.");
}
}
@Override
@Transactional
public void activateUserByEmail(String email) {
PortalUser user = portalUserService.findByEmailAddr(email);
if (user == null) {
throw new IllegalArgumentException("사용자를 찾을 수 없습니다.");
}
if (user.getUserStatus() == PortalUserEnums.UserStatus.ACTIVE) {
throw new IllegalArgumentException("이미 활성화된 계정입니다.");
}
user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE);
portalUserService.save(user);
log.info("사용자 이메일 인증 완료 - 활성화: {}", email);
}
}
@@ -49,6 +49,15 @@ public class PortalUserService {
return portalUserRepository.findByLoginId(loginId);
}
public PortalUser findByEmailAddr(String emailAddr) {
return portalUserRepository.findPortalUserByEmailAddr(emailAddr)
.orElseThrow(() -> new IllegalArgumentException("해당 이메일 주소의 사용자를 찾을 수 없습니다."));
}
public PortalUser save(PortalUser user) {
return portalUserRepository.save(user);
}
public boolean existsByLoginId(String loginId) {
return portalUserRepository.existsByLoginId(loginId);
}
@@ -67,9 +67,10 @@ public class PortalAuthenticationManager implements AuthenticationManager {
throw new DisabledException("사용자 승인 대기중입니다. ");
}
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) {
throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요.");
}
// 25.11.13 - 이메일 인증 로그인은 허용
// if (user.getUserStatus().equals(PortalUserEnums.UserStatus.READY)) {
// throw new DisabledException("이메일 인증이 완료되지 않았습니다. 인증 메일을 확인해주세요.");
// }
if (user.getUserStatus().equals(PortalUserEnums.UserStatus.ADMINBLOCK)) {
throw new DisabledException("법인 관리자에 의해 비활성화된 계정입니다. 법인 관리자에게 문의하시기 바랍니다.");
@@ -55,7 +55,11 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
HttpSession session = request.getSession();
// 세션에 상태 저장
if (isDormantAccount(user)) {
if (isEmailVerificationRequired(user)) {
session.setAttribute("success", "이메일 인증이 완료되지 않았습니다. 이메일을 확인하여 인증을 완료해주세요.");
session.setAttribute("emailVerificationRequired", true);
session.setAttribute("redirectUrl", contextPath + "/mypage/verification-email");
} else if (isDormantAccount(user)) {
session.setAttribute("success", "90일 이상 미접속하여 계정이 잠금 처리되었습니다. 본인인증 후 이용해주세요.");
session.setAttribute("dormantAccount", true);
session.setAttribute("dormantLoginId", username);
@@ -114,4 +118,8 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
return PortalUserEnums.UserStatus.DORMANT.equals(user.getUserStatus());
}
private boolean isEmailVerificationRequired(PortalUser user) {
return PortalUserEnums.UserStatus.READY.equals(user.getUserStatus());
}
}
+2
View File
@@ -75,6 +75,8 @@ portal:
log-path: /Log/App/eapim/
auth-ttl: 300
auth:
resend_limit_seconds: 30
user-approval: true
password-expiration-days: 90
+1
View File
@@ -40,6 +40,7 @@
<logger name="com.eactive.apim" level="DEBUG" />
<logger name="org.thymeleaf.templateparser" level="DEBUG" />
<!-- Hibernate SQL Logging -->
@@ -145,10 +145,10 @@
}
if (document.loginForm.message && document.loginForm.message.value) {
if (document.loginForm.message.value.includes("이메일 인증이 완료되지 않았습니다")) {
console.log("이메일 재발송 팝업 트리거");
let loginId = $('#loginId').val();
console.log("Retrieved loginId:", loginId);
customPopups.showEmailResendPopup(document.loginForm.message.value, loginId);
console.log("이메일 인증 페이지 이동");
location.href = '/mypage/verification-email'
// console.log("Retrieved loginId:", loginId);
// customPopups.showEmailResendPopup(document.loginForm.message.value, loginId);
} else {
customPopups.showAlert(document.loginForm.message.value);
}
@@ -248,6 +248,23 @@
customPopups.showAlert(successMsg);
}
// 이메일 인증 체크
const emailVerificationRequired = [[${session.emailVerificationRequired}]];
if (emailVerificationRequired) {
const emailVerifyMsg = /*[[${session.success}]]*/ '';
const redirectUrl = /*[[${session.redirectUrl}]]*/ '';
console.log('emailVerifyMsg:', emailVerifyMsg);
console.log('redirectUrl:', redirectUrl);
customPopups.showAlert(emailVerifyMsg);
$('#customAlertOkButton').off('click.custom').on('click.custom', function () {
customPopups.hideAlert('#customAlert');
window.location.href = redirectUrl;
});
return; // 다른 체크 중단
}
// 장기미사용 계정 체크
const dormantAccount = [[${session.dormantAccount}]];
if (dormantAccount) {
@@ -428,7 +445,7 @@
}
});
});
G
const hashtagItems = document.querySelectorAll('.hashtag ul li a');
const searchBox = document.getElementById('search-box');
@@ -0,0 +1,246 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/kbank_base_layout}">
<body>
<section layout:fragment="contentFragment" class="content">
<div class="content_wrap">
<!-- 타이틀 -->
<div class="sub_title2" id="title-verification-email">
<h2 class="title add5">이메일 인증</h2>
</div>
<div class="inner i_cs10 h_inner2">
<div class="form_type">
<form id="verificationEmailForm" role="form" name="verificationEmailForm" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="hidden" id="userId" name="userId" th:value="${userId}">
<!-- 이메일 주소 표시 (readonly) -->
<div class="info1 p_top2">
<p class="title w_tit">
<span class="dot">이메일 주소</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp4">
<input type="email" id="email" name="email" class="common_input_type_1"
th:value="${email}" readonly style="background-color: #f5f5f5;">
</div>
</div>
<p class="info_txt" style="margin-top: 10px; color: #666;">
위 이메일 주소로 인증코드가 발송됩니다.
</p>
</div>
<!-- 인증코드 받기 버튼 -->
<div class="info1 pt28">
<div class="btn_check" style="width: 100%;">
<a class="common_btn_type_2 cbt btn_send_code" style="width: 100%;"><span>인증코드 받기</span></a>
</div>
</div>
<!-- 인증번호 입력 -->
<div class="info1 pt28 auth-code-container" id="authCodeContainer" style="display: none;">
<p class="title w_tit">
<span class="dot">인증코드 입력</span>
</p>
<div class="info_line">
<div class="info_box1 w_inp5">
<div class="certify_box">
<input type="text" id="authCode" class="common_input_type_1 w_input"
placeholder="이메일로 받은 인증코드를 입력하세요" maxlength="6" required>
<span class="certify_time" id="certify_time">05:00</span>
</div>
</div>
<div class="btn_check">
<a class="common_btn_type_2 cbt btn_verify_code"><span>인증코드 확인</span></a>
</div>
</div>
</div>
<!-- 제출 버튼 -->
<div class="btn_application m_top">
<a type="button" class="common_btn_type_1 gray btn_cancel"><span>취소</span></a>
<div class="btn_gap"></div>
<button type="button" class="common_btn_type_1 btn_complete" disabled><span>인증 완료</span></button>
</div>
</form>
</div>
<!-- &lt;!&ndash; 탭 메뉴 &ndash;&gt;-->
<!-- <div class="tab-wrap">-->
<!-- <div class="tabs">-->
<!-- <ul class="tab_nav tabm">-->
<!-- <li class="active"><a href="#tab1">이메일 인증</a></li>-->
<!-- </ul>-->
<!-- &lt;!&ndash; 이메일 인증 폼 &ndash;&gt;-->
<!-- <div class="tab active" id="tab1">-->
<!-- -->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
</section>
</body>
<th:block layout:fragment="contentScript">
<script th:inline="javascript">
$(document).ready(function() {
let timerInterval;
let resendTimerInterval;
let isVerified = false;
// CSRF 토큰 설정
const csrfHeaderName = /*[[${_csrf.headerName}]]*/ 'X-CSRF-TOKEN';
const csrfToken = /*[[${_csrf.token}]]*/ '';
// 서버 설정값
const resendLimitSeconds = /*[[${@environment.getProperty('portal.auth.resend_limit_seconds', '60')}]]*/ 60;
// 취소 버튼 클릭 시 메인 페이지로 이동
$('.btn_cancel').on('click', function() {
if (confirm('이메일 인증을 취소하고 메인 페이지로 이동하시겠습니까?')) {
location.href = '/';
}
});
// 타이머 시작 함수 (인증 코드 유효 시간)
function startTimer(duration) {
let timer = duration;
clearInterval(timerInterval);
timerInterval = setInterval(function () {
let minutes = parseInt(timer / 60, 10);
let seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
$('#certify_time').text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(timerInterval);
$('#certify_time').text("00:00").css('color', 'red');
customPopups.showAlert("인증 시간이 만료되었습니다. 인증코드를 다시 받아주세요.");
}
}, 1000);
}
// 재발송 타이머 시작 함수
function startResendTimer() {
let resendTimer = resendLimitSeconds;
const $button = $('.btn_send_code');
clearInterval(resendTimerInterval);
$button.prop('disabled', true);
resendTimerInterval = setInterval(function () {
if (resendTimer > 0) {
$button.find('span').text('재발송 가능 (' + resendTimer + '초)');
resendTimer--;
} else {
clearInterval(resendTimerInterval);
$button.prop('disabled', false).find('span').text('인증코드 재발송');
}
}, 1000);
}
// 인증코드 받기 버튼 클릭
$('.btn_send_code').on('click', function(e) {
e.preventDefault();
const email = $('#email').val();
const $button = $(this);
$button.prop('disabled', true).find('span').text('발송 중...');
$.ajax({
url: '/mypage/send-verification-code',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ email: email }),
headers: {
[csrfHeaderName]: csrfToken
},
success: function(response) {
customPopups.showAlert('인증코드가 이메일로 발송되었습니다.');
$('#authCodeContainer').show();
startTimer(300); // 5분 타이머
startResendTimer(); // 재발송 타이머 시작 (1분)
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.error || '인증코드 발송에 실패했습니다.';
customPopups.showAlert(errorMsg);
$button.prop('disabled', false).find('span').text('인증코드 받기');
}
});
});
// 인증코드 확인 버튼 클릭
$('.btn_verify_code').on('click', function(e) {
e.preventDefault();
const authCode = $('#authCode').val().trim();
const email = $('#email').val();
if (!authCode) {
customPopups.showAlert('인증코드를 입력해주세요.');
return;
}
if (authCode.length !== 6) {
customPopups.showAlert('6자리 인증코드를 입력해주세요.');
return;
}
$.ajax({
url: '/mypage/verify-email-code',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
email: email,
code: authCode
}),
headers: {
[csrfHeaderName]: csrfToken
},
success: function(response) {
clearInterval(timerInterval);
isVerified = true;
customPopups.showAlert('이메일 인증이 완료되었습니다!');
// 알림 확인 후 메인 페이지로 이동
$('#customAlertOkButton').off('click.emailVerify').on('click.emailVerify', function () {
customPopups.hideAlert('#customAlert');
location.href = '/';
});
},
error: function(xhr) {
const errorMsg = xhr.responseJSON?.error || '인증코드가 일치하지 않습니다.';
customPopups.showAlert(errorMsg);
}
});
});
// 인증 완료 버튼 클릭
$('.btn_complete').on('click', function(e) {
e.preventDefault();
if (!isVerified) {
customPopups.showAlert('이메일 인증을 먼저 완료해주세요.');
return;
}
// 메인 페이지로 이동
customPopups.showAlert('이메일 인증이 완료되었습니다. 메인 페이지로 이동합니다.');
setTimeout(function() {
location.href = '/';
}, 1500);
});
});
</script>
</th:block>
</html>