본문 바로가기
Spring Security

[Spring Security] Password Encoder 알아보기

by 개발자 JACOB 2023. 6. 12.

1. Password Encoder의 역할?


비밀번호를 저장할 때, 일반 플레인 텍스트를 데이터베이스에 저장하면 안 됩니다. DBA나 해커가 고객 데이터베이스에 접근한다면, 비밀번호를 볼 수 있기 때문입니다. 따라서 비밀번호는 암호화 과정을 거쳐 데이터베이스에 저장하는 것이 바람직합니다. 여기 몇 가지 암호화 과정이 있습니다.

 

인코딩 방법

인코딩 방법은 데이터를 한 형식에서 다른 형식으로 변환하는 프로세스이며 암호화와는 관련이 없습니다. 이 방법은 이미 너무 알려져 있으며, 복호화도 쉽습니다. 따라서 인코딩은 보안이 필요한 데이터에 사용되지 않습니다. 인코딩 방식은 ASCII, BASE64, UNICODE 방식이 있습니다.

 

Encryption 방법

Encryption 방법은 테이터를 변환하는 프로세스로 기밀을 보장하는 방식입니다. 기밀을 보장하기 위해 암호화에는 '키'의 사용이 필용합니다. Encryption 방법은 '키'의 도움으로 복호화를 사용해 되돌릴 수 있습니다. '키'의 보안이 뚫리지 않는다면, Encryption 암호화는 안전합니다.

 

Hashing 방법

Hashing 에서 데이터는 일부 Hashing 함수를 사용해서 Hash 값으로 변환합니다. 그리고 해싱된 데이터는 되돌릴 수 없습니다. 또 생성된 해시 값으로는 원본 데이터를 확인할 수 없습니다. 해싱 알고리즘의 출력과 함께 임의의 데이터가 주어지면 원본 데이터를 볼 필요 없이 이 데이터가 원본 입력 데이터와 일치하는지 확인할 수 있습니다.

 

2. PasswordEncoder 살펴보기


클라이언트가 아이디 비밀번호를 입력한다면, 해싱 알고리즘을 통해 데이터베이스에서 비밀번호를 가져와 해시 값이 일치하는지 비교합니다.

 

public interface PasswordEncoder {

	String encode(CharSequence rawPassword);

	boolean matches(CharSequence rawPassword, String encodedPassword);

	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

 

PasswordEncoder 인터페이스입니다. 비밀번호를 암호화하고, 비교하고, 암호화를 강화하는 메소드들이 있습니다. upgradeEncoding 메소드의 기본 return은 false로, true로 설정하면 보안을 강화할 수 있습니다. 단, 2중으로 강화하기 때문에 사용할 때 신중해야 합니다.

 

스프링 시큐리티에서는 PasswordEncoder의 구현으로 NoOpPasswordEncoder, StandardPasswordEncoder, Pbkd2PasswordEncoder, BCryptPasswordEncoder, ScryptPasswordEncoder, Argaon2PasswordEncoder를 제공합니다.

 

여기서 BCryptPasswordEncoder, SCryptPasswordEncoder, Argaon2PasswordEncoder를 사용하는 것을 권장합니다. SCryptPasswordEncoder와 Aragon2PasswordEncoder는 각각 클라이언트에게 비밀번호 인코딩 시 요구사항들이 많아집니다. (RAM 사용, CPU 사용 등) 따라서 더욱더 강력하게 보안을 할 수 있지만, 서버의 리소스를 많이 잡아먹을 수 있기 때문에 BCryptPasswordEncoder를 사용할 것을 권장합니다.

 

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

 

스프링빈에 BCryptPasswordEncoder를 등록한 모습입니다. BCryptPasswordEncoder의 생성자를 보겠습니다.

 

/**
 * @param version the version of bcrypt, can be 2a,2b,2y
 * @param strength the log rounds to use, between 4 and 31
 * @param random the secure random instance to use
 */
public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
    if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
        throw new IllegalArgumentException("Bad strength");
    }
    this.version = version;
    this.strength = (strength == -1) ? 10 : strength;
    this.random = random;
}

 

생성자에는 각각 버전, (보안의) 강도, random 값을 설정할 수 있습니다. 여기서 기본 생성자로 생성 시에는 strength는 -1이 되는데, -1일 시 보안의 강도는 10으로 자동으로 설정됩니다. 주석을 보신다면, strength는 4에서 31까지 설정할 수 있다는 것을 알 수 있습니다.

 

public BCryptPasswordEncoder() {
    this(-1);
}

 

기본 생성자일 경우 int인 strength는 -1로 선택되는 것을 볼 수 있습니다.

 

위 사진은 데이터베이스에서 회원가입 시 저장된 회원의 정보입니다. 저는 스프링 빈으로 BcryptPasswordEncoder를 생성했고 기본 생성자를 선택했습니다. 따라서 이 설정을 바탕으로 해싱된 비밀번호 확인할 수 있습니다.

 

앞의 $2a는 버전을 뜻하고, 뒤의 $10은 보안의 강도를 뜻합니다. 저는 기본 생성자를 선택했기 때문에 strength가 -1로 생성되었고, -1일 경우 자동으로 보안 강도는 10으로 책정되기 때문에 $10이 찍히는 모습입니다.

 

public enum BCryptVersion {

    $2A("$2a"),

    $2Y("$2y"),

    $2B("$2b");

    private final String version;

    BCryptVersion(String version) {
        this.version = version;
    }

    public String getVersion() {
        return this.version;
    }

}

 

이것은 BCryptVersion인데, $2a, $2y, $2b가 있는 것을 알 수 있습니다.

 

public BCryptPasswordEncoder(int strength, SecureRandom random) {
    this(BCryptVersion.$2A, strength, random);
}

 

만약 생성자에 Version을 넣지 않으면 $2A 버전으로 초기화됩니다. 기본 생성자를 선택했기 때문에 위 사진에서 버전이 $2A로 찍히는 것을 볼 수 있었습니다.

 

아무튼 우리가 설정한 정보를 바탕으로 솔트(소금)를 구성합니다. 음식을 먹을 때 간을 맞추기 위해 소금을 뿌리는 것처럼, 해싱 알고리즘을 강화하기 위해 솔트를 설정합니다. 우리가 설정한 이 솔트로 해싱 알고리즘이 구축되는 것입니다.