[Java] 비밀번호 유효성 검사기

식빵·2024년 3월 21일
0

Java Lab

목록 보기
21/23
post-thumbnail

유효성 검사 Java 코드

코드를 최대한 순수 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;
    }
}
profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글