코드를 최대한 순수
Java
만 사용하려 했지만,
부분적으로org.springframework.util.StringUtils
의 도움을 받았습니다.
이 부분도 외부 라이브러리 도움없이 Java 로 메소드를 만드시면 어떠한
Dependency
없이 코드 구현이 가능합니다.
참고로 정규식은 ChatGpt 에게 많이 의존해서 작성한지라...
정규식 질문하시면 대답 못할 수도 있습니다 😂
package me.dailycode.authentication.util;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* 아래와 같은 기능을 제공하는 비밀번호 유효성 검사기 클래스
* <ul>
* <li>8~50자리의 영문문자, 숫자, 특수문자</li>
* <li>연속적인 숫자(ex : 1234567)</li>
* <li>동일한 "단일 문자" 반복(ex : aaaa)</li>
* <li>동일한 "문자열" 반복(ex : qweqweqwe)</li>
* <li>키보드 상에서 나란히 있는 문자열 (ex: qwer)</li>
* </ul>
*/
public class PasswordStrictValidator {
/**
* 키보드의 각 행에 해당하는 문자열을 리스트로 정의합니다.
* 이 리스트는 키보드의 행을 나타내며, 각 행은 키보드에서 나란히 있는 문자들의 집합을 포함합니다.
*/
private static final List<String> KEYBOARD_ROWS = Arrays.asList(
"qwertyuiop",
"asdfghjkl",
"zxcvbnm"
);
/**
* 숫자 문자열.
*/
private static final String NUMBERS = "0123456789";
/**
* 나란히 나오는 것의 최대 제한수
*/
private static final int ADJACENT_MAX_LIMIT = 4;
/**
* 한개의 문자가 중복해서 나오는 것의 최대 제한수
*/
private static final int SINGLE_CHAR_DUPLICATE_MAX_LIMIT = 4;
/**
* 비밀번호 최소 길이
*/
private static final int PASSWORD_MIN_LENGTH = 8;
/**
* 비밀번호 최대 길이
*/
private static final int PASSWORD_MAX_LENGTH = 50;
public static void check(String rawPassword, PasswordAdditionalCheck...additionalChecks) throws ApiException {
// null, 공백 체크
if (!StringUtils.hasText(rawPassword)) {
throw new RuntimeException("비밀번호는 공백일 수 없습니다.");
}
// 중간중간 있을 수 있는 공백 체크
String temp = StringUtils.trimAllWhitespace(rawPassword);
if (rawPassword.length() != temp.length()) {
throw new RuntimeException("비밀번호에 공백을 사용할 수 없습니다.");
}
// 길이 검사
if ((rawPassword.length() < PASSWORD_MIN_LENGTH)
|| (rawPassword.length() > PASSWORD_MAX_LENGTH)) {
throw new RuntimeException(
String.format("비밀번호 길이는 %d자 이상, %d자 이하만 가능합니다.",
PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH
));
}
// 영문자, 숫자, 특수문자가 최소 하나씩은 존재하는지 판단
/*
정규식 설명:
(?=.*[a-zA-Z]): 비밀번호에 적어도 하나의 영문자가 포함되어야 함.
(?=.*[~!@#$%^&*+=()_-]): 비밀번호에 적어도 하나의 특수문자가 포함되어야 함.
(?=.*[0-9]): 비밀번호에 적어도 하나의 숫자가 포함되어야 함.
.+: 비밀번호의 길이가 최소 1자 이상이어야 함.
*/
if (!rawPassword.matches("^(?=.*[a-zA-Z])(?=.*[~!@#$%^&*+=()_-])(?=.*[0-9]).+$")) {
throw new RuntimeException("비밀번호는 영문자, 숫자, 특수문자를 조합하여 작성해야 합니다.");
}
// 동일한 문자 반복 - 4번 발견되면 유효성 검증 실패
if (isSingleCharRepeated(rawPassword, SINGLE_CHAR_DUPLICATE_MAX_LIMIT)) {
throw new RuntimeException(
String.format("동일한 문자가 연속으로 나오는 것은 최대 %d개만 가능합니다.", (SINGLE_CHAR_DUPLICATE_MAX_LIMIT - 1)));
}
// 길이가 3 이상인 문자열이 나란히 반복될 때 - 3번 발견시 검증 실패
if (Pattern.compile("(.{3,}?)(?=\\1{2,})").matcher(rawPassword).find()) {
throw new RuntimeException("길이가 3인 동일한 문자열이 연속으로 나오는 것은 최대 2번만 가능합니다.");
}
// 연속적인 숫자 - 4개 이상 발견 시 유효성 검증 실패!
if (!isAdjacentNumbersValid(rawPassword, ADJACENT_MAX_LIMIT)) {
throw new RuntimeException(
String.format("연속된 숫자(ex:1234)는 최대 %d개만 포함 가능합니다.", (ADJACENT_MAX_LIMIT - 1)));
}
// 키보드 상에서 나란히 있는 문자열 4개 이상 발견 시 유효성 검증 실패!
if (!isAdjacentKeyboardRowValid(rawPassword.toLowerCase(Locale.US), ADJACENT_MAX_LIMIT)) {
throw new RuntimeException(
String.format("키보드 상에서 나란히 있는 문자열(ex: qwer)은 최대 %d개만 가능합니다.", (ADJACENT_MAX_LIMIT - 1)));
}
// 추가적으로 필요한 검증 수행
if (additionalChecks != null) {
for (PasswordAdditionalCheck additionalCheck : additionalChecks) {
additionalCheck.check(rawPassword);
}
}
}
@FunctionalInterface
public static interface PasswordAdditionalCheck {
void check(String rawPassword) throws ApiException;
}
/**
* 키보드의 행에 해당하는 문자열에 대한 유효성 검사를 수행합니다.
* 비밀번호 내에서 연속된 문자열이 키보드의 행에 속하는지 확인합니다.
*
* @param password 검사할 비밀번호
* @param minConsecutiveLength 연속된 문자열의 최소 길이
* @return 비밀번호가 유효한 경우 true, 그렇지 않은 경우 false
*/
private static boolean isAdjacentKeyboardRowValid(String password, int minConsecutiveLength) {
for (int i = 0; i < password.length() - minConsecutiveLength + 1; i++) {
String currentSubstring = password.substring(i, i + minConsecutiveLength);
for (String row : KEYBOARD_ROWS) {
if (row.contains(currentSubstring)) {
return false;
}
}
}
return true;
}
/**
* 숫자 문자열에 대한 유효성 검사를 수행합니다.
* 비밀번호 내에서 연속된 문자열이 숫자 문자열에 속하는지 확인합니다.
*
* @param password 검사할 비밀번호
* @param minConsecutiveLength 연속된 문자열의 최소 길이
* @return 비밀번호가 유효한 경우 true, 그렇지 않은 경우 false
*/
private static boolean isAdjacentNumbersValid(String password, int minConsecutiveLength) {
for (int i = 0; i < password.length() - minConsecutiveLength + 1; i++) {
String currentSubstring = password.substring(i, i + minConsecutiveLength);
if (NUMBERS.contains(currentSubstring)) {
return false;
}
}
return true;
}
/**
* 단일 문자 반복 검사
* 단일 문자가 minDuplicateCnt 이상 나오면 유효성 검증 실패
* @param password 검사할 비밀번호
* @param maxDuplicateCnt 중복 문자 출현횟수 제한선
* @return maxDuplicateCnt 만큼의 문자 반복이 발견되면 true, 아니면 false
*/
private static boolean isSingleCharRepeated(String password, int maxDuplicateCnt) {
for (int i = 0; i < password.length() - maxDuplicateCnt + 1; i++) {
String currentSubstring = password.substring(i, i + maxDuplicateCnt);
// 첫글자를 빼내서, 여러번 돌려본다.
char c = currentSubstring.charAt(0);
char[] chars = new char[maxDuplicateCnt];
Arrays.fill(chars, c);
String checkString = new String(chars);
if (checkString.equals(currentSubstring)) {
return true;
}
}
return false;
}
}