Programming/Java

java 임시 비밀번호 생성(SecureRandom 사용하는 이유)

Jan92 2023. 7. 30. 00:51

java 특수문자를 포함한 비밀번호 생성 방법(SecureRandom 사용 이유)

 

SecureRandom을 사용한 임시 비밀번호 생성

해당 포스팅에서는 'java 임시 비밀번호 생성 방법'에 대한 내용을 다루고 있으며, 임시 비밀번호 생성 과정에서 Random 클래스가 아닌 'SecureRandom 클래스를 사용하는 이유'도 함께 살펴봅니다.

 

 


임시 비밀번호 생성 기본 예시

private static final char[] rndAllCharacters = new char[]{
        //number
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        //uppercase
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        //lowercase
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        //special symbols
        '@', '$', '!', '%', '*', '?', '&'
};

public String getRandomPassword(int length) {
    SecureRandom random = new SecureRandom();
    StringBuilder stringBuilder = new StringBuilder();

    int rndAllCharactersLength = rndAllCharacters.length;
    for (int i = 0; i < length; i++) {
        stringBuilder.append(rndAllCharacters[random.nextInt(rndAllCharactersLength)]);
    }

    return stringBuilder.toString();
}

SecureRandom을 사용하여 임시 비밀번호를 생성하는 기본적인 코드는 다음과 같은데요.

 

하지만, 만약 비밀번호에 '대문자, 소문자, 숫자, 특수문자가 각 1개 이상 포함되어야 한다' 같은 조건이 존재한다면, 위 코드는 조건을 충족하지 못하는 결과를 반환할 수도 있다는 문제점이 있습니다. 

 

 


조건에 일치하는 임시 비밀번호 생성 예시

1. Patten.matches() + Recursion Function

//rndAllCharacters는 위와 동일합니다.
private static final char[] rndAllCharacters = new char[]{ ... };


public String getRandomPassword1(int length) {
    SecureRandom random = new SecureRandom();
    StringBuilder stringBuilder = new StringBuilder();

    int rndAllCharactersLength = rndAllCharacters.length;
    for (int i = 0; i < length; i++) {
        stringBuilder.append(rndAllCharacters[random.nextInt(rndAllCharactersLength)]);
    }

    String randomPassword = stringBuilder.toString();

    // 최소 8자리에 대문자, 소문자, 숫자, 특수문자 각 1개 이상 포함
    String pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}";
    if (!Pattern.matches(pattern, randomPassword)) {
        return getRandomPassword1(length);    //비밀번호 조건(패턴)에 맞지 않는 경우 메서드 재실행
    }
    return randomPassword;
}

첫 번째로 사용한 방법은 Pattern.matches() 메서드를 통해 비밀번호 조건 일치 여부를 검증 후, 일치하지 않는 경우 해당 메서드를 재귀로 실행하는 방법입니다.

 

* 인자로 들어오는 length로 인해 발생할 수 있는 예외는 고려하지 않았습니다.

 

 

2. 조건에 맞춘 비밀번호 생성

//rndAllCharacters는 위와 동일합니다.
private static final char[] rndAllCharacters = new char[]{ ... };


private static final char[] numberCharacters = new char[] {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};

private static final char[] uppercaseCharacters = new char[] {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
};

private static final char[] lowercaseCharacters = new char[] {
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
};

private static final char[] specialSymbolCharacters = new char[] {
        '@', '$', '!', '%', '*', '?', '&'
};



public String getRandomPassword2(int length) {
    SecureRandom random = new SecureRandom();
    StringBuilder stringBuilder = new StringBuilder();
    
    List<Character> passwordCharacters = new ArrayList<>();

    int numberCharactersLength = numberCharacters.length;
    passwordCharacters.add(numberCharacters[random.nextInt(numberCharactersLength)]);

    int uppercaseCharactersLength = uppercaseCharacters.length;
    passwordCharacters.add(uppercaseCharacters[random.nextInt(uppercaseCharactersLength)]);

    int lowercaseCharactersLength = lowercaseCharacters.length;
    passwordCharacters.add(lowercaseCharacters[random.nextInt(lowercaseCharactersLength)]);

    int specialSymbolCharactersLength = specialSymbolCharacters.length;
    passwordCharacters.add(specialSymbolCharacters[random.nextInt(specialSymbolCharactersLength)]);

    int rndAllCharactersLength = rndAllCharacters.length;
    for (int i = 0; i < length-4; i++) {
        passwordCharacters.add(rndAllCharacters[random.nextInt(rndAllCharactersLength)]);
    }

    Collections.shuffle(passwordCharacters);

    for (Character character : passwordCharacters) {
        stringBuilder.append(character);
    }

    return stringBuilder.toString();
}

두 번째로 사용한 방법은 애초에 조건에 맞는 비밀번호를 생성하는 것인데요.

최소 조건에 맞는 비밀번호를 먼저 생성 후, rndAllCharacters에서 나머지 길이만큼의 비밀번호를 생성하는 방식입니다.

(여기서도 '대문자, 소문자, 숫자, 특수문자가 각 1개 이상 포함되어야 한다'를 조건으로 가정하였습니다.)

 

생성된 각각의 character를 stringBuilder에 바로 넣었을 때, 위치에 대한 패턴이 생기는 것을 방지하기 위해 Collections.shuffle() 메서드를 추가하였습니다.

 

 


Random이 아닌 SecureRandom을 사용하는 이유

먼저 java에서 난수 생성을 하기 위해서는 아래 3가지 방법이 있는데요.

 

1. Math.random()

2. Random Class

3. SecureRandom Class

 

If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers

(Random Class 개발자 노트 내용 중 일부)

 

먼저 Random 클래스의 경우 기본적으로 현재 시간을 시드(seed) 값으로 하고, 그것을 기반으로 난수를 생성하는데요.

위 내용과 같이 서로 다른 인스턴스에 대해 동일한 seed 값이 사용되었을 때, 동일한 숫자 시퀀스를 생성하여 반환한다는 문제가 있습니다.

 

Random은 내부적으로 LCG(Linear Congruential Generator)를 사용하는 반면, SecureRandom의 경우 더 강력한 난수 생성기인 PRNG(Pseudo Random Number Generator)를 사용합니다.

 

또한 Random 클래스에는 48bit의 seed 값을 사용하는 반면, SecureRandom 클래스에는 최대 128bit의 seed 값이 사용되기 때문에 반복될 가능성이 적다는 특징이 있는데요.

 

 

NativePRNG class implSetSeed() method

가장 중요한 것은 Random에서는 기본적으로 시간을 seed 값으로 사용하는 반면, SecureRandom는 기본 운영 체제에서 가져온 임의의 데이터(seedFile)와 실제로 감지할 수 없는 I/O 이벤트의 타이밍을 seed 값으로 사용하기 때문에 예측할 수 없다는 것입니다.

 

따라서 Session ID, 암호화 키 등의 보안 결정을 위한 값을 생성할 때는 Random이 아닌 SecureRandom을 사용하는 것이 권장되고 있습니다.

 

 

Random Class 동일한 seed 값에 대한 예시

 

SecureRandom Class 동일한 seed 값에 대한 예시

동일한 seed 값을 가진 Random 인스턴스에서는 같은 난수를 생성한다는 것을 확인할 수 있으며, SecureRandom에서는 동일한 seed 값을 가진 인스턴스에서도 다른 난수를 생성하는 것을 확인할 수 있습니다.

 

 

***

Random과 SecureRandom이 생성하는 난수는 무작위로 생성되는 것이 아닌 어떤 알고리즘을 통해 생성되는 규칙이 있는 '의사난수'입니다.

 

 


Math.Random()을 사용하지 않는 이유

randomNumberGenerator

추가로 Math.random() 메서드의 경우 내부적으로 Random 객체를 생성하여 사용하기 때문에 Random과 동일한 문제가 발생하는데요.

 

또한 코드상으로도 여러 개의 난수가 필요한 경우 Math.random()을 사용하게 되면 random() 메서드가 호출될 때마다 Random 인스턴스를 생성하기 때문에 Math.random() 메서드를 사용하는 것보다 위 예시들처럼 Random 인스턴스를 직접 생성하여 재사용하는 것이 효율적입니다.

 

 

 

< 참고 자료 >

https://www.techiedelight.com/ko/difference-java-util-random-java-security-securerandom/
https://kdhyo98.tistory.com/48