Implement SMS two-factor authentication with AJAX support
This commit is contained in:
@@ -43,13 +43,6 @@
|
|||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<script src="js/jquery-1.12.1.min.js"></script>
|
|
||||||
<!-- <script src="js/popper.js"></script> -->
|
|
||||||
<script src="js/bootstrap.min.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/login-style.css">
|
|
||||||
<link rel="stylesheet" href="css/login-fontawesome_all.css">
|
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
|
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
|
||||||
@@ -59,13 +52,20 @@
|
|||||||
href="<c:url value="/img/ic_eai_favi_${themeColor}.png"/>"
|
href="<c:url value="/img/ic_eai_favi_${themeColor}.png"/>"
|
||||||
type="image/ico" sizes="16x16" />
|
type="image/ico" sizes="16x16" />
|
||||||
<title><%=systemModeTitle%><%=localeMessage.getString("screen.title")%></title>
|
<title><%=systemModeTitle%><%=localeMessage.getString("screen.title")%></title>
|
||||||
<%-- <jsp:include page="/jsp/common/include/css.jsp"/> --%>
|
<link rel="stylesheet" href="<c:url value="/css/login-style.css"/>">
|
||||||
<jsp:include page="/jsp/common/include/script.jsp"/>
|
<link rel="stylesheet" href="<c:url value="/css/login-fontawesome_all.css"/>">
|
||||||
|
<link rel="stylesheet" href="<c:url value="/addon/bootstrap-4.6.2/css/bootstrap.min.css"/>">
|
||||||
|
<script src="<c:url value="/js/jquery-1.12.1.min.js"/>"></script>
|
||||||
|
<script src="<c:url value="/addon/bootstrap-4.6.2/js/bootstrap.bundle.min.js"/>"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
if( self.name == 'mainFrame' || self.name == 'leftFrame' || self.name == 'topFrame') {
|
if( self.name == 'mainFrame' || self.name == 'leftFrame' || self.name == 'topFrame') {
|
||||||
parent.window.location.href='<c:url value="/"/>';
|
parent.window.location.href='<c:url value="/"/>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SMS 인증 관련 변수
|
||||||
|
var smsAuthTimer = null;
|
||||||
|
var resendTimer = null;
|
||||||
|
|
||||||
function fncPreMain() {
|
function fncPreMain() {
|
||||||
if ($("input[name=userId]").val() ==null || $("input[name=userId]").val().length == 0 ){
|
if ($("input[name=userId]").val() ==null || $("input[name=userId]").val().length == 0 ){
|
||||||
alert("<%= localeMessage.getString("login.checkid") %>");
|
alert("<%= localeMessage.getString("login.checkid") %>");
|
||||||
@@ -83,7 +83,140 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sessionStorage["serviceType"]= $("select[name=serviceType]").val();
|
sessionStorage["serviceType"]= $("select[name=serviceType]").val();
|
||||||
$("#loginForm").submit();
|
|
||||||
|
// AJAX 로그인 요청
|
||||||
|
$.ajax({
|
||||||
|
url: '<c:url value="/login-ajax.do"/>',
|
||||||
|
type: 'POST',
|
||||||
|
data: $('#loginForm').serialize(),
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.smsAuthRequired) {
|
||||||
|
// SMS 인증 필요
|
||||||
|
showSmsAuthModal(response);
|
||||||
|
} else if (response.success) {
|
||||||
|
// 직접 로그인 성공
|
||||||
|
window.location.href = response.redirectUrl;
|
||||||
|
} else {
|
||||||
|
// 로그인 실패
|
||||||
|
alert(response.errorMessage || '로그인에 실패했습니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
alert('로그인 처리 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMS 인증 모달 표시
|
||||||
|
function showSmsAuthModal(data) {
|
||||||
|
$('#smsAuthMaskedPhone').text(data.maskedPhone);
|
||||||
|
$('#smsAuthCode').val('');
|
||||||
|
startExpiryTimer(data.expiresAt);
|
||||||
|
startResendTimer(data.resendableAt);
|
||||||
|
$('#smsAuthModal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유효시간 타이머
|
||||||
|
function startExpiryTimer(expiresAt) {
|
||||||
|
clearInterval(smsAuthTimer);
|
||||||
|
updateRemainingTime(expiresAt);
|
||||||
|
smsAuthTimer = setInterval(function() {
|
||||||
|
var remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
|
||||||
|
$('#smsAuthRemainingTime').text(remaining);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(smsAuthTimer);
|
||||||
|
alert('인증 시간이 만료되었습니다. 다시 시도해주세요.');
|
||||||
|
$('#smsAuthModal').modal('hide');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRemainingTime(expiresAt) {
|
||||||
|
var remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
|
||||||
|
$('#smsAuthRemainingTime').text(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 재발송 타이머
|
||||||
|
function startResendTimer(resendableAt) {
|
||||||
|
$('#btnSmsResend').prop('disabled', true);
|
||||||
|
clearInterval(resendTimer);
|
||||||
|
updateResendButtonText(resendableAt);
|
||||||
|
resendTimer = setInterval(function() {
|
||||||
|
var remaining = Math.max(0, Math.ceil((resendableAt - Date.now()) / 1000));
|
||||||
|
if (remaining <= 0) {
|
||||||
|
$('#btnSmsResend').prop('disabled', false).text('재발송');
|
||||||
|
clearInterval(resendTimer);
|
||||||
|
} else {
|
||||||
|
$('#btnSmsResend').text('재발송 (' + remaining + '초)');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateResendButtonText(resendableAt) {
|
||||||
|
var remaining = Math.max(0, Math.ceil((resendableAt - Date.now()) / 1000));
|
||||||
|
if (remaining > 0) {
|
||||||
|
$('#btnSmsResend').text('재발송 (' + remaining + '초)');
|
||||||
|
} else {
|
||||||
|
$('#btnSmsResend').text('재발송');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMS 인증번호 재발송
|
||||||
|
function resendSmsAuthCode() {
|
||||||
|
$.ajax({
|
||||||
|
url: '<c:url value="/sms-auth/resend.json"/>',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.sent) {
|
||||||
|
startExpiryTimer(data.expiresAt);
|
||||||
|
startResendTimer(data.resendableAt);
|
||||||
|
alert('인증번호가 재발송되었습니다.');
|
||||||
|
} else {
|
||||||
|
alert(data.errorMessage || '재발송에 실패했습니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('재발송 요청 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMS 인증번호 확인
|
||||||
|
function verifySmsAuthCode() {
|
||||||
|
var code = $('#smsAuthCode').val();
|
||||||
|
if (!code || code.length !== 6) {
|
||||||
|
alert('6자리 인증번호를 입력해주세요.');
|
||||||
|
$('#smsAuthCode').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '<c:url value="/sms-auth/verify.json"/>',
|
||||||
|
type: 'POST',
|
||||||
|
data: { authCode: code },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
$('#smsAuthModal').modal('hide');
|
||||||
|
window.location.href = data.redirectUrl;
|
||||||
|
} else {
|
||||||
|
alert(data.errorMessage || '인증에 실패했습니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('인증 요청 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMS 인증 취소
|
||||||
|
function cancelSmsAuth() {
|
||||||
|
clearInterval(smsAuthTimer);
|
||||||
|
clearInterval(resendTimer);
|
||||||
|
$('#smsAuthModal').modal('hide');
|
||||||
}
|
}
|
||||||
function changePwd(){
|
function changePwd(){
|
||||||
if ($("input[name=resetUserId]").val() == null || $("input[name=resetUserId]").val().length == 0 ){
|
if ($("input[name=resetUserId]").val() == null || $("input[name=resetUserId]").val().length == 0 ){
|
||||||
@@ -212,11 +345,13 @@
|
|||||||
<div style="text-align: right !important;" class="form-group">
|
<div style="text-align: right !important;" class="form-group">
|
||||||
<a style="font-size: 12px !important;" href="#pwdChgModal" data-toggle="modal" >Password Change</a>
|
<a style="font-size: 12px !important;" href="#pwdChgModal" data-toggle="modal" >Password Change</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- SSO 로그인 버튼 -->
|
<!-- SSO 로그인 버튼 (비상모드 시 숨김) -->
|
||||||
|
<c:if test="${not emergencyMode}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3"
|
<input type="button" id="btn_sso_login" class="form-control btn btn-success rounded submit px-3"
|
||||||
value="SSO Login">
|
value="SSO Login">
|
||||||
</div>
|
</div>
|
||||||
|
</c:if>
|
||||||
<p style="color:red;"><%=resultMsg %></p>
|
<p style="color:red;"><%=resultMsg %></p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -258,6 +393,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SMS 인증 모달 -->
|
||||||
|
<div class="modal fade" id="smsAuthModal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">SMS 인증</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>인증번호가 <strong><span id="smsAuthMaskedPhone"></span></strong>로 발송되었습니다.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" id="smsAuthCode" class="form-control"
|
||||||
|
placeholder="인증번호 6자리" maxlength="6"
|
||||||
|
onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||||
|
</div>
|
||||||
|
<p class="text-muted">남은 시간: <span id="smsAuthRemainingTime" class="text-danger font-weight-bold">60</span>초</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="btnSmsResend" class="btn btn-secondary" onclick="resendSmsAuthCode()" disabled>재발송</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="verifySmsAuthCode()">확인</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick="cancelSmsAuth()">취소</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ public class MonitoringFrontFilter implements Filter {
|
|||||||
|| uri.indexOf(request.getContextPath() +"/dashmain3.do") > -1
|
|| uri.indexOf(request.getContextPath() +"/dashmain3.do") > -1
|
||||||
|| uri.indexOf(request.getContextPath() + "/loginForm.do") > -1
|
|| uri.indexOf(request.getContextPath() + "/loginForm.do") > -1
|
||||||
|| uri.indexOf(request.getContextPath() + "/login.do") > -1
|
|| uri.indexOf(request.getContextPath() + "/login.do") > -1
|
||||||
|
|| uri.indexOf(request.getContextPath() + "/login-ajax.do") > -1
|
||||||
|
|| uri.indexOf(request.getContextPath() + "/sms-auth/") > -1
|
||||||
|| uri.indexOf(request.getContextPath() + "/ssoLogin.do") > -1
|
|| uri.indexOf(request.getContextPath() + "/ssoLogin.do") > -1
|
||||||
|| uri.indexOf(request.getContextPath() + "/changePassword.do") > -1
|
|| uri.indexOf(request.getContextPath() + "/changePassword.do") > -1
|
||||||
|| uri.indexOf(request.getContextPath() + "/dashboard") > -1
|
|| uri.indexOf(request.getContextPath() + "/dashboard") > -1
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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 LoginResponseDto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 성공 여부
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 2차 인증 필요 여부
|
||||||
|
*/
|
||||||
|
private boolean smsAuthRequired;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마스킹된 전화번호 (예: 010****1234)
|
||||||
|
*/
|
||||||
|
private String maskedPhone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증번호 만료 시각 (timestamp, 밀리초)
|
||||||
|
*/
|
||||||
|
private long expiresAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재발급 가능 시각 (timestamp, 밀리초)
|
||||||
|
*/
|
||||||
|
private long resendableAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리다이렉트 URL (로그인 성공 시)
|
||||||
|
*/
|
||||||
|
private String redirectUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지 (로그인 실패 시)
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.naming.Context;
|
import javax.naming.Context;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import javax.naming.directory.DirContext;
|
import javax.naming.directory.DirContext;
|
||||||
import javax.naming.directory.InitialDirContext;
|
import javax.naming.directory.InitialDirContext;
|
||||||
import javax.servlet.ServletOutputStream;
|
import javax.servlet.ServletOutputStream;
|
||||||
@@ -27,9 +28,13 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.servlet.View;
|
import org.springframework.web.servlet.View;
|
||||||
|
|
||||||
import com.eactive.eai.common.seed.Seed;
|
import com.eactive.eai.common.seed.Seed;
|
||||||
|
import com.eactive.eai.rms.ext.kjb.smsauth.SmsAuthService;
|
||||||
|
import com.eactive.ext.kjb.common.KjbProperty;
|
||||||
|
import com.eactive.ext.kjb.util.KjbPropertyInjector;
|
||||||
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
import com.eactive.eai.data.converter.LocalDateTimeFormatters;
|
||||||
import com.eactive.eai.rms.common.context.MonitoringContext;
|
import com.eactive.eai.rms.common.context.MonitoringContext;
|
||||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
||||||
@@ -92,8 +97,18 @@ public class MainController implements InterceptorSkipController {
|
|||||||
@RequestMapping(value = "/loginForm.do")
|
@RequestMapping(value = "/loginForm.do")
|
||||||
public String loginForm(HttpServletRequest request, Model model) {
|
public String loginForm(HttpServletRequest request, Model model) {
|
||||||
boolean fullScreen = request.getParameter("fullScreen") != null;
|
boolean fullScreen = request.getParameter("fullScreen") != null;
|
||||||
|
boolean emergencyMode = request.getParameter("emergency") != null;
|
||||||
model.addAttribute("fullScreen", fullScreen);
|
model.addAttribute("fullScreen", fullScreen);
|
||||||
|
model.addAttribute("emergencyMode", emergencyMode);
|
||||||
|
|
||||||
|
// 세션에 비상모드 저장 (SMS 인증 우회용)
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
session.setAttribute("emergencyMode", emergencyMode);
|
||||||
|
|
||||||
|
if (emergencyMode) {
|
||||||
|
logger.info("비상 로그인 모드 활성화 - SSO/SMS 인증 우회");
|
||||||
|
}
|
||||||
|
|
||||||
return "/common/screen/emergency";
|
return "/common/screen/emergency";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +118,9 @@ public class MainController implements InterceptorSkipController {
|
|||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
dto = new LoginVo();
|
dto = new LoginVo();
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpSession session = request.getSession();
|
HttpSession session = request.getSession();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initializeLoginAndSetDashboardImage(session);
|
initializeLoginAndSetDashboardImage(session);
|
||||||
|
|
||||||
@@ -120,6 +135,150 @@ public class MainController implements InterceptorSkipController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX 로그인 엔드포인트 (SMS 2차 인증 대응)
|
||||||
|
*/
|
||||||
|
@RequestMapping("/login-ajax.do")
|
||||||
|
@ResponseBody
|
||||||
|
public LoginResponseDto preMainAjax(HttpServletRequest request, LoginVo dto) {
|
||||||
|
if (dto == null) {
|
||||||
|
dto = new LoginVo();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
|
||||||
|
try {
|
||||||
|
initializeLoginAndSetDashboardImage(session);
|
||||||
|
|
||||||
|
// 1. 비밀번호 인증
|
||||||
|
UserInfo userInfo = authenticateAndVerifyUser(request, session, dto);
|
||||||
|
|
||||||
|
if (userInfo == null) {
|
||||||
|
String errorMsg = (String) session.getAttribute(RESULT_MSG);
|
||||||
|
session.removeAttribute(RESULT_MSG);
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage(errorMsg != null ? errorMsg : "로그인에 실패했습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 비상모드 체크 - SMS 인증 우회
|
||||||
|
Boolean emergencyMode = (Boolean) session.getAttribute("emergencyMode");
|
||||||
|
if (Boolean.TRUE.equals(emergencyMode)) {
|
||||||
|
logger.info("비상 로그인 모드 - SMS 인증 우회 - userId: {}", userInfo.getUserid());
|
||||||
|
session.removeAttribute("emergencyMode"); // 사용 후 제거
|
||||||
|
// 바로 로그인 완료로 이동
|
||||||
|
String result = loadUserInfoAndSetupSession(request, session, dto, userInfo);
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
String redirectUrl = contextPath + "/main.do";
|
||||||
|
if (result != null && result.startsWith("redirect:")) {
|
||||||
|
String path = result.substring("redirect:".length());
|
||||||
|
if (!path.startsWith(contextPath)) {
|
||||||
|
redirectUrl = contextPath + path;
|
||||||
|
} else {
|
||||||
|
redirectUrl = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String serviceType = request.getParameter("serviceType");
|
||||||
|
if (serviceType != null && !redirectUrl.contains("serviceType")) {
|
||||||
|
redirectUrl += (redirectUrl.contains("?") ? "&" : "?") + "serviceType=" + serviceType;
|
||||||
|
}
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(true)
|
||||||
|
.smsAuthRequired(false)
|
||||||
|
.redirectUrl(redirectUrl)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SMS 인증 필요 여부 체크
|
||||||
|
SmsAuthService smsAuthService = getSmsAuthService();
|
||||||
|
if (smsAuthService != null && smsAuthService.isEnabled()) {
|
||||||
|
// SMS 인증 활성화 상태
|
||||||
|
if (!smsAuthService.hasValidPhoneNumber(userInfo)) {
|
||||||
|
// 휴대폰번호 없는 사용자
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("휴대폰번호가 등록되어 있지 않아 로그인할 수 없습니다. 관리자에게 문의하세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMS 인증번호 생성 및 발송
|
||||||
|
String authCode = smsAuthService.generateAuthCode();
|
||||||
|
boolean sent = smsAuthService.sendAuthCode(userInfo.getCphnno(), authCode);
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 세션에 SMS 인증 정보 저장
|
||||||
|
smsAuthService.setupAuthSession(session, userInfo, dto, authCode);
|
||||||
|
|
||||||
|
// serviceType 세션에 저장
|
||||||
|
String serviceType = request.getParameter("serviceType");
|
||||||
|
if (serviceType != null) {
|
||||||
|
session.setAttribute("serviceType", serviceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(true)
|
||||||
|
.smsAuthRequired(true)
|
||||||
|
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
|
||||||
|
.expiresAt(smsAuthService.getExpiresAt(session))
|
||||||
|
.resendableAt(smsAuthService.getResendableAt(session))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SMS 인증 비활성화 또는 서비스 없음 - 바로 로그인 완료
|
||||||
|
String result = loadUserInfoAndSetupSession(request, session, dto, userInfo);
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
String redirectUrl = contextPath + "/main.do";
|
||||||
|
if (result != null && result.startsWith("redirect:")) {
|
||||||
|
String path = result.substring("redirect:".length());
|
||||||
|
if (!path.startsWith(contextPath)) {
|
||||||
|
redirectUrl = contextPath + path;
|
||||||
|
} else {
|
||||||
|
redirectUrl = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceType 파라미터 추가
|
||||||
|
String serviceType = request.getParameter("serviceType");
|
||||||
|
if (serviceType != null && !redirectUrl.contains("serviceType")) {
|
||||||
|
redirectUrl += (redirectUrl.contains("?") ? "&" : "?") + "serviceType=" + serviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(true)
|
||||||
|
.smsAuthRequired(false)
|
||||||
|
.redirectUrl(redirectUrl)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("AJAX 로그인 처리 중 오류", e);
|
||||||
|
return LoginResponseDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("로그인 처리 중 오류가 발생했습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SmsAuthService 인스턴스 조회 (lazy initialization)
|
||||||
|
*/
|
||||||
|
private SmsAuthService getSmsAuthService() {
|
||||||
|
try {
|
||||||
|
KjbPropertyInjector injector = new KjbPropertyInjector(monitoringPropertyService, "Monitoring");
|
||||||
|
KjbProperty kjbProperty = injector.inject(new KjbProperty());
|
||||||
|
return new SmsAuthService(kjbProperty);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("SmsAuthService 초기화 실패 - SMS 인증 비활성화 처리", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SSO 로그인용 세션 설정 (SsoController에서 호출)
|
* SSO 로그인용 세션 설정 (SsoController에서 호출)
|
||||||
* 비밀번호 인증 없이 UserInfo만으로 로그인 처리
|
* 비밀번호 인증 없이 UserInfo만으로 로그인 처리
|
||||||
@@ -286,9 +445,12 @@ public class MainController implements InterceptorSkipController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용자 정보 로드 및 세션 설정
|
/**
|
||||||
private String loadUserInfoAndSetupSession(HttpServletRequest request,
|
* 사용자 정보 로드 및 세션 설정
|
||||||
HttpSession session, LoginVo dto, UserInfo userInfo)
|
* public으로 변경하여 SmsAuthController에서 호출 가능
|
||||||
|
*/
|
||||||
|
public String loadUserInfoAndSetupSession(HttpServletRequest request,
|
||||||
|
HttpSession session, LoginVo dto, UserInfo userInfo)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
// LDAP 사용시 주석
|
// LDAP 사용시 주석
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package com.eactive.eai.rms.ext.kjb.smsauth;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||||
|
import com.eactive.eai.rms.common.interceptor.InterceptorSkipController;
|
||||||
|
import com.eactive.eai.rms.common.login.LoginVo;
|
||||||
|
import com.eactive.eai.rms.common.login.MainController;
|
||||||
|
import com.eactive.eai.rms.common.util.CommonConstants;
|
||||||
|
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
|
||||||
|
import com.eactive.ext.kjb.common.KjbProperty;
|
||||||
|
import com.eactive.ext.kjb.util.KjbPropertyInjector;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/sms-auth")
|
||||||
|
public class SmsAuthController implements InterceptorSkipController {
|
||||||
|
|
||||||
|
public static final String PROP_GROUP_ID = "Monitoring";
|
||||||
|
|
||||||
|
private final MainController mainController;
|
||||||
|
private final KjbPropertyInjector kjbPropertyInjector;
|
||||||
|
|
||||||
|
private SmsAuthService smsAuthService;
|
||||||
|
private KjbProperty kjbProperty;
|
||||||
|
|
||||||
|
public SmsAuthController(MonitoringPropertyService monitoringPropertyService,
|
||||||
|
MainController mainController) {
|
||||||
|
this.mainController = mainController;
|
||||||
|
this.kjbPropertyInjector = new KjbPropertyInjector(monitoringPropertyService, PROP_GROUP_ID);
|
||||||
|
reloadProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프로퍼티 리로드 (DB 변경 대응)
|
||||||
|
*/
|
||||||
|
private void reloadProperty() {
|
||||||
|
this.kjbProperty = kjbPropertyInjector.inject(new KjbProperty());
|
||||||
|
this.smsAuthService = new SmsAuthService(kjbProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증번호 발송
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/send.json", method = RequestMethod.POST)
|
||||||
|
@ResponseBody
|
||||||
|
public SmsAuthDto sendAuthCode(HttpServletRequest request) {
|
||||||
|
// 프로퍼티 리로드 (DB 변경 대응)
|
||||||
|
reloadProperty();
|
||||||
|
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
UserInfo userInfo = smsAuthService.getUserInfoFromSession(session);
|
||||||
|
|
||||||
|
if (userInfo == null) {
|
||||||
|
log.warn("SMS 인증 세션 정보 없음 - send 요청");
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(false)
|
||||||
|
.errorMessage("인증 세션이 만료되었습니다. 다시 로그인해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smsAuthService.hasValidPhoneNumber(userInfo)) {
|
||||||
|
log.warn("유효한 휴대폰번호 없음 - userId: {}", userInfo.getUserid());
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(false)
|
||||||
|
.errorMessage("휴대폰번호가 등록되어 있지 않습니다. 관리자에게 문의하세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증번호 생성 및 발송
|
||||||
|
String authCode = smsAuthService.generateAuthCode();
|
||||||
|
boolean sent = smsAuthService.sendAuthCode(userInfo.getCphnno(), authCode);
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
// 세션에 인증번호 저장 (기존 UserInfo, LoginVo 유지)
|
||||||
|
LoginVo loginVo = smsAuthService.getLoginVoFromSession(session);
|
||||||
|
smsAuthService.setupAuthSession(session, userInfo, loginVo, authCode);
|
||||||
|
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(true)
|
||||||
|
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
|
||||||
|
.expiresAt(smsAuthService.getExpiresAt(session))
|
||||||
|
.resendableAt(smsAuthService.getResendableAt(session))
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(false)
|
||||||
|
.errorMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증번호 재발송
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/resend.json", method = RequestMethod.POST)
|
||||||
|
@ResponseBody
|
||||||
|
public SmsAuthDto resendAuthCode(HttpServletRequest request) {
|
||||||
|
// 프로퍼티 리로드 (DB 변경 대응)
|
||||||
|
reloadProperty();
|
||||||
|
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
UserInfo userInfo = smsAuthService.getUserInfoFromSession(session);
|
||||||
|
|
||||||
|
if (userInfo == null) {
|
||||||
|
log.warn("SMS 인증 세션 정보 없음 - resend 요청");
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(false)
|
||||||
|
.errorMessage("인증 세션이 만료되었습니다. 다시 로그인해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smsAuthService.canResend(session)) {
|
||||||
|
long waitTime = smsAuthService.getResendWaitTime(session);
|
||||||
|
log.debug("재발송 대기 중 - 남은 시간: {}ms", waitTime);
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(false)
|
||||||
|
.errorMessage(String.format("%.0f초 후에 재발송이 가능합니다.", Math.ceil(waitTime / 1000.0)))
|
||||||
|
.resendableAt(smsAuthService.getResendableAt(session))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새 인증번호 생성 및 발송
|
||||||
|
String authCode = smsAuthService.generateAuthCode();
|
||||||
|
boolean sent = smsAuthService.sendAuthCode(userInfo.getCphnno(), authCode);
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
LoginVo loginVo = smsAuthService.getLoginVoFromSession(session);
|
||||||
|
smsAuthService.setupAuthSession(session, userInfo, loginVo, authCode);
|
||||||
|
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(true)
|
||||||
|
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
|
||||||
|
.expiresAt(smsAuthService.getExpiresAt(session))
|
||||||
|
.resendableAt(smsAuthService.getResendableAt(session))
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.sent(false)
|
||||||
|
.errorMessage("인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증번호 검증 및 로그인 완료
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/verify.json", method = RequestMethod.POST)
|
||||||
|
@ResponseBody
|
||||||
|
public SmsAuthDto verifyAuthCode(HttpServletRequest request,
|
||||||
|
@RequestParam("authCode") String authCode) {
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
UserInfo userInfo = smsAuthService.getUserInfoFromSession(session);
|
||||||
|
LoginVo loginVo = smsAuthService.getLoginVoFromSession(session);
|
||||||
|
|
||||||
|
if (userInfo == null || loginVo == null) {
|
||||||
|
log.warn("SMS 인증 세션 정보 없음 - verify 요청");
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("인증 세션이 만료되었습니다. 다시 로그인해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smsAuthService.isExpired(session)) {
|
||||||
|
log.warn("SMS 인증번호 만료됨 - userId: {}", userInfo.getUserid());
|
||||||
|
smsAuthService.clearAuthSession(session);
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("인증번호가 만료되었습니다. 다시 시도해주세요.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!smsAuthService.validateAuthCode(session, authCode)) {
|
||||||
|
log.warn("SMS 인증번호 불일치 - userId: {}", userInfo.getUserid());
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("인증번호가 일치하지 않습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증 성공 - 로그인 완료 처리
|
||||||
|
try {
|
||||||
|
smsAuthService.clearAuthSession(session);
|
||||||
|
|
||||||
|
// MainController의 loadUserInfoAndSetupSession 호출
|
||||||
|
String result = mainController.loadUserInfoAndSetupSession(request, session, loginVo, userInfo);
|
||||||
|
|
||||||
|
// result가 "redirect:/main.do" 형태이면 redirectUrl 추출
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
String redirectUrl = contextPath + "/main.do";
|
||||||
|
if (result != null && result.startsWith("redirect:")) {
|
||||||
|
String path = result.substring("redirect:".length());
|
||||||
|
// 이미 contextPath가 포함되어 있지 않으면 추가
|
||||||
|
if (!path.startsWith(contextPath)) {
|
||||||
|
redirectUrl = contextPath + path;
|
||||||
|
} else {
|
||||||
|
redirectUrl = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceType 파라미터 추가
|
||||||
|
String serviceType = request.getParameter("serviceType");
|
||||||
|
if (serviceType == null) {
|
||||||
|
serviceType = (String) session.getAttribute("serviceType");
|
||||||
|
}
|
||||||
|
if (serviceType != null && !redirectUrl.contains("serviceType")) {
|
||||||
|
redirectUrl += (redirectUrl.contains("?") ? "&" : "?") + "serviceType=" + serviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("SMS 인증 성공 - 로그인 완료 - userId: {}", userInfo.getUserid());
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.success(true)
|
||||||
|
.redirectUrl(redirectUrl)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("SMS 인증 후 로그인 처리 실패", e);
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.success(false)
|
||||||
|
.errorMessage("로그인 처리 중 오류가 발생했습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증 상태 조회
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/status.json", method = RequestMethod.GET)
|
||||||
|
@ResponseBody
|
||||||
|
public SmsAuthDto getStatus(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
UserInfo userInfo = smsAuthService.getUserInfoFromSession(session);
|
||||||
|
|
||||||
|
if (userInfo == null) {
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.enabled(false)
|
||||||
|
.errorMessage("인증 세션이 없습니다.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SmsAuthDto.builder()
|
||||||
|
.enabled(smsAuthService.isEnabled())
|
||||||
|
.maskedPhone(smsAuthService.maskPhoneNumber(userInfo.getCphnno()))
|
||||||
|
.expiresAt(smsAuthService.getExpiresAt(session))
|
||||||
|
.resendableAt(smsAuthService.getResendableAt(session))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.eactive.eai.rms.ext.kjb.smsauth;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SmsAuthDto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증 활성화 여부
|
||||||
|
*/
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발송 성공 여부
|
||||||
|
*/
|
||||||
|
private boolean sent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 성공 여부
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마스킹된 전화번호 (예: 010****1234)
|
||||||
|
*/
|
||||||
|
private String maskedPhone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 만료 시각 (timestamp, 밀리초)
|
||||||
|
*/
|
||||||
|
private long expiresAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재발급 가능 시각 (timestamp, 밀리초)
|
||||||
|
*/
|
||||||
|
private long resendableAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리다이렉트 URL (인증 성공 시)
|
||||||
|
*/
|
||||||
|
private String redirectUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지
|
||||||
|
*/
|
||||||
|
private String errorMessage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package com.eactive.eai.rms.ext.kjb.smsauth;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||||
|
import com.eactive.eai.rms.common.login.LoginVo;
|
||||||
|
import com.eactive.ext.kjb.common.KjbProperty;
|
||||||
|
import com.eactive.ext.kjb.common.KjbUmsException;
|
||||||
|
import com.eactive.ext.kjb.ums.KjbUmsService;
|
||||||
|
import com.eactive.ext.kjb.ums.UmsBizWorkCode;
|
||||||
|
import com.eactive.ext.kjb.ums.gson.VerifyPhoneUmsTemplate;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class SmsAuthService {
|
||||||
|
|
||||||
|
// 세션 키
|
||||||
|
public static final String SESSION_KEY_AUTH_CODE = "SMS_AUTH_CODE";
|
||||||
|
public static final String SESSION_KEY_ISSUED_AT = "SMS_AUTH_ISSUED_AT";
|
||||||
|
public static final String SESSION_KEY_USER_INFO = "SMS_AUTH_USER_INFO";
|
||||||
|
public static final String SESSION_KEY_LOGIN_VO = "SMS_AUTH_LOGIN_VO";
|
||||||
|
|
||||||
|
// 시간 설정 (밀리초)
|
||||||
|
public static final long EXPIRY_MS = 60_000; // 1분
|
||||||
|
public static final long RESEND_INTERVAL_MS = 20_000; // 20초
|
||||||
|
|
||||||
|
// 인증번호 생성 모드
|
||||||
|
public static final String MODE_REAL = "real";
|
||||||
|
public static final String MODE_HHMM00 = "hhmm00";
|
||||||
|
public static final String MODE_FIXED = "fixed";
|
||||||
|
|
||||||
|
private final KjbProperty kjbProperty;
|
||||||
|
private final KjbUmsService kjbUmsService;
|
||||||
|
private final SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
|
public SmsAuthService(KjbProperty kjbProperty) {
|
||||||
|
this.kjbProperty = kjbProperty;
|
||||||
|
this.kjbUmsService = new KjbUmsService(kjbProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증이 필요한지 확인
|
||||||
|
*/
|
||||||
|
public boolean isRequired(UserInfo userInfo) {
|
||||||
|
if (!kjbProperty.isSmsAuthEnabled()) {
|
||||||
|
log.debug("SMS 인증 비활성화 상태");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String phone = userInfo.getCphnno();
|
||||||
|
if (StringUtils.isEmpty(phone)) {
|
||||||
|
log.warn("SMS 인증 필요하나 휴대폰번호 없음 - userId: {}", userInfo.getUserid());
|
||||||
|
// 전화번호 없으면 인증 불가이므로 isRequired는 true 반환하고
|
||||||
|
// 컨트롤러에서 에러 처리
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 휴대폰번호가 유효한지 확인
|
||||||
|
*/
|
||||||
|
public boolean hasValidPhoneNumber(UserInfo userInfo) {
|
||||||
|
String phone = userInfo.getCphnno();
|
||||||
|
return StringUtils.isNotEmpty(phone) && phone.replaceAll("[^0-9]", "").length() >= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증번호 생성
|
||||||
|
*/
|
||||||
|
public String generateAuthCode() {
|
||||||
|
String mode = kjbProperty.getSmsAuthMode();
|
||||||
|
|
||||||
|
if (MODE_FIXED.equalsIgnoreCase(mode)) {
|
||||||
|
String fixedValue = kjbProperty.getSmsAuthFixedValue();
|
||||||
|
log.debug("SMS 인증번호 생성 (fixed 모드): {}", fixedValue);
|
||||||
|
return fixedValue;
|
||||||
|
} else if (MODE_HHMM00.equalsIgnoreCase(mode)) {
|
||||||
|
String timeCode = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HHmm")) + "00";
|
||||||
|
log.debug("SMS 인증번호 생성 (hhmm00 모드): {}", timeCode);
|
||||||
|
return timeCode;
|
||||||
|
} else {
|
||||||
|
// real 모드 - 랜덤 6자리
|
||||||
|
String randomCode = String.format("%06d", random.nextInt(1000000));
|
||||||
|
log.debug("SMS 인증번호 생성 (real 모드): {}", randomCode);
|
||||||
|
return randomCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증번호 발송
|
||||||
|
*/
|
||||||
|
public boolean sendAuthCode(String phone, String authCode) {
|
||||||
|
try {
|
||||||
|
String cleanedPhone = phone.replaceAll("[^0-9]", "").trim();
|
||||||
|
String guid = String.format("EAPIM_EMS-%s",
|
||||||
|
DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now()));
|
||||||
|
|
||||||
|
VerifyPhoneUmsTemplate content = VerifyPhoneUmsTemplate.builder()
|
||||||
|
.authNumber(authCode)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
kjbUmsService.sendKakaoAlimTalk(guid, UmsBizWorkCode.VERIFY_PHONE, cleanedPhone, content);
|
||||||
|
log.info("SMS 인증번호 발송 완료 - guid: {}, phone: {}****{}",
|
||||||
|
guid, cleanedPhone.substring(0, 3), cleanedPhone.substring(cleanedPhone.length() - 4));
|
||||||
|
log.debug("SMS 인증번호 발송 - authCode: {}", authCode);
|
||||||
|
return true;
|
||||||
|
} catch (KjbUmsException e) {
|
||||||
|
log.error("SMS 인증번호 발송 실패", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세션에 SMS 인증 정보 저장
|
||||||
|
*/
|
||||||
|
public void setupAuthSession(HttpSession session, UserInfo userInfo, LoginVo loginVo, String authCode) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
session.setAttribute(SESSION_KEY_AUTH_CODE, authCode);
|
||||||
|
session.setAttribute(SESSION_KEY_ISSUED_AT, now);
|
||||||
|
session.setAttribute(SESSION_KEY_USER_INFO, userInfo);
|
||||||
|
session.setAttribute(SESSION_KEY_LOGIN_VO, loginVo);
|
||||||
|
log.debug("SMS 인증 세션 설정 완료 - userId: {}", userInfo.getUserid());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증번호 검증
|
||||||
|
*/
|
||||||
|
public boolean validateAuthCode(HttpSession session, String inputCode) {
|
||||||
|
String storedCode = (String) session.getAttribute(SESSION_KEY_AUTH_CODE);
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
|
||||||
|
if (storedCode == null || issuedAt == null) {
|
||||||
|
log.warn("SMS 인증 세션 정보 없음");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpired(session)) {
|
||||||
|
log.warn("SMS 인증번호 만료됨");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean valid = storedCode.equals(inputCode);
|
||||||
|
if (valid) {
|
||||||
|
log.info("SMS 인증번호 검증 성공");
|
||||||
|
} else {
|
||||||
|
log.warn("SMS 인증번호 불일치");
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증번호 만료 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean isExpired(HttpSession session) {
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
if (issuedAt == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis() - issuedAt > EXPIRY_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재발송 가능 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean canResend(HttpSession session) {
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
if (issuedAt == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return System.currentTimeMillis() - issuedAt >= RESEND_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 남은 유효 시간 (밀리초)
|
||||||
|
*/
|
||||||
|
public long getRemainingTime(HttpSession session) {
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
if (issuedAt == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long remaining = EXPIRY_MS - (System.currentTimeMillis() - issuedAt);
|
||||||
|
return Math.max(0, remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재발송 가능 시간까지 남은 시간 (밀리초)
|
||||||
|
*/
|
||||||
|
public long getResendWaitTime(HttpSession session) {
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
if (issuedAt == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long wait = RESEND_INTERVAL_MS - (System.currentTimeMillis() - issuedAt);
|
||||||
|
return Math.max(0, wait);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세션에서 UserInfo 조회
|
||||||
|
*/
|
||||||
|
public UserInfo getUserInfoFromSession(HttpSession session) {
|
||||||
|
return (UserInfo) session.getAttribute(SESSION_KEY_USER_INFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세션에서 LoginVo 조회
|
||||||
|
*/
|
||||||
|
public LoginVo getLoginVoFromSession(HttpSession session) {
|
||||||
|
return (LoginVo) session.getAttribute(SESSION_KEY_LOGIN_VO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증 세션 정보 클리어
|
||||||
|
*/
|
||||||
|
public void clearAuthSession(HttpSession session) {
|
||||||
|
session.removeAttribute(SESSION_KEY_AUTH_CODE);
|
||||||
|
session.removeAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
session.removeAttribute(SESSION_KEY_USER_INFO);
|
||||||
|
session.removeAttribute(SESSION_KEY_LOGIN_VO);
|
||||||
|
log.debug("SMS 인증 세션 정보 클리어 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 만료 시각 (timestamp) 반환
|
||||||
|
*/
|
||||||
|
public long getExpiresAt(HttpSession session) {
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
if (issuedAt == null) {
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
return issuedAt + EXPIRY_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재발송 가능 시각 (timestamp) 반환
|
||||||
|
*/
|
||||||
|
public long getResendableAt(HttpSession session) {
|
||||||
|
Long issuedAt = (Long) session.getAttribute(SESSION_KEY_ISSUED_AT);
|
||||||
|
if (issuedAt == null) {
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
return issuedAt + RESEND_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전화번호 마스킹
|
||||||
|
*/
|
||||||
|
public String maskPhoneNumber(String phone) {
|
||||||
|
if (StringUtils.isEmpty(phone)) {
|
||||||
|
return "****";
|
||||||
|
}
|
||||||
|
String cleaned = phone.replaceAll("[^0-9]", "");
|
||||||
|
if (cleaned.length() < 7) {
|
||||||
|
return "****";
|
||||||
|
}
|
||||||
|
return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMS 인증 활성화 여부
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return kjbProperty.isSmsAuthEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user