Implement SMS two-factor authentication with AJAX support
This commit is contained in:
@@ -74,6 +74,8 @@ public class MonitoringFrontFilter implements Filter {
|
||||
|| uri.indexOf(request.getContextPath() +"/dashmain3.do") > -1
|
||||
|| uri.indexOf(request.getContextPath() + "/loginForm.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() + "/changePassword.do") > -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 javax.naming.Context;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.InitialDirContext;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
@@ -27,9 +28,13 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.View;
|
||||
|
||||
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.rms.common.context.MonitoringContext;
|
||||
import com.eactive.eai.rms.common.datasource.DataSourceType;
|
||||
@@ -92,8 +97,18 @@ public class MainController implements InterceptorSkipController {
|
||||
@RequestMapping(value = "/loginForm.do")
|
||||
public String loginForm(HttpServletRequest request, Model model) {
|
||||
boolean fullScreen = request.getParameter("fullScreen") != null;
|
||||
boolean emergencyMode = request.getParameter("emergency") != null;
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -103,9 +118,9 @@ public class MainController implements InterceptorSkipController {
|
||||
if (dto == null) {
|
||||
dto = new LoginVo();
|
||||
}
|
||||
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
|
||||
|
||||
try {
|
||||
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에서 호출)
|
||||
* 비밀번호 인증 없이 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 {
|
||||
|
||||
// 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