충돌 해결
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.eactive.apim.portal.apps.app.controller;
|
package com.eactive.apim.portal.apps.app.controller;
|
||||||
|
|
||||||
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
import com.eactive.apim.portal.apprequest.entity.AppRequest;
|
||||||
|
import com.eactive.apim.portal.approval.statemachine.InvalidApprovalTransitionException;
|
||||||
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
|
||||||
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
import com.eactive.apim.portal.apps.apis.service.ApiService;
|
||||||
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
|
||||||
@@ -178,7 +179,6 @@ public class MyAppController {
|
|||||||
|
|
||||||
model.addAttribute("apiKey", apiKey);
|
model.addAttribute("apiKey", apiKey);
|
||||||
model.addAttribute("secretAvailable", secretAvailable);
|
model.addAttribute("secretAvailable", secretAvailable);
|
||||||
model.addAttribute("authType", "OAuth2");
|
|
||||||
|
|
||||||
return new ModelAndView(CREDENTIAL_DETAIL);
|
return new ModelAndView(CREDENTIAL_DETAIL);
|
||||||
}
|
}
|
||||||
@@ -217,9 +217,17 @@ public class MyAppController {
|
|||||||
appServiceFacade.cancelApiRequest(id, SecurityUtil.getPortalAuthenticatedUser().getPortalOrg());
|
appServiceFacade.cancelApiRequest(id, SecurityUtil.getPortalAuthenticatedUser().getPortalOrg());
|
||||||
result.put("success", true);
|
result.put("success", true);
|
||||||
result.put("message", "신청이 취소되었습니다.");
|
result.put("message", "신청이 취소되었습니다.");
|
||||||
} catch (Exception e) {
|
} catch (InvalidApprovalTransitionException e) {
|
||||||
result.put("success", false);
|
result.put("success", false);
|
||||||
result.put("message", "신청 취소 중 오류가 발생했습니다: " + e.getMessage());
|
result.put("message", "내부 결재가 진행 중이라 신청을 취소할 수 없습니다. 취소가 필요한 경우 관리자에게 문의해 주세요.");
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
log.error("API Key 신청 취소 중 GW 차단 실패. id={}", id, e);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("API Key 신청 취소 실패. id={}", id, e);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "신청 취소 중 오류가 발생했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public class AppServiceFacade {
|
|||||||
private final ApiServiceHelper apiServiceHelper;
|
private final ApiServiceHelper apiServiceHelper;
|
||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final AdminGatewayClient adminGatewayClient;
|
||||||
|
|
||||||
public List<ClientDTO> getApikeyList(PortalOrg portalOrg) {
|
public List<ClientDTO> getApikeyList(PortalOrg portalOrg) {
|
||||||
|
|
||||||
@@ -68,9 +69,13 @@ public class AppServiceFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<AppRequest> getPendingApiKeyList(PortalOrg portalOrg) {
|
public List<AppRequest> getPendingApiKeyList(PortalOrg portalOrg) {
|
||||||
List<AppRequest> appRequests = appRequestRepository.findAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(portalOrg, Arrays.asList(AppRequestType.NEW, AppRequestType.MODIFY, AppRequestType.DELETE),
|
List<AppRequestType> types = Arrays.asList(AppRequestType.NEW, AppRequestType.MODIFY, AppRequestType.DELETE);
|
||||||
|
List<AppRequest> appRequests = appRequestRepository.findAllByOrgAndTypeIsInAndApproval_ApprovalStatusIn(portalOrg, types,
|
||||||
Arrays.asList(new ProcessingState(), new RequestedState()));
|
Arrays.asList(new ProcessingState(), new RequestedState()));
|
||||||
|
|
||||||
|
// 승인정보(approval) 없는 신청도 목록에 노출한다. (사용자가 직접 삭제 가능)
|
||||||
|
appRequests.addAll(appRequestRepository.findAllByOrgAndTypeIsInAndApprovalIsNull(portalOrg, types));
|
||||||
|
|
||||||
return appRequests;
|
return appRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +139,23 @@ public class AppServiceFacade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void cancelApiRequest(String id, PortalOrg portalOrg) {
|
public void cancelApiRequest(String id, PortalOrg portalOrg) {
|
||||||
appRequestRepository.findByIdAndOrg(id, portalOrg).ifPresent(approvalService::cancelAppApproval);
|
appRequestRepository.findByIdAndOrg(id, portalOrg).ifPresent(request -> {
|
||||||
|
if (request.getApproval() == null) {
|
||||||
|
// 승인정보 없는 신청은 결재 워크플로우가 없으므로 즉시 삭제.
|
||||||
|
// 단, GW에 클라이언트가 존재할 수 있으므로 차단(appstatus=0)+리로드를 먼저 수행하고
|
||||||
|
// 실패 시 삭제를 중단한다. (/api_key_delete 와 동일한 순서)
|
||||||
|
if (StringUtils.isNotBlank(request.getClientId())) {
|
||||||
|
try {
|
||||||
|
adminGatewayClient.blockClient(request.getClientId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("게이트웨이 차단 처리에 실패하여 삭제를 중단했습니다. 잠시 후 다시 시도해 주세요.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appRequestRepository.delete(request);
|
||||||
|
} else {
|
||||||
|
approvalService.cancelAppApproval(request);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -164,9 +185,15 @@ public class AppServiceFacade {
|
|||||||
Map<String, ApiServiceDTO> mainIconsMap = apiServiceHelper.getMainIconsFromServiceDtos(apiServices);
|
Map<String, ApiServiceDTO> mainIconsMap = apiServiceHelper.getMainIconsFromServiceDtos(apiServices);
|
||||||
|
|
||||||
for (String apiId : apiList) {
|
for (String apiId : apiList) {
|
||||||
ApiServiceDTO serviceDTO = mainIconsMap.get(apiId);
|
// 신청 이후 API 스펙/그룹이 삭제된 경우 null 가능
|
||||||
ApiSpecInfoDto spec = apiService.selectDetail(apiId);
|
ApiSpecInfoDto spec = apiService.selectDetail(apiId);
|
||||||
|
if (spec == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ApiServiceDTO serviceDTO = mainIconsMap.get(apiId);
|
||||||
|
if (serviceDTO != null) {
|
||||||
spec.setService(serviceDTO.getGroupName());
|
spec.setService(serviceDTO.getGroupName());
|
||||||
|
}
|
||||||
appRequest.getApiSpecList().add(spec);
|
appRequest.getApiSpecList().add(spec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class AccountController {
|
|||||||
private final UserSessionService userSessionService;
|
private final UserSessionService userSessionService;
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/confirm_password")
|
@PostMapping("/password/confirm")
|
||||||
public ResponseEntity<ValidationResponse> confirmPassword(@RequestParam String inputPassword) {
|
public ResponseEntity<ValidationResponse> confirmPassword(@RequestParam String inputPassword) {
|
||||||
String currentLoginId = SecurityUtil.getCurrentLoginId();
|
String currentLoginId = SecurityUtil.getCurrentLoginId();
|
||||||
boolean isPasswordCorrect = userFacade.verifyCurrentPassword(currentLoginId, inputPassword);
|
boolean isPasswordCorrect = userFacade.verifyCurrentPassword(currentLoginId, inputPassword);
|
||||||
@@ -60,19 +60,23 @@ public class AccountController {
|
|||||||
return ResponseEntity.ok(new ValidationResponse(isPasswordCorrect, message));
|
return ResponseEntity.ok(new ValidationResponse(isPasswordCorrect, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/change_password")
|
@GetMapping("/password/verify")
|
||||||
public String showChangePasswordPage(Model model) {
|
public String showChangePasswordPage(Model model, HttpSession session) {
|
||||||
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
||||||
|
// ENFORCE 강제 상태면 변경 페이지에 "변경/로그아웃" 강제 팝업을 띄운다.
|
||||||
|
if (Boolean.TRUE.equals(session.getAttribute("pwEnforce"))) {
|
||||||
|
model.addAttribute("forcedPasswordReset", true);
|
||||||
|
}
|
||||||
return "apps/mypage/passwordChangeEntry";
|
return "apps/mypage/passwordChangeEntry";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/new_password")
|
@GetMapping("/password/change")
|
||||||
public String showNewPasswordPage(Model model) {
|
public String showNewPasswordPage(Model model) {
|
||||||
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
||||||
return "apps/mypage/passwordChange";
|
return "apps/mypage/passwordChange";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/verify_current_password")
|
@PostMapping("/password/verify")
|
||||||
public String verifyCurrentPassword(@RequestParam String currentPassword, RedirectAttributes redirectAttributes, HttpSession session, Model model) {
|
public String verifyCurrentPassword(@RequestParam String currentPassword, RedirectAttributes redirectAttributes, HttpSession session, Model model) {
|
||||||
String currentLoginId = SecurityUtil.getCurrentLoginId();
|
String currentLoginId = SecurityUtil.getCurrentLoginId();
|
||||||
if (userFacade.verifyCurrentPassword(currentLoginId, currentPassword)) {
|
if (userFacade.verifyCurrentPassword(currentLoginId, currentPassword)) {
|
||||||
@@ -80,11 +84,11 @@ public class AccountController {
|
|||||||
return "apps/mypage/passwordChange";
|
return "apps/mypage/passwordChange";
|
||||||
} else {
|
} else {
|
||||||
redirectAttributes.addFlashAttribute("error", "현재 비밀번호가 일치하지 않습니다.");
|
redirectAttributes.addFlashAttribute("error", "현재 비밀번호가 일치하지 않습니다.");
|
||||||
return "redirect:/change_password";
|
return "redirect:/password/verify";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/mypage/change_new_password")
|
@PostMapping("/password/change")
|
||||||
public String updatePassword(@RequestParam String newPassword,
|
public String updatePassword(@RequestParam String newPassword,
|
||||||
@RequestParam String confirmPassword,
|
@RequestParam String confirmPassword,
|
||||||
HttpSession session,
|
HttpSession session,
|
||||||
@@ -96,10 +100,11 @@ public class AccountController {
|
|||||||
String currentLoginId = SecurityUtil.getCurrentLoginId();
|
String currentLoginId = SecurityUtil.getCurrentLoginId();
|
||||||
userFacade.updatePassword(currentLoginId, newPassword, confirmPassword);
|
userFacade.updatePassword(currentLoginId, newPassword, confirmPassword);
|
||||||
|
|
||||||
// 비밀번호 만료 관련 세션 속성 제거
|
// 비밀번호 만료/강제 관련 세션 속성 제거
|
||||||
session.removeAttribute("passwordExpired");
|
session.removeAttribute("passwordExpired");
|
||||||
session.removeAttribute("success");
|
session.removeAttribute("success");
|
||||||
session.removeAttribute("redirectUrl");
|
session.removeAttribute("redirectUrl");
|
||||||
|
session.removeAttribute("pwEnforce");
|
||||||
|
|
||||||
// 세션 무효화 전에 DB 세션 레코드를 정리한다.
|
// 세션 무효화 전에 DB 세션 레코드를 정리한다.
|
||||||
// SecurityContextLogoutHandler 는 HTTP 세션만 invalidate 하고 UserSession DB 레코드는
|
// SecurityContextLogoutHandler 는 HTTP 세션만 invalidate 하고 UserSession DB 레코드는
|
||||||
@@ -113,11 +118,16 @@ public class AccountController {
|
|||||||
redirectAttributes.addFlashAttribute("success", "비밀번호가 성공적으로 변경되었습니다.");
|
redirectAttributes.addFlashAttribute("success", "비밀번호가 성공적으로 변경되었습니다.");
|
||||||
return "redirect:/login";
|
return "redirect:/login";
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 검증 실패(비밀번호 규칙/이력 등) — 사용자에게 안내, 스택은 불필요
|
||||||
|
logger.warn("비밀번호 변경 검증 실패: {}", e.getMessage());
|
||||||
model.addAttribute("error", e.getMessage());
|
model.addAttribute("error", e.getMessage());
|
||||||
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
||||||
return "apps/mypage/passwordChange";
|
return "apps/mypage/passwordChange";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
model.addAttribute("error", e.getMessage());
|
// 예기치 못한 오류(트랜잭션 롤백 등) — 원인 추적을 위해 스택은 남기되,
|
||||||
|
// 사용자에게는 시스템 예외 메시지를 노출하지 않고 일반 안내만 보여준다.
|
||||||
|
logger.error("비밀번호 변경 처리 중 오류", e);
|
||||||
|
model.addAttribute("error", "비밀번호 변경 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
||||||
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
model.addAttribute("passwordChangeRequest", new PasswordChangeRequestDTO());
|
||||||
return "apps/mypage/passwordChange";
|
return "apps/mypage/passwordChange";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.eactive.apim.portal.apps.user.dto;
|
|||||||
import com.eactive.apim.portal.common.validator.AuthNumberMatch;
|
import com.eactive.apim.portal.common.validator.AuthNumberMatch;
|
||||||
import com.eactive.apim.portal.common.validator.CellPhone;
|
import com.eactive.apim.portal.common.validator.CellPhone;
|
||||||
import com.eactive.apim.portal.common.validator.PasswordMatch;
|
import com.eactive.apim.portal.common.validator.PasswordMatch;
|
||||||
import com.eactive.apim.portal.common.validator.PasswordRuleForDjbank;
|
import com.eactive.apim.portal.common.validator.PasswordRule;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.hibernate.validator.constraints.Length;
|
import org.hibernate.validator.constraints.Length;
|
||||||
import org.hibernate.validator.constraints.NotEmpty;
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
@@ -12,7 +12,7 @@ import org.hibernate.validator.constraints.NotEmpty;
|
|||||||
@AuthNumberMatch(recipient = "loginId", authField = "authNumber")
|
@AuthNumberMatch(recipient = "loginId", authField = "authNumber")
|
||||||
@PasswordMatch(input = "password", confirm = "password2")
|
@PasswordMatch(input = "password", confirm = "password2")
|
||||||
@Data
|
@Data
|
||||||
@PasswordRuleForDjbank(password = "password", loginId = "loginId", mobile = "mobileNumber")
|
@PasswordRule(password = "password", loginId = "loginId", mobile = "mobileNumber")
|
||||||
public class PortalUserRegistrationDTO {
|
public class PortalUserRegistrationDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.eactive.apim.portal.apps.user.dto;
|
package com.eactive.apim.portal.apps.user.dto;
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.validator.PasswordMatch;
|
import com.eactive.apim.portal.common.validator.PasswordMatch;
|
||||||
import com.eactive.apim.portal.common.validator.PasswordRuleForDjbank;
|
import com.eactive.apim.portal.common.validator.PasswordRule;
|
||||||
import com.eactive.apim.portal.common.validator.UniqueId;
|
import com.eactive.apim.portal.common.validator.UniqueId;
|
||||||
import com.eactive.apim.portal.portaluser.entity.UserStatus;
|
import com.eactive.apim.portal.portaluser.entity.UserStatus;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -13,7 +13,7 @@ import java.io.Serializable;
|
|||||||
|
|
||||||
@PasswordMatch(input = "password", confirm = "password2")
|
@PasswordMatch(input = "password", confirm = "password2")
|
||||||
@Data
|
@Data
|
||||||
@PasswordRuleForDjbank(loginId = "userId", password = "password", mobile = "mobilePhone")
|
@PasswordRule(loginId = "userId", password = "password", mobile = "mobilePhone")
|
||||||
public class UserRegisterDTO implements Serializable {
|
public class UserRegisterDTO implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public class UserRegisterFacadeImpl implements UserRegisterFacade {
|
|||||||
@Override
|
@Override
|
||||||
public ValidationResponse checkPassword(String password, String loginId, String mobileNumber) {
|
public ValidationResponse checkPassword(String password, String loginId, String mobileNumber) {
|
||||||
boolean isValid = passwordValidator.isValidPassword(password, loginId, mobileNumber);
|
boolean isValid = passwordValidator.isValidPassword(password, loginId, mobileNumber);
|
||||||
String message = isValid ? "유효한 비밀번호입니다." : "비밀번호는 영문/숫자/특수문자 포함 8~20자, 로그인 아이디, 휴대폰 번호, 3자리 이상 연속, 반복 문자 사용 불가능 합니다.";
|
String message = isValid ? "유효한 비밀번호입니다." : "비밀번호는 영문/숫자/특수문자 포함 8~50자, 로그인 아이디, 휴대폰 번호, 3자리 이상 연속, 반복 문자 사용 불가능 합니다.";
|
||||||
return new ValidationResponse(isValid, message);
|
return new ValidationResponse(isValid, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ public class PasswordService {
|
|||||||
// 새 비밀번호 설정
|
// 새 비밀번호 설정
|
||||||
String newPasswordHash = passwordEncoder.encode(newPassword);
|
String newPasswordHash = passwordEncoder.encode(newPassword);
|
||||||
user.setPasswordHash(newPasswordHash);
|
user.setPasswordHash(newPasswordHash);
|
||||||
|
// 변경일 기록 → 재설정 강제(null 트리거) 해제
|
||||||
|
user.setPasswordChangeDate(LocalDateTime.now());
|
||||||
portalUserRepository.save(user);
|
portalUserRepository.save(user);
|
||||||
|
|
||||||
savePasswordHistory(user.getId(), newPasswordHash);
|
savePasswordHistory(user.getId(), newPasswordHash);
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ public class PortalUserService {
|
|||||||
user.setPasswordHash(passwordEncoder.encode(dto.getPassword()));
|
user.setPasswordHash(passwordEncoder.encode(dto.getPassword()));
|
||||||
user.setMobileNumber(dto.getMobileNumber());
|
user.setMobileNumber(dto.getMobileNumber());
|
||||||
user.setEmailAddr(normalizedEmail);
|
user.setEmailAddr(normalizedEmail);
|
||||||
|
// 가입 시점을 비밀번호 변경일로 기록 → 신규 가입자는 재설정 강제 대상에서 제외된다.
|
||||||
|
user.setPasswordChangeDate(java.time.LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.eactive.apim.portal.apps.user.validator;
|
package com.eactive.apim.portal.apps.user.validator;
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.validator.PasswordRuleForKbankValidator;
|
import com.eactive.apim.portal.common.validator.PasswordRuleValidator;
|
||||||
import com.eactive.apim.portal.common.validator.PasswordRuleForDjbankValidator;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -17,13 +16,13 @@ public class PasswordValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValidPassword(String password, String loginId, String mobileNumber) {
|
public boolean isValidPassword(String password, String loginId, String mobileNumber) {
|
||||||
PasswordRuleForDjbankValidator validator = new PasswordRuleForDjbankValidator();
|
PasswordRuleValidator validator = new PasswordRuleValidator();
|
||||||
return validator.isValid(password, loginId, mobileNumber);
|
return validator.isValid(password, loginId, mobileNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidLengthAndCharacters(String password) {
|
private boolean isValidLengthAndCharacters(String password) {
|
||||||
final int MIN = 8;
|
final int MIN = 8;
|
||||||
final int MAX = 20;
|
final int MAX = 50;
|
||||||
final String REGEX = "^(?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "}$";
|
final String REGEX = "^(?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "}$";
|
||||||
return password.matches(REGEX);
|
return password.matches(REGEX);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.eactive.apim.portal.common.dto;
|
package com.eactive.apim.portal.common.dto;
|
||||||
|
|
||||||
import com.eactive.apim.portal.common.validator.PasswordRuleForKbank;
|
import com.eactive.apim.portal.common.validator.PasswordRule;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@PasswordRuleForKbank(password = "password", loginId = "loginId", mobile = "mobile")
|
@PasswordRule(password = "password", loginId = "loginId", mobile = "mobile")
|
||||||
public class PasswordValidationDTO {
|
public class PasswordValidationDTO {
|
||||||
private String password;
|
private String password;
|
||||||
private String loginId;
|
private String loginId;
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ import javax.validation.Payload;
|
|||||||
import java.lang.annotation.*;
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
@Constraint(validatedBy = PasswordRuleValidator.class)
|
@Constraint(validatedBy = PasswordRuleValidator.class)
|
||||||
@Target({ElementType.FIELD})
|
@Target({ElementType.TYPE})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
public @interface PasswordRule {
|
public @interface PasswordRule {
|
||||||
String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~20자, 3자리 이상 연속,반복 문자 불가)";
|
String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~50자, 아이디, 휴대전화, 3자리 이상 연속,반복 문자 불가)";
|
||||||
|
|
||||||
Class<?>[] groups() default {};
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
Class<? extends Payload>[] payload() default {};
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
|
||||||
|
String password();
|
||||||
|
|
||||||
|
String loginId();
|
||||||
|
|
||||||
|
String mobile();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
|
||||||
|
|
||||||
import javax.validation.Constraint;
|
|
||||||
import javax.validation.Payload;
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Constraint(validatedBy = PasswordRuleForDjbankValidator.class)
|
|
||||||
@Target({ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface PasswordRuleForDjbank {
|
|
||||||
String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~20자, 아이디, 휴대전화, 3자리 이상 연속,반복 문자 불가)";
|
|
||||||
|
|
||||||
Class<?>[] groups() default {};
|
|
||||||
|
|
||||||
Class<? extends Payload>[] payload() default {};
|
|
||||||
|
|
||||||
String password();
|
|
||||||
|
|
||||||
String loginId();
|
|
||||||
|
|
||||||
String mobile();
|
|
||||||
|
|
||||||
}
|
|
||||||
-144
@@ -1,144 +0,0 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
|
||||||
|
|
||||||
import org.apache.commons.beanutils.PropertyUtils;
|
|
||||||
|
|
||||||
import javax.validation.ConstraintValidator;
|
|
||||||
import javax.validation.ConstraintValidatorContext;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Sungpil Hyun
|
|
||||||
*/
|
|
||||||
public class PasswordRuleForDjbankValidator implements ConstraintValidator<PasswordRuleForDjbank, Object> {
|
|
||||||
|
|
||||||
// 최소 8자, 최대 20자 상수 선언
|
|
||||||
private static final int MIN = 8;
|
|
||||||
private static final int MAX = 20;
|
|
||||||
|
|
||||||
private String password;
|
|
||||||
private String loginId;
|
|
||||||
private String mobileNumber;
|
|
||||||
|
|
||||||
// 3자리 연속 문자 정규식
|
|
||||||
private static final String SAMEPT = "(\\w)\\1\\1";
|
|
||||||
// 공백 문자 정규식
|
|
||||||
private static final String BLANKPT = "(\\s)";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(PasswordRuleForDjbank constraintAnnotation) {
|
|
||||||
this.password = constraintAnnotation.password();
|
|
||||||
this.loginId = constraintAnnotation.loginId();
|
|
||||||
this.mobileNumber = constraintAnnotation.mobile();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
|
|
||||||
|
|
||||||
String passwordValue = null;
|
|
||||||
String loginIdValue = null;
|
|
||||||
String mobileNumberValue = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
passwordValue = (String) PropertyUtils.getProperty(value, this.password);
|
|
||||||
loginIdValue = (String) PropertyUtils.getProperty(value, this.loginId);
|
|
||||||
mobileNumberValue = (String) PropertyUtils.getProperty(value, this.mobileNumber);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid(passwordValue, loginIdValue, mobileNumberValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValid(String password, String loginId, String mobileNumber) {
|
|
||||||
// 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식
|
|
||||||
String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$";
|
|
||||||
|
|
||||||
// 정규식 검사객체
|
|
||||||
Matcher matcher;
|
|
||||||
|
|
||||||
// 공백 체크
|
|
||||||
if (password == null || "".equals(password)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASCII 문자 비교를 위한 UpperCase
|
|
||||||
String tmpPw = password.toUpperCase();
|
|
||||||
// 문자열 길이
|
|
||||||
int strLen = tmpPw.length();
|
|
||||||
|
|
||||||
// 글자 길이 체크
|
|
||||||
if (strLen > 20 || strLen < 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginId != null && !loginId.isEmpty()) {
|
|
||||||
String[] loginParts = loginId.split("@");
|
|
||||||
if (loginParts.length > 0) {
|
|
||||||
String username = loginParts[0].toUpperCase();
|
|
||||||
if (tmpPw.contains(username)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile number validation
|
|
||||||
if (mobileNumber != null && !mobileNumber.isEmpty()) {
|
|
||||||
String[] mobileParts = mobileNumber.split("-");
|
|
||||||
for (String part : mobileParts) {
|
|
||||||
if (!part.isEmpty() && tmpPw.contains(part)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 공백 체크
|
|
||||||
matcher = Pattern.compile(BLANKPT).matcher(tmpPw);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비밀번호 정규식 체크
|
|
||||||
matcher = Pattern.compile(REGEX).matcher(tmpPw);
|
|
||||||
if (!matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 동일한 문자 3개 이상 체크
|
|
||||||
matcher = Pattern.compile(SAMEPT).matcher(tmpPw);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 연속된 문자 / 숫자 3개 이상 체크
|
|
||||||
// ASCII Char를 담을 배열 선언
|
|
||||||
int[] tmpArray = new int[strLen];
|
|
||||||
|
|
||||||
// Make Array
|
|
||||||
for (int i = 0; i < strLen; i++) {
|
|
||||||
tmpArray[i] = tmpPw.charAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation Array
|
|
||||||
for (int i = 0; i < strLen - 2; i++) {
|
|
||||||
if (isContinuous(tmpArray[i], tmpArray[i + 2]) && isContinuous(tmpArray[i], tmpArray[i + 1], tmpArray[i + 2])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Validation Complete
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static boolean isContinuous(int first, int third) {
|
|
||||||
// 첫 글자 A-Z / 0-9
|
|
||||||
return (first > 47 && third < 58) || (first > 64 && third < 91);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isContinuous(int first, int second, int third) {
|
|
||||||
// 배열의 연속된 수 검사
|
|
||||||
// 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2
|
|
||||||
return Math.abs(third - second) == 1 && Math.abs(third - first) == 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
import javax.validation.Constraint;
|
|
||||||
import javax.validation.Payload;
|
|
||||||
|
|
||||||
@Constraint(validatedBy = PasswordRuleForKbankValidator.class)
|
|
||||||
@Target({ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface PasswordRuleForKbank {
|
|
||||||
String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~20자, 아이디, 휴대전화, 3자리 이상 연속,반복 문자 불가)";
|
|
||||||
|
|
||||||
Class<?>[] groups() default {};
|
|
||||||
|
|
||||||
Class<? extends Payload>[] payload() default {};
|
|
||||||
|
|
||||||
String password();
|
|
||||||
|
|
||||||
String loginId();
|
|
||||||
|
|
||||||
String mobile();
|
|
||||||
|
|
||||||
}
|
|
||||||
-144
@@ -1,144 +0,0 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import javax.validation.ConstraintValidator;
|
|
||||||
import javax.validation.ConstraintValidatorContext;
|
|
||||||
import org.apache.commons.beanutils.PropertyUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Sungpil Hyun
|
|
||||||
*/
|
|
||||||
public class PasswordRuleForKbankValidator implements ConstraintValidator<PasswordRuleForKbank, Object> {
|
|
||||||
|
|
||||||
// 최소 8자, 최대 20자 상수 선언
|
|
||||||
private static final int MIN = 8;
|
|
||||||
private static final int MAX = 20;
|
|
||||||
|
|
||||||
private String password;
|
|
||||||
private String loginId;
|
|
||||||
private String mobileNumber;
|
|
||||||
|
|
||||||
// 3자리 연속 문자 정규식
|
|
||||||
private static final String SAMEPT = "(\\w)\\1\\1";
|
|
||||||
// 공백 문자 정규식
|
|
||||||
private static final String BLANKPT = "(\\s)";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(PasswordRuleForKbank constraintAnnotation) {
|
|
||||||
this.password = constraintAnnotation.password();
|
|
||||||
this.loginId = constraintAnnotation.loginId();
|
|
||||||
this.mobileNumber = constraintAnnotation.mobile();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
|
|
||||||
|
|
||||||
String passwordValue = null;
|
|
||||||
String loginIdValue = null;
|
|
||||||
String mobileNumberValue = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
passwordValue = (String) PropertyUtils.getProperty(value, this.password);
|
|
||||||
loginIdValue = (String) PropertyUtils.getProperty(value, this.loginId);
|
|
||||||
mobileNumberValue = (String) PropertyUtils.getProperty(value, this.mobileNumber);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid(passwordValue, loginIdValue, mobileNumberValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValid(String password, String loginId, String mobileNumber) {
|
|
||||||
// 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식
|
|
||||||
String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$";
|
|
||||||
|
|
||||||
// 정규식 검사객체
|
|
||||||
Matcher matcher;
|
|
||||||
|
|
||||||
// 공백 체크
|
|
||||||
if (password == null || "".equals(password)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASCII 문자 비교를 위한 UpperCase
|
|
||||||
String tmpPw = password.toUpperCase();
|
|
||||||
// 문자열 길이
|
|
||||||
int strLen = tmpPw.length();
|
|
||||||
|
|
||||||
// 글자 길이 체크
|
|
||||||
if (strLen > 20 || strLen < 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginId != null && !loginId.isEmpty()) {
|
|
||||||
String[] loginParts = loginId.split("@");
|
|
||||||
if (loginParts.length > 0) {
|
|
||||||
String username = loginParts[0].toUpperCase();
|
|
||||||
if (tmpPw.contains(username)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile number validation
|
|
||||||
if (mobileNumber != null && !mobileNumber.isEmpty()) {
|
|
||||||
String[] mobileParts = mobileNumber.split("-");
|
|
||||||
for (String part : mobileParts) {
|
|
||||||
if (!part.isEmpty() && tmpPw.contains(part)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 공백 체크
|
|
||||||
matcher = Pattern.compile(BLANKPT).matcher(tmpPw);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비밀번호 정규식 체크
|
|
||||||
matcher = Pattern.compile(REGEX).matcher(tmpPw);
|
|
||||||
if (!matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 동일한 문자 3개 이상 체크
|
|
||||||
matcher = Pattern.compile(SAMEPT).matcher(tmpPw);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 연속된 문자 / 숫자 3개 이상 체크
|
|
||||||
// ASCII Char를 담을 배열 선언
|
|
||||||
int[] tmpArray = new int[strLen];
|
|
||||||
|
|
||||||
// Make Array
|
|
||||||
for (int i = 0; i < strLen; i++) {
|
|
||||||
tmpArray[i] = tmpPw.charAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation Array
|
|
||||||
for (int i = 0; i < strLen - 2; i++) {
|
|
||||||
if (isContinuous(tmpArray[i], tmpArray[i + 2]) && isContinuous(tmpArray[i], tmpArray[i + 1], tmpArray[i + 2])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Validation Complete
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static boolean isContinuous(int first, int third) {
|
|
||||||
// 첫 글자 A-Z / 0-9
|
|
||||||
return (first > 47 && third < 58) || (first > 64 && third < 91);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isContinuous(int first, int second, int third) {
|
|
||||||
// 배열의 연속된 수 검사
|
|
||||||
// 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2
|
|
||||||
return Math.abs(third - second) == 1 && Math.abs(third - first) == 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
|
||||||
|
|
||||||
import javax.validation.Constraint;
|
|
||||||
import javax.validation.Payload;
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Constraint(validatedBy = PasswordRuleForDjbankValidator.class)
|
|
||||||
@Target({ElementType.TYPE})
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface PasswordRuleForKjbank {
|
|
||||||
String message() default "비밀 번호 규칙에 부합하지 않습니다.(영문/숫자/특수문자 포함 8~20자, 아이디, 휴대전화, 3자리 이상 연속,반복 문자 불가)";
|
|
||||||
|
|
||||||
Class<?>[] groups() default {};
|
|
||||||
|
|
||||||
Class<? extends Payload>[] payload() default {};
|
|
||||||
|
|
||||||
String password();
|
|
||||||
|
|
||||||
String loginId();
|
|
||||||
|
|
||||||
String mobile();
|
|
||||||
|
|
||||||
}
|
|
||||||
-144
@@ -1,144 +0,0 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
|
||||||
|
|
||||||
import org.apache.commons.beanutils.PropertyUtils;
|
|
||||||
|
|
||||||
import javax.validation.ConstraintValidator;
|
|
||||||
import javax.validation.ConstraintValidatorContext;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Sungpil Hyun
|
|
||||||
*/
|
|
||||||
public class PasswordRuleForKjbankValidator implements ConstraintValidator<PasswordRuleForKjbank, Object> {
|
|
||||||
|
|
||||||
// 최소 8자, 최대 20자 상수 선언
|
|
||||||
private static final int MIN = 8;
|
|
||||||
private static final int MAX = 20;
|
|
||||||
|
|
||||||
private String password;
|
|
||||||
private String loginId;
|
|
||||||
private String mobileNumber;
|
|
||||||
|
|
||||||
// 3자리 연속 문자 정규식
|
|
||||||
private static final String SAMEPT = "(\\w)\\1\\1";
|
|
||||||
// 공백 문자 정규식
|
|
||||||
private static final String BLANKPT = "(\\s)";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(PasswordRuleForKjbank constraintAnnotation) {
|
|
||||||
this.password = constraintAnnotation.password();
|
|
||||||
this.loginId = constraintAnnotation.loginId();
|
|
||||||
this.mobileNumber = constraintAnnotation.mobile();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
|
|
||||||
|
|
||||||
String passwordValue = null;
|
|
||||||
String loginIdValue = null;
|
|
||||||
String mobileNumberValue = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
passwordValue = (String) PropertyUtils.getProperty(value, this.password);
|
|
||||||
loginIdValue = (String) PropertyUtils.getProperty(value, this.loginId);
|
|
||||||
mobileNumberValue = (String) PropertyUtils.getProperty(value, this.mobileNumber);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid(passwordValue, loginIdValue, mobileNumberValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValid(String password, String loginId, String mobileNumber) {
|
|
||||||
// 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식
|
|
||||||
String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$";
|
|
||||||
|
|
||||||
// 정규식 검사객체
|
|
||||||
Matcher matcher;
|
|
||||||
|
|
||||||
// 공백 체크
|
|
||||||
if (password == null || "".equals(password)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASCII 문자 비교를 위한 UpperCase
|
|
||||||
String tmpPw = password.toUpperCase();
|
|
||||||
// 문자열 길이
|
|
||||||
int strLen = tmpPw.length();
|
|
||||||
|
|
||||||
// 글자 길이 체크
|
|
||||||
if (strLen > 20 || strLen < 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginId != null && !loginId.isEmpty()) {
|
|
||||||
String[] loginParts = loginId.split("@");
|
|
||||||
if (loginParts.length > 0) {
|
|
||||||
String username = loginParts[0].toUpperCase();
|
|
||||||
if (tmpPw.contains(username)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile number validation
|
|
||||||
if (mobileNumber != null && !mobileNumber.isEmpty()) {
|
|
||||||
String[] mobileParts = mobileNumber.split("-");
|
|
||||||
for (String part : mobileParts) {
|
|
||||||
if (!part.isEmpty() && tmpPw.contains(part)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 공백 체크
|
|
||||||
matcher = Pattern.compile(BLANKPT).matcher(tmpPw);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비밀번호 정규식 체크
|
|
||||||
matcher = Pattern.compile(REGEX).matcher(tmpPw);
|
|
||||||
if (!matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 동일한 문자 3개 이상 체크
|
|
||||||
matcher = Pattern.compile(SAMEPT).matcher(tmpPw);
|
|
||||||
if (matcher.find()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 연속된 문자 / 숫자 3개 이상 체크
|
|
||||||
// ASCII Char를 담을 배열 선언
|
|
||||||
int[] tmpArray = new int[strLen];
|
|
||||||
|
|
||||||
// Make Array
|
|
||||||
for (int i = 0; i < strLen; i++) {
|
|
||||||
tmpArray[i] = tmpPw.charAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation Array
|
|
||||||
for (int i = 0; i < strLen - 2; i++) {
|
|
||||||
if (isContinuous(tmpArray[i], tmpArray[i + 2]) && isContinuous(tmpArray[i], tmpArray[i + 1], tmpArray[i + 2])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Validation Complete
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static boolean isContinuous(int first, int third) {
|
|
||||||
// 첫 글자 A-Z / 0-9
|
|
||||||
return (first > 47 && third < 58) || (first > 64 && third < 91);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean isContinuous(int first, int second, int third) {
|
|
||||||
// 배열의 연속된 수 검사
|
|
||||||
// 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2
|
|
||||||
return Math.abs(third - second) == 1 && Math.abs(third - first) == 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.eactive.apim.portal.common.validator;
|
package com.eactive.apim.portal.common.validator;
|
||||||
|
|
||||||
|
import org.apache.commons.beanutils.PropertyUtils;
|
||||||
|
|
||||||
import javax.validation.ConstraintValidator;
|
import javax.validation.ConstraintValidator;
|
||||||
import javax.validation.ConstraintValidatorContext;
|
import javax.validation.ConstraintValidatorContext;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
@@ -8,11 +10,15 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* Created by Sungpil Hyun
|
* Created by Sungpil Hyun
|
||||||
*/
|
*/
|
||||||
public class PasswordRuleValidator implements ConstraintValidator<PasswordRule, String> {
|
public class PasswordRuleValidator implements ConstraintValidator<PasswordRule, Object> {
|
||||||
|
|
||||||
// 최소 8자, 최대 20자 상수 선언
|
// 최소 8자, 최대 50자 상수 선언
|
||||||
private static final int MIN = 8;
|
private static final int MIN = 8;
|
||||||
private static final int MAX = 20;
|
private static final int MAX = 50;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
private String loginId;
|
||||||
|
private String mobileNumber;
|
||||||
|
|
||||||
// 3자리 연속 문자 정규식
|
// 3자리 연속 문자 정규식
|
||||||
private static final String SAMEPT = "(\\w)\\1\\1";
|
private static final String SAMEPT = "(\\w)\\1\\1";
|
||||||
@@ -21,11 +27,30 @@ public class PasswordRuleValidator implements ConstraintValidator<PasswordRule,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(PasswordRule constraintAnnotation) {
|
public void initialize(PasswordRule constraintAnnotation) {
|
||||||
|
this.password = constraintAnnotation.password();
|
||||||
|
this.loginId = constraintAnnotation.loginId();
|
||||||
|
this.mobileNumber = constraintAnnotation.mobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid(String password, ConstraintValidatorContext constraintValidatorContext) {
|
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
|
||||||
|
|
||||||
|
String passwordValue = null;
|
||||||
|
String loginIdValue = null;
|
||||||
|
String mobileNumberValue = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
passwordValue = (String) PropertyUtils.getProperty(value, this.password);
|
||||||
|
loginIdValue = (String) PropertyUtils.getProperty(value, this.loginId);
|
||||||
|
mobileNumberValue = (String) PropertyUtils.getProperty(value, this.mobileNumber);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid(passwordValue, loginIdValue, mobileNumberValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid(String password, String loginId, String mobileNumber) {
|
||||||
// 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식
|
// 영어, 숫자, 특수문자 포함한 MIN to MAX 글자 정규식
|
||||||
String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$";
|
String REGEX = "^((?=.*\\d)(?=.*[a-zA-Z])(?=.*[\\W]).{" + MIN + "," + MAX + "})$";
|
||||||
|
|
||||||
@@ -43,10 +68,30 @@ public class PasswordRuleValidator implements ConstraintValidator<PasswordRule,
|
|||||||
int strLen = tmpPw.length();
|
int strLen = tmpPw.length();
|
||||||
|
|
||||||
// 글자 길이 체크
|
// 글자 길이 체크
|
||||||
if (strLen > 20 || strLen < 8) {
|
if (strLen > MAX || strLen < MIN) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loginId != null && !loginId.isEmpty()) {
|
||||||
|
String[] loginParts = loginId.split("@");
|
||||||
|
if (loginParts.length > 0) {
|
||||||
|
String username = loginParts[0].toUpperCase();
|
||||||
|
if (tmpPw.contains(username)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile number validation
|
||||||
|
if (mobileNumber != null && !mobileNumber.isEmpty()) {
|
||||||
|
String[] mobileParts = mobileNumber.split("-");
|
||||||
|
for (String part : mobileParts) {
|
||||||
|
if (!part.isEmpty() && tmpPw.contains(part)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 공백 체크
|
// 공백 체크
|
||||||
matcher = Pattern.compile(BLANKPT).matcher(tmpPw);
|
matcher = Pattern.compile(BLANKPT).matcher(tmpPw);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
@@ -94,5 +139,4 @@ public class PasswordRuleValidator implements ConstraintValidator<PasswordRule,
|
|||||||
// 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2
|
// 3번째 글자 - 2번째 글자 = 1, 3번째 글자 - 1번째 글자 = 2
|
||||||
return Math.abs(third - second) == 1 && Math.abs(third - first) == 2;
|
return Math.abs(third - second) == 1 && Math.abs(third - first) == 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package com.eactive.apim.portal.config;
|
||||||
|
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 변경 강제(ENFORCE) 가드.
|
||||||
|
*
|
||||||
|
* <p>로그인 시 {@code PortalAuthenticationSuccessHandler} 가 대상자(비밀번호 미변경/만료)에게
|
||||||
|
* 세션 플래그 {@link #ENFORCE_SESSION_ATTR} 를 설정한다. 이 플래그가 있는 동안에는 비밀번호
|
||||||
|
* 변경/검증/로그아웃 경로를 제외한 모든 요청을 변경 페이지로 리다이렉트하여 접근을 차단한다.
|
||||||
|
* 비밀번호 변경 완료 시 플래그가 제거되어 정상 접근이 회복된다.</p>
|
||||||
|
*
|
||||||
|
* <p>정적 자원 경로는 {@code PortalConfigWebDispatcherServlet.addInterceptors} 의
|
||||||
|
* excludePathPatterns 로 제외한다.</p>
|
||||||
|
*/
|
||||||
|
public class PasswordChangeEnforcementInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
/** ENFORCE 대상 세션 플래그. 로그인 핸들러가 설정, 변경 완료 시 제거. */
|
||||||
|
public static final String ENFORCE_SESSION_ATTR = "pwEnforce";
|
||||||
|
|
||||||
|
/** 강제 상태에서도 접근 허용하는 경로(화이트리스트) */
|
||||||
|
private static final Set<String> ALLOWED_PATHS = new HashSet<>(Arrays.asList(
|
||||||
|
"/password/verify", // 현재 비밀번호 입력(진입) + 검증(POST)
|
||||||
|
"/password/change", // 새 비밀번호 폼(GET) + 실제 변경(POST)
|
||||||
|
"/password/confirm", // 비밀번호 확인 AJAX
|
||||||
|
"/actionLogout.do", // 로그아웃
|
||||||
|
"/login",
|
||||||
|
"/error", "/403", "/404"
|
||||||
|
));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session == null || !Boolean.TRUE.equals(session.getAttribute(ENFORCE_SESSION_ATTR))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AJAX/API 등 비(非)페이지 요청은 강제 리다이렉트 대상에서 제외한다.
|
||||||
|
// (세션 heartbeat 같은 인프라 호출을 302로 튕기면 keepalive JS가 세션만료로 오판하여
|
||||||
|
// 로그인↔홈↔변경페이지 무한 리다이렉트가 발생한다.)
|
||||||
|
if (!isTopLevelHtmlNavigation(request)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = request.getServletPath();
|
||||||
|
if (path != null && ALLOWED_PATHS.contains(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String target = request.getContextPath() + "/password/verify";
|
||||||
|
// 이미 목적지면 재리다이렉트하지 않는다(무한 루프 방지).
|
||||||
|
if (request.getRequestURI().equals(target)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.sendRedirect(target);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 브라우저 주소창 이동(최상위 HTML 문서 요청)인지 판별한다.
|
||||||
|
* GET + Accept: text/html + 비-AJAX 만 강제 리다이렉트 대상으로 본다.
|
||||||
|
*/
|
||||||
|
private boolean isTopLevelHtmlNavigation(HttpServletRequest request) {
|
||||||
|
if (!"GET".equalsIgnoreCase(request.getMethod())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String fetchMode = request.getHeader("Sec-Fetch-Mode");
|
||||||
|
if (fetchMode != null && !"navigate".equalsIgnoreCase(fetchMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String accept = request.getHeader("Accept");
|
||||||
|
return accept != null && accept.contains("text/html");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.eactive.apim.portal.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 변경 강제 정책 레벨.
|
||||||
|
*
|
||||||
|
* <p>PTL_PROPERTY (group={@code Portal}, name={@code passwordChangeEnforcement}) 값으로 제어한다.</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #NONE} — 정책 미적용. 안내/강제 없음.</li>
|
||||||
|
* <li>{@link #PERMISSIVE} — 대상자 로그인 시 1회 안내 팝업만. 강제 없음.</li>
|
||||||
|
* <li>{@link #ENFORCE} — 대상자는 비밀번호 변경 완료 전까지 변경/검증/로그아웃 외 접근 차단.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public enum PasswordEnforcementPolicy {
|
||||||
|
NONE,
|
||||||
|
PERMISSIVE,
|
||||||
|
ENFORCE;
|
||||||
|
|
||||||
|
/** PTL_PROPERTY 그룹명 */
|
||||||
|
public static final String PROPERTY_GROUP = "Portal";
|
||||||
|
/** PTL_PROPERTY 이름 */
|
||||||
|
public static final String PROPERTY_NAME = "passwordChangeEnforcement";
|
||||||
|
/** 기본값 (배포 직후 동작) */
|
||||||
|
public static final PasswordEnforcementPolicy DEFAULT = ENFORCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열을 정책으로 파싱한다. 대소문자 무시, 미해당/공백이면 {@link #DEFAULT} 반환.
|
||||||
|
*/
|
||||||
|
public static PasswordEnforcementPolicy from(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return PasswordEnforcementPolicy.valueOf(value.trim().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-6
@@ -13,6 +13,7 @@ import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
|||||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||||
import com.eactive.apim.portal.portaluser.entity.UserPasswordHistory;
|
import com.eactive.apim.portal.portaluser.entity.UserPasswordHistory;
|
||||||
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||||
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
|
import com.eactive.apim.portal.portaluser.repository.UserPasswordHistoryRepository;
|
||||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||||
@@ -52,6 +53,7 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
|
|||||||
private final UserInvitationRepository userInvitationRepository;
|
private final UserInvitationRepository userInvitationRepository;
|
||||||
private final PortalOrgRepository portalOrgRepository;
|
private final PortalOrgRepository portalOrgRepository;
|
||||||
private final UserSessionService userSessionService;
|
private final UserSessionService userSessionService;
|
||||||
|
private final PortalPropertyService portalPropertyService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||||
@@ -85,13 +87,18 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
|
|||||||
session.setAttribute("dormantLoginId", username);
|
session.setAttribute("dormantLoginId", username);
|
||||||
session.setAttribute("redirectUrl", contextPath + "/dormant_account");
|
session.setAttribute("redirectUrl", contextPath + "/dormant_account");
|
||||||
} else if (isTemporaryPasswordLogin(user)) {
|
} else if (isTemporaryPasswordLogin(user)) {
|
||||||
session.setAttribute("success", "임시 비밀번호로 로그인하셨습니다. <br>계정 보안을 위해 비밀번호를 변경해 주세요.");
|
applyPasswordChangeState(session,
|
||||||
session.setAttribute("passwordExpired", true);
|
"임시 비밀번호로 로그인하셨습니다. <br>계정 보안을 위해 비밀번호를 변경해 주세요.",
|
||||||
session.setAttribute("redirectUrl", contextPath + "/new_password");
|
contextPath + "/password/change");
|
||||||
} else if (isPasswordChangeRequired(user)) {
|
} else if (isPasswordChangeRequired(user)) {
|
||||||
session.setAttribute("success", "비밀번호를 변경한 지 90일이 경과하였습니다.<br>계정 보안을 위해 비밀번호를 변경해 주세요.");
|
applyPasswordChangeState(session,
|
||||||
session.setAttribute("passwordExpired", true);
|
"비밀번호를 변경한 지 " + portalProperties.getPasswordExpirationDays() + "일이 경과하였습니다.<br>계정 보안을 위해 비밀번호를 변경해 주세요.",
|
||||||
session.setAttribute("redirectUrl", contextPath + "/new_password");
|
contextPath + "/password/change");
|
||||||
|
} else if (user.getPasswordChangeDate() == null) {
|
||||||
|
// 비밀번호 변경일 미기록(예: 기존 가입자) → 재설정 대상. 현재 비밀번호 검증 진입 경로로 안내.
|
||||||
|
applyPasswordChangeState(session,
|
||||||
|
"계정 보안을 위해 비밀번호 재설정이 필요합니다.<br>비밀번호를 변경해 주세요.",
|
||||||
|
contextPath + "/password/verify");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 초대 코드 확인 - ROLE_USER만 확인 (세션에 저장하여 메인 페이지에서 팝업으로 표시)
|
// 초대 코드 확인 - ROLE_USER만 확인 (세션에 저장하여 메인 페이지에서 팝업으로 표시)
|
||||||
@@ -179,6 +186,35 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
|
|||||||
sessionLogger.info(logMessage.toString());
|
sessionLogger.info(logMessage.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 변경 대상자에게 정책(NONE/PERMISSIVE/ENFORCE)을 적용한다.
|
||||||
|
* <ul>
|
||||||
|
* <li>NONE — 아무것도 하지 않음.</li>
|
||||||
|
* <li>PERMISSIVE — 안내 알림만(로그인 후 dismissible 팝업). 강제 없음.</li>
|
||||||
|
* <li>ENFORCE — 안내 + 세션 강제 플래그 설정 → 인터셉터가 변경 완료까지 접근 차단.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private void applyPasswordChangeState(HttpSession session, String message, String redirectUrl) {
|
||||||
|
PasswordEnforcementPolicy policy = PasswordEnforcementPolicy.from(
|
||||||
|
portalPropertyService.getOrCreateProperty(
|
||||||
|
PasswordEnforcementPolicy.PROPERTY_GROUP,
|
||||||
|
PasswordEnforcementPolicy.PROPERTY_NAME,
|
||||||
|
PasswordEnforcementPolicy.DEFAULT.name(),
|
||||||
|
"비밀번호 변경 강제 정책 (NONE|PERMISSIVE|ENFORCE)"));
|
||||||
|
|
||||||
|
if (policy == PasswordEnforcementPolicy.NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setAttribute("success", message);
|
||||||
|
session.setAttribute("passwordExpired", true);
|
||||||
|
session.setAttribute("redirectUrl", redirectUrl);
|
||||||
|
|
||||||
|
if (policy == PasswordEnforcementPolicy.ENFORCE) {
|
||||||
|
session.setAttribute(PasswordChangeEnforcementInterceptor.ENFORCE_SESSION_ATTR, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isPasswordChangeRequired(PortalUser user) {
|
private boolean isPasswordChangeRequired(PortalUser user) {
|
||||||
// 가장 최근 비밀번호 변경 이력 조회
|
// 가장 최근 비밀번호 변경 이력 조회
|
||||||
Optional<UserPasswordHistory> latestHistory = passwordHistoryRepository
|
Optional<UserPasswordHistory> latestHistory = passwordHistoryRepository
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
|||||||
import org.springframework.web.multipart.support.MultipartFilter;
|
import org.springframework.web.multipart.support.MultipartFilter;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
|
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
@@ -76,6 +77,17 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
|
|||||||
registry.addConverter(enabledStatusConverter());
|
registry.addConverter(enabledStatusConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
// 비밀번호 변경 강제(ENFORCE) 가드. 정적 자원은 제외한다.
|
||||||
|
registry.addInterceptor(new PasswordChangeEnforcementInterceptor())
|
||||||
|
.addPathPatterns("/**")
|
||||||
|
.excludePathPatterns(
|
||||||
|
"/css/**", "/js/**", "/img/**", "/images/**", "/webfonts/**",
|
||||||
|
"/font/**", "/html/**", "/plugins/**", "/favicon.ico",
|
||||||
|
"/api/**");
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public EnabledStatusConverter enabledStatusConverter() {
|
public EnabledStatusConverter enabledStatusConverter() {
|
||||||
return new EnabledStatusConverter();
|
return new EnabledStatusConverter();
|
||||||
|
|||||||
@@ -369,12 +369,12 @@ page:
|
|||||||
credential_detail:
|
credential_detail:
|
||||||
name: "인증키 정보"
|
name: "인증키 정보"
|
||||||
path: "/myapikey/credential_detail"
|
path: "/myapikey/credential_detail"
|
||||||
change_password:
|
password_verify:
|
||||||
name: "비밀번호 변경"
|
name: "비밀번호 변경"
|
||||||
path: "/change_password"
|
path: "/password/verify"
|
||||||
verify_current_password:
|
password_change:
|
||||||
name: "비밀번호 변경"
|
name: "비밀번호 변경"
|
||||||
path: "/verify_current_password"
|
path: "/password/change"
|
||||||
myapikey_register_step1:
|
myapikey_register_step1:
|
||||||
name: "앱 생성 (기본 정보)"
|
name: "앱 생성 (기본 정보)"
|
||||||
path: "/myapikey/register/step1"
|
path: "/myapikey/register/step1"
|
||||||
|
|||||||
@@ -69,6 +69,24 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- ERROR 레벨만 별도 수집(스택 트레이스 포함). 장애 원인 추적용. -->
|
||||||
|
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/error.log</file>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||||
|
<level>ERROR</level>
|
||||||
|
<onMatch>ACCEPT</onMatch>
|
||||||
|
<onMismatch>DENY</onMismatch>
|
||||||
|
</filter>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_PATH}/backup/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||||
|
<maxFileSize>200MB</maxFileSize>
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
<level>${CONSOLE_EFFECTIVE_LEVEL}</level>
|
<level>${CONSOLE_EFFECTIVE_LEVEL}</level>
|
||||||
@@ -89,12 +107,14 @@
|
|||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="ROLLING"/>
|
<appender-ref ref="ROLLING"/>
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="ERROR_FILE"/>
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
<springProfile name="dev">
|
<springProfile name="dev">
|
||||||
<root level="DEBUG">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="ROLLING"/>
|
<appender-ref ref="ROLLING"/>
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="ERROR_FILE"/>
|
||||||
</root>
|
</root>
|
||||||
</springProfile>
|
</springProfile>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -39,7 +39,7 @@ deptUserManageRegister.id=Department User ID
|
|||||||
deptUserManageRegister.name=Department User Name
|
deptUserManageRegister.name=Department User Name
|
||||||
portalUser.confirm.password=Please enter your existing password
|
portalUser.confirm.password=Please enter your existing password
|
||||||
portalUser.Register.userName=Name
|
portalUser.Register.userName=Name
|
||||||
portalUser.Register.pass=Password (Combination of uppercase letters, lowercase letters, numbers, special characters, 8-20 characters)
|
portalUser.Register.pass=Password (Combination of uppercase letters, lowercase letters, numbers, special characters, 8-50 characters)
|
||||||
portalUser.Register.passConfirm=Confirm Password
|
portalUser.Register.passConfirm=Confirm Password
|
||||||
portalUser.Register.email=Email ID
|
portalUser.Register.email=Email ID
|
||||||
portalUser.Register.domain=Domain
|
portalUser.Register.domain=Domain
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ deptUserManageRegister.name=\uBD80\uC11C \uC0AC\uC6A9\uC790 \uC774\uB984
|
|||||||
entrprsUserManageList.regName=\uBC95\uC778 \uC0AC\uC6A9\uC790 \uB4F1\uB85D \uC774\uB984
|
entrprsUserManageList.regName=\uBC95\uC778 \uC0AC\uC6A9\uC790 \uB4F1\uB85D \uC774\uB984
|
||||||
portalUser.confirm.password=\uAE30\uC874 \uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694
|
portalUser.confirm.password=\uAE30\uC874 \uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694
|
||||||
portalUser.Register.userName=\uC774\uB984
|
portalUser.Register.userName=\uC774\uB984
|
||||||
portalUser.Register.pass=\uC601\uBB38 \uB300\uBB38\uC790,\uC601\uBB38 \uC18C\uBB38\uC790,\uC22B\uC790,\uD2B9\uC218\uBB38\uC790 \uC870\uD569 8-20\uC790
|
portalUser.Register.pass=\uC601\uBB38 \uB300\uBB38\uC790,\uC601\uBB38 \uC18C\uBB38\uC790,\uC22B\uC790,\uD2B9\uC218\uBB38\uC790 \uC870\uD569 8-50\uC790
|
||||||
portalUser.Register.passConfirm=\uBE44\uBC00\uBC88\uD638 \uD655\uC778
|
portalUser.Register.passConfirm=\uBE44\uBC00\uBC88\uD638 \uD655\uC778
|
||||||
portalUser.Register.email=\uC774\uBA54\uC77C \uC544\uC774\uB514
|
portalUser.Register.email=\uC774\uBA54\uC77C \uC544\uC774\uB514
|
||||||
portalUser.Register.domain=\uB3C4\uBA54\uC778
|
portalUser.Register.domain=\uB3C4\uBA54\uC778
|
||||||
|
|||||||
@@ -7571,6 +7571,69 @@ button.djb-comment-submit:disabled {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-policy-checklist {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 12px 0 0 0;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li .policy-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li .policy-icon::before {
|
||||||
|
content: "•";
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-idle {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-idle .policy-icon {
|
||||||
|
color: #b5b5b5;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-pass {
|
||||||
|
color: #1a8f4c;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-pass .policy-icon {
|
||||||
|
color: #1a8f4c;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-pass .policy-icon::before {
|
||||||
|
content: "✔";
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-fail {
|
||||||
|
color: #d63a3a;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-fail .policy-icon {
|
||||||
|
color: #d63a3a;
|
||||||
|
}
|
||||||
|
.password-policy-checklist li.is-fail .policy-icon::before {
|
||||||
|
content: "✖";
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-policy-note {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-carousel-section {
|
.hero-carousel-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -17995,6 +18058,12 @@ input[type=checkbox]:checked + .custom-checkbox {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.detail-wrap .dt-cancel-notice {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
.detail-wrap .dt-actions {
|
.detail-wrap .dt-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+28404
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* 비밀번호 문자열 정책 라이브 검증 (공용)
|
||||||
|
*
|
||||||
|
* 서버 검증기 PasswordRuleValidator.isValid(= @PasswordRule) 의
|
||||||
|
* "문자열" 규칙을 그대로 클라이언트로 포팅한다. 아이디/휴대전화 포함 여부는
|
||||||
|
* 민감정보 노출을 피하기 위해 서버 검증에만 맡긴다.
|
||||||
|
*
|
||||||
|
* 사용법(마크업 구동):
|
||||||
|
* <ul class="password-policy-checklist" data-password-input="newPassword">
|
||||||
|
* <li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">...</span></li>
|
||||||
|
* ... (rule: length | letter | digit | special | nospace | norepeat | noseq)
|
||||||
|
* </ul>
|
||||||
|
* <input type="password" id="newPassword" ...>
|
||||||
|
*
|
||||||
|
* DOM ready 시 자동으로 스캔하여 대상 input 에 바인딩한다.
|
||||||
|
*/
|
||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 3자리 연속(오름/내림) 문자·숫자 검사 — 서버 로직과 동일하게 대문자로 비교
|
||||||
|
function hasSequential(pw) {
|
||||||
|
var s = pw.toUpperCase();
|
||||||
|
for (var i = 0; i < s.length - 2; i++) {
|
||||||
|
var a = s.charCodeAt(i), b = s.charCodeAt(i + 1), c = s.charCodeAt(i + 2);
|
||||||
|
var sameCategory = (a > 47 && c < 58) || (a > 64 && c < 91); // 숫자 0-9 또는 영문 A-Z
|
||||||
|
var consecutive = Math.abs(c - b) === 1 && Math.abs(c - a) === 2;
|
||||||
|
if (sameCategory && consecutive) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 규칙별 판정 함수 (통과=true)
|
||||||
|
var RULES = {
|
||||||
|
length: function (pw) { return pw.length >= 8 && pw.length <= 50; },
|
||||||
|
letter: function (pw) { return /[a-zA-Z]/.test(pw); },
|
||||||
|
digit: function (pw) { return /[0-9]/.test(pw); },
|
||||||
|
special: function (pw) { return /[^A-Za-z0-9_]/.test(pw); }, // 서버 정규식 \W 기준 (밑줄 제외)
|
||||||
|
nospace: function (pw) { return !/\s/.test(pw); },
|
||||||
|
norepeat: function (pw) { return !/(\w)\1\1/.test(pw.toUpperCase()); },
|
||||||
|
noseq: function (pw) { return !hasSequential(pw); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전체 문자열 규칙 통과 여부
|
||||||
|
function isValid(pw) {
|
||||||
|
if (!pw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var key in RULES) {
|
||||||
|
if (RULES.hasOwnProperty(key) && !RULES[key](pw)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 체크리스트(ul) 하나를 대상 input 에 바인딩
|
||||||
|
function bind(input, list) {
|
||||||
|
var $input = (input && input.jquery) ? input : $(input);
|
||||||
|
var $list = (list && list.jquery) ? list : $(list);
|
||||||
|
var $items = $list.find('li[data-rule]');
|
||||||
|
if (!$input.length || !$items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
var pw = $input.val() || '';
|
||||||
|
$items.each(function () {
|
||||||
|
var $li = $(this);
|
||||||
|
var rule = RULES[$li.attr('data-rule')];
|
||||||
|
if (!rule) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$li.removeClass('is-idle is-pass is-fail');
|
||||||
|
if (pw.length === 0) {
|
||||||
|
$li.addClass('is-idle');
|
||||||
|
} else {
|
||||||
|
$li.addClass(rule(pw) ? 'is-pass' : 'is-fail');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.on('input.passwordPolicy', update);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마크업 구동 자동 초기화
|
||||||
|
function init(root) {
|
||||||
|
var $root = root ? $(root) : $(document);
|
||||||
|
$root.find('ul.password-policy-checklist[data-password-input]').each(function () {
|
||||||
|
var $list = $(this);
|
||||||
|
var input = document.getElementById($list.attr('data-password-input'));
|
||||||
|
if (input) {
|
||||||
|
bind(input, $list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
global.PasswordPolicy = {
|
||||||
|
RULES: RULES,
|
||||||
|
isValid: isValid,
|
||||||
|
bind: bind,
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
init(document);
|
||||||
|
});
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// 비밀번호 문자열 정책 라이브 체크리스트 (마이페이지 비밀번호 변경 + 회원가입 공용)
|
||||||
|
.password-policy-checklist {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 12px 0 0 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "\2022"; // •
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-idle {
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
.policy-icon {
|
||||||
|
color: #b5b5b5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-pass {
|
||||||
|
color: #1a8f4c;
|
||||||
|
|
||||||
|
.policy-icon {
|
||||||
|
color: #1a8f4c;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "\2714"; // ✔
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-fail {
|
||||||
|
color: #d63a3a;
|
||||||
|
|
||||||
|
.policy-icon {
|
||||||
|
color: #d63a3a;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "\2716"; // ✖
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-policy-note {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
@use 'components/breadcrumb' as *;
|
@use 'components/breadcrumb' as *;
|
||||||
@use 'components/test-env-notice' as *;
|
@use 'components/test-env-notice' as *;
|
||||||
@use 'components/djb-inquiry-comments' as *;
|
@use 'components/djb-inquiry-comments' as *;
|
||||||
|
@use 'components/password-policy' as *;
|
||||||
|
|
||||||
// 5. Page-specific styles
|
// 5. Page-specific styles
|
||||||
@use 'pages/index' as *;
|
@use 'pages/index' as *;
|
||||||
|
|||||||
@@ -1232,6 +1232,14 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 내부 결재 진행중: 신청 취소 불가 안내
|
||||||
|
.dt-cancel-notice {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
// Footer Buttons layout
|
// Footer Buttons layout
|
||||||
.dt-actions {
|
.dt-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -57,8 +57,8 @@
|
|||||||
<!-- Status Badge -->
|
<!-- Status Badge -->
|
||||||
<span class="app-card-badge"
|
<span class="app-card-badge"
|
||||||
th:classappend="${request.approval != null and request.approval.approvalStatus != null and request.approval.approvalStatus.toString() == 'REQUESTED' ? 'badge-requested' : 'badge-pending'}"
|
th:classappend="${request.approval != null and request.approval.approvalStatus != null and request.approval.approvalStatus.toString() == 'REQUESTED' ? 'badge-requested' : 'badge-pending'}"
|
||||||
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인대기'}">
|
th:text="${request.approval != null and request.approval.approvalStatus != null ? request.approval.approvalStatus.description : '승인정보 없음'}">
|
||||||
승인대기
|
승인정보 없음
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- App Description & Expected Completion Date -->
|
<!-- App Description & Expected Completion Date -->
|
||||||
|
|||||||
@@ -54,6 +54,9 @@
|
|||||||
대기중
|
대기중
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dt-app-status-row" th:if="${appRequest.approval == null}">
|
||||||
|
<span class="dt-status-badge status-pending">승인정보 없음</span>
|
||||||
|
</div>
|
||||||
<h3 class="dt-app-name" th:text="${appRequest.clientName}">앱 이름</h3>
|
<h3 class="dt-app-name" th:text="${appRequest.clientName}">앱 이름</h3>
|
||||||
<!-- App Description -->
|
<!-- App Description -->
|
||||||
<p class="dt-app-desc"
|
<p class="dt-app-desc"
|
||||||
@@ -200,12 +203,19 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 내부 결재 진행중: 취소 불가 안내 -->
|
||||||
|
<p class="dt-cancel-notice" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"
|
||||||
|
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
|
||||||
|
appRequest.approval.approvalStatus.toString() == 'PROCESSING'}">
|
||||||
|
내부 결재가 진행 중이라 신청을 취소할 수 없습니다. 취소가 필요한 경우 관리자에게 문의해 주세요.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Bottom Navigation Actions -->
|
<!-- Bottom Navigation Actions -->
|
||||||
<div class="dt-actions">
|
<div class="dt-actions">
|
||||||
<!-- Cancel Request Button (danger red) -->
|
<!-- Cancel Request Button (danger red) -->
|
||||||
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red"
|
<button type="button" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')" class="dt-btn-red"
|
||||||
th:if="${appRequest.approval != null and appRequest.approval.approvalStatus != null and
|
th:if="${appRequest.approval == null or (appRequest.approval.approvalStatus != null and
|
||||||
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED')}"
|
(appRequest.approval.approvalStatus.toString() == 'PENDING' or appRequest.approval.approvalStatus.toString() == 'REQUESTED'))}"
|
||||||
th:data-request-id="${appRequest.id}" onclick="cancelRequestById(this)">
|
th:data-request-id="${appRequest.id}" onclick="cancelRequestById(this)">
|
||||||
신청 취소
|
신청 취소
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -51,16 +51,6 @@
|
|||||||
<!-- Credential Info Section Card -->
|
<!-- Credential Info Section Card -->
|
||||||
<div class="dt-info-container">
|
<div class="dt-info-container">
|
||||||
|
|
||||||
<!-- 인증 방식 -->
|
|
||||||
<div class="dt-row">
|
|
||||||
<div class="dt-col">
|
|
||||||
<label class="dt-label">인증 방식</label>
|
|
||||||
<div class="dt-input-group">
|
|
||||||
<div class="dt-input-box" th:text="${authType}">OAuth2</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Client ID -->
|
<!-- Client ID -->
|
||||||
<div class="dt-row">
|
<div class="dt-row">
|
||||||
<div class="dt-col">
|
<div class="dt-col">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<a class="nav-link" th:href="@{/my_company_info}">기업 정보 변경</a>
|
<a class="nav-link" th:href="@{/my_company_info}">기업 정보 변경</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" th:href="@{/change_password}">비밀번호 변경</a>
|
<a class="nav-link active" th:href="@{/password/verify}">비밀번호 변경</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="password-change-wrapper">
|
<div class="password-change-wrapper">
|
||||||
<h2 class="page-outer-title">비밀번호 변경</h2>
|
<h2 class="page-outer-title">비밀번호 변경</h2>
|
||||||
|
|
||||||
<form id="passwordChangeForm" th:action="@{/mypage/change_new_password}" method="post">
|
<form id="passwordChangeForm" th:action="@{/password/change}" method="post">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||||
|
|
||||||
<div class="register-form-container">
|
<div class="register-form-container">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<span class="form-label-text">새 비밀번호</span>
|
<span class="form-label-text">새 비밀번호</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field-wrapper">
|
<div class="form-field-wrapper">
|
||||||
<input type="password" name="newPassword" class="form-input"
|
<input type="password" id="newPassword" name="newPassword" class="form-input"
|
||||||
placeholder="새 비밀번호 입력" required>
|
placeholder="새 비밀번호 입력" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,10 +52,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="password-rules-list">
|
<ul class="password-policy-checklist" data-password-input="newPassword">
|
||||||
<li>영문 대문자, 소문자, 숫자, 특수문자 8~20자</li>
|
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
|
||||||
<li>아이디, 휴대전화, 3자리 이상연속, 반복문자 사용불가</li>
|
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문 포함</span></li>
|
||||||
|
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자 포함</span></li>
|
||||||
|
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자 포함</span></li>
|
||||||
|
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용 불가</span></li>
|
||||||
|
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
|
||||||
|
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p class="password-policy-note">아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions" style="justify-content: flex-end;">
|
<div class="form-actions" style="justify-content: flex-end;">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="password-change-wrapper">
|
<div class="password-change-wrapper">
|
||||||
<h2 class="page-outer-title">비밀번호 변경</h2>
|
<h2 class="page-outer-title">비밀번호 변경</h2>
|
||||||
|
|
||||||
<form th:action="@{/verify_current_password}" method="post">
|
<form th:action="@{/password/verify}" method="post">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
|
||||||
|
|
||||||
<div class="register-form-container">
|
<div class="register-form-container">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
<div class="form-actions" style="justify-content: flex-end;">
|
<div class="form-actions" style="justify-content: flex-end;">
|
||||||
<div class="right-buttons">
|
<div class="right-buttons">
|
||||||
<button type="button" class="btn-cancel" th:onclick="|location.href='@{/}'|">취소</button>
|
<button type="button" class="btn-cancel" th:unless="${forcedPasswordReset}" th:onclick="|location.href='@{/}'|">취소</button>
|
||||||
<button type="submit" class="btn-apply btn-primary">확인</button>
|
<button type="submit" class="btn-apply btn-primary">확인</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +55,26 @@
|
|||||||
customPopups.showAlert([[${error}]]);
|
customPopups.showAlert([[${error}]]);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
<script th:if="${forcedPasswordReset}" th:inline="javascript">
|
||||||
|
// 비밀번호 재설정 강제(ENFORCE): "변경" 또는 "로그아웃"만 선택 가능
|
||||||
|
$(function () {
|
||||||
|
$('#customConfirmCloseButton').hide();
|
||||||
|
$('#customConfirmYesButton').text('변경');
|
||||||
|
$('#customConfirmNoButton').text('로그아웃');
|
||||||
|
customPopups.showConfirm(
|
||||||
|
'계정 보안을 위해 비밀번호 재설정이 필요합니다.<br>비밀번호를 변경하거나 로그아웃해 주세요.',
|
||||||
|
function (selection) {
|
||||||
|
if (selection) {
|
||||||
|
// 변경: 팝업 닫고 현재 페이지(비밀번호 변경)에서 진행
|
||||||
|
$('#customConfirmCloseButton').show();
|
||||||
|
} else {
|
||||||
|
// 로그아웃
|
||||||
|
window.location.href = [[@{/actionLogout.do}]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -78,6 +78,16 @@
|
|||||||
th:placeholder="#{portalUser.Register.pass}">
|
th:placeholder="#{portalUser.Register.pass}">
|
||||||
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
|
<input type="hidden" name="isPasswordValid" id="isPasswordValid"/>
|
||||||
<div id="password-validation" class="org-validation-message"></div>
|
<div id="password-validation" class="org-validation-message"></div>
|
||||||
|
<ul class="password-policy-checklist" data-password-input="password">
|
||||||
|
<li data-rule="length" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문/숫자/특수문자 포함 8~50자</span></li>
|
||||||
|
<li data-rule="letter" class="is-idle"><span class="policy-icon"></span><span class="policy-text">영문 포함</span></li>
|
||||||
|
<li data-rule="digit" class="is-idle"><span class="policy-icon"></span><span class="policy-text">숫자 포함</span></li>
|
||||||
|
<li data-rule="special" class="is-idle"><span class="policy-icon"></span><span class="policy-text">특수문자 포함</span></li>
|
||||||
|
<li data-rule="nospace" class="is-idle"><span class="policy-icon"></span><span class="policy-text">공백 사용 불가</span></li>
|
||||||
|
<li data-rule="norepeat" class="is-idle"><span class="policy-icon"></span><span class="policy-text">동일 문자 3자리 이상 반복 불가</span></li>
|
||||||
|
<li data-rule="noseq" class="is-idle"><span class="policy-icon"></span><span class="policy-text">연속된 문자/숫자 3자리 이상 불가</span></li>
|
||||||
|
</ul>
|
||||||
|
<p class="password-policy-note">아이디, 휴대전화 번호는 비밀번호에 사용할 수 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
<script th:src="@{/plugins/jquery/jquery-3.7.1.min.js}"></script>
|
<script th:src="@{/plugins/jquery/jquery-3.7.1.min.js}"></script>
|
||||||
<script th:src="@{/js/lodash.js}"></script>
|
<script th:src="@{/js/lodash.js}"></script>
|
||||||
<script th:src="@{/js/common.js}"></script>
|
<script th:src="@{/js/common.js}"></script>
|
||||||
|
<script th:src="@{/js/password-policy.js}"></script>
|
||||||
<script th:src="@{/js/moment.min.js}"></script>
|
<script th:src="@{/js/moment.min.js}"></script>
|
||||||
<script th:src="@{/js/daterangepicker.js}"></script>
|
<script th:src="@{/js/daterangepicker.js}"></script>
|
||||||
<script th:src="@{/plugins/codemirror/codemirror.js}"></script>
|
<script th:src="@{/plugins/codemirror/codemirror.js}"></script>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
|
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
|
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
|
||||||
<li><a th:href="@{/change_password}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
|
<li><a th:href="@{/password/verify}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><a th:href="@{/webhook}">Webhook 관리</a></li>
|
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><a th:href="@{/webhook}">Webhook 관리</a></li>
|
||||||
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
|
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
|
||||||
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
|
||||||
<li><a th:href="@{/change_password}">비밀번호 변경</a></li>
|
<li><a th:href="@{/password/verify}">비밀번호 변경</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
th:classappend="${activeMenu == 'profile'} ? 'service-nav__item--active' : ''"
|
th:classappend="${activeMenu == 'profile'} ? 'service-nav__item--active' : ''"
|
||||||
class="service-nav__item">내정보 관리</a>
|
class="service-nav__item">내정보 관리</a>
|
||||||
|
|
||||||
<a th:href="@{/change_password}"
|
<a th:href="@{/password/verify}"
|
||||||
th:classappend="${activeMenu == 'password'} ? 'service-nav__item--active' : ''"
|
th:classappend="${activeMenu == 'password'} ? 'service-nav__item--active' : ''"
|
||||||
class="service-nav__item">비밀번호 변경</a>
|
class="service-nav__item">비밀번호 변경</a>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|||||||
Reference in New Issue
Block a user