이번엔 UserDetailsService의 단짝인 PasswordEncoder에 대해 알아보겠다.
PasswordEncoder 형태
PasswordEncoder 모습은 아래와 같다.
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
- encode(...): 제공된 문자열을 인코딩해 반환
- matches(...): 인코딩 된 문자열과 기본 비밀번호를 비교
- upgradeEncoding(...): true를 반환할 시 인코딩된 비밀번호가 다시 인코딩 됨
PasswordEncoder 구현
이전까지 PasswordEncoder를 아래와 같이 구현해 사용했었다.
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
여기서의 NoOpPasswordEncoder는 비밀번호를 encoding 없이 일반 text로 활용하겠다는 것으로 해당 Class의 코드를 확인해 보면 아래와 같다.
@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {
private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();
private NoOpPasswordEncoder() {
}
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
public static PasswordEncoder getInstance() {
return INSTANCE;
}
}
encode()와 matches()가 구현되어 있으며 해당 Class는 모든 Password에 대해 동작이 동일하므로 singleton 형태인 것을 확인할 수 있다.
PasswordEncoder는 위의 Class 이외에도 Pbkdf2PasswordEncoder, BCryptPasswordEncoder 등 여러 Class를 제공한다.
- NoOpPasswordEncoder: 비밀번호를 인코딩하지 않고 일반 Text로 사용
- StandardPasswordEncoder: SHA-256을 사용해 비밀번호를 해시.
- Pbkdf2PasswordEncoder: 무차별 대입 공격에 대한 취약성을 줄일 수 있는 방식.
- BCryptPasswordEncoder: bcrypt 강력한 해싱을 사용해 비밀번호 인코딩
- SCryptPasswordEncoder: scrypt 해싱 기능을 사용해 비밀번호 인코딩
차근차근 살펴보자.
StandardPasswordEncoder
SHA-256을 사용해 비밀번호를 해시한다. 해당 방법은 Deprecated 되었기에 사용하지 않아야 하지만 현재 서비스 중인 Application에서 사용하고 있는 경우가 있다.
Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder의 생성자는 아래와 같다.
여기서 첫 번째 방법은 Deprecated 되었으므로 두 번째 방법을 사용해야 한다.
형태는 아래와 같다.
PasswordEncoder p = new Pbkdf2PasswordEncoder(
"secret", 16, 310000,
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
키 값, 인코딩에 사용되는 반복 횟수, 해시 크기, 해시 방법 순이다.
secretKeyFactoryAlgoritym은 아래와 같은 옵션이 있다.
- PBKDF2WithHmacSHA1
- PBKDF2WithHmacSHA256
- PBKDF2WithHmacSHA512
숫자가 클수록 강력한 비밀번호가 되지만 성능 측면에선 소비하는 리소스가 많아지므로 상황에 맞게 선택해야 한다.
BCryptPasswordEncoder
Bcrypt 강력 해시 기능을 사용하는 방법으로 사용자는 Version(버전), Strength(강도), SecureRandom을 선택할 수 있다.
강도의 값이 클수록 암호를 해시하는 작업이 기하급수적으로 올라간다.
사용은 아래와 같이 한다.
PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4);
SecureRandom s = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, s);
SCryptPasswordEncoder
scrypt 해싱 기능을 사용하며 인자로 5개의 숫자를 받는다.
DelegatingPasswordEncoder
DelegatingPasswordEncoder는 다른 PasswordEncoder와 사용 방법이 다르다.
어떤 Application은 상황에 따라 다른 PasswordEncoder를 사용하고 싶을 수 있다.
혹은 기존에 사용하던 password encoding 방법에서 취약점이 발견되어 다른 방법으로 변경해야 하는 경우 가 있을 수 있다.
이러한 경우 DelegationPasswordEncoder를 사용하는 것이 좋다.
DelegatingPasswordEncoder는 인코딩 알고리즘을 구현하지 않고 다른 PasswordEncoder 구현체에 인코딩을 위임한다. 인코딩 된 해시는 사용된 알고리즘의 이름이 prefix로 시작한다.(중괄호 필수)
- {noop}
- {bcrypt}
- {pbkdf2}
- ...
DelegatingPasswordEncoder는 접두사를 확인해 알맞은 PasswordEncode 구현체에 작업을 위임한다.
만약 해시된 비밀번호가 "{noop}ABCDE"라면 DelegatingPasswordEncoder는 {noop} prefix를 확인해 NoOpPasswordEncoder에게 작업을 위임한다.
활용법은 아래와 같다.
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
public class DelegatingPasswordEncoderExample {
public static void main(String[] args) {
// 여러 인코더를 등록
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
// 기본 인코더를 bcrypt로 설정
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
// 패스워드 인코딩
String encodedPassword = passwordEncoder.encode("myPassword");
System.out.println("Encoded Password: " + encodedPassword);
// 패스워드 매칭
boolean matches = passwordEncoder.matches("myPassword", encodedPassword);
System.out.println("Password matches: " + matches);
}
}
만약 해시한 값에 접두사가 없으면 기본 인코더를 사용한다. 기본 인코더는 인스턴스를 생성할 때 지정된 첫 번째 인코더이다.
Spring Security는 DelegatingPasswordEncoder를 편리하게 생성할 수 있도록 PasswordEncoderFactories 클래스에서 createDelegatingPasswordEncoder() method를 제공한다.
해당 method는 모든 표준 PasswordEncoder 매핑이 되어있고 bcrypt를 기본 인코더로 하는 DelegatingPasswordEncoder를 반환한다.
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
'Java > Spring' 카테고리의 다른 글
Spring Security - SSCM (2) | 2024.06.15 |
---|---|
Spring Security - UserDetails, UserDetailService (1) | 2024.06.12 |
Spring Security - Spring Security 겉핥기 (0) | 2024.06.11 |
Spring Security - Spring Security를 공부하는 이유 (1) | 2024.06.04 |