Spring Security에서는 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는 PasswordEncoder 인터페이스와 구현체들을 제공한다.
public interface PasswordEncoder {
// 비밀번호를 암호화(단방향)
String encode(CharSequence rawPassword);
// 암호화된 비밀번호와 암호화되지 않은 비밀번호와 일치하는지 비교
boolean matches(CharSequence rawPassword, String encodedPassword);
// 인코딩된 암호화를 다시 한번 인코딩 할 때 사용 (true 일 경우 다시 인코딩, 기본값 -> false
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
NoOpPasswordEncoder
: 암호를 인코딩하지 않고 일반 텍스트로 유지(절대 사용하면 안된다. 테스트 용도로만 사용)StandardPasswordEncoder
: SHA-256을 이용해 암호를 해시한다. (강도가 약한 해싱 알고리즘이기 때문에 지금은 많이 사용되지 않는다.) Pbkdf2PasswordEncoder
: PBKDF2를 이용한다.BCryptPasswordEncoder
: bcrypt 강력 해싱 함수로 암호를 인코딩한다.SCryptPasswordEncoder
: scrypt 해싱 함수로 암호를 인코딩한다.DelegatingPasswordEncoder
는 여러 인코딩 알고리즘을 사용할 수 있게 해주는 기능이다. 예를 들어 특정 애플리케이션 버전부터 인코딩 알고리즘이 변경된 경우가 있다. 현재 사용되는 알고리즘에서 취약성이 발견되어 다른 인코딩 알고리즘으로 변경하고자 할 때 사용하기 좋다.
DelegatingPasswordEncoder
는 PasswordEncoder
인터페이스의 한 구현이며 자체 인코딩 알고리즘을 구현하는 대신 같은 계약의 다른 구현 인스턴스에 작업을 위임한다.DelegatingPasswordEncoder
는 암호의 prefix를 기준으로 올바른 PasswordEncoder
구현에 작업을 위임한다.DelegatingPasswordEncoder
는 각 인스턴스를 맵에 저장한다.NoOpPassowrd Encoder
는 {noop}
가 할당되고 BcryptPasswordEncoder
는 {bcrypt}
가 할당된다.@Configuration
public class ProjectConfig {
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
// 세개의 인코더 알고리즘을 설정한다.
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder(16384, 8, 1, 32, 64));
// BCryptPasswordEncoder를 사용
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
}
DelegatingPasswordEncoder
는 접두사를 기준으로 암호를 비교한다. 접두사가 없으면 DelegatingPasswordEncoder
는 기본 인코더를 이용하며 기본 PasswordEncoder
는 DelegatingPasswordEncoder
인스턴스를 만들 때 첫 번째 매개변수로 지정한다.
스프링 시큐리티는 편의를 위해 모든 표준 제공 PasswordEncoder
의 구현에 대한 맵을 가진 DelegatingPasswordEncoder
를 생성하는 방법을 제공한다. PasswordEncoderFactories
클래스에는 bcrypt
가 기본 인코더인 DelegatingPasswordEncoder
의 구현을 반환하는 정적 메서드 createDelegatingPasswordEncoder()
가 있다 구현 방법은 아래와 같다.
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
키 생성기는 특정한 종류의 키를 생성하는 객체로서 일반적으로 암호화나 해싱 알고리즘에 필요하다.
public interface StringKeyGenerator {
// 키 값을 나타내는 문자열 하나를 반환하는 generateKey() 메서드 하나만 있다.
String generateKey();
}
// 솔트 값을 가져오는 방법
// 8바이트 키를 생성하고 이를 16진수 문자열로 인코딩하며 메서드는 이러한 작업의 결과를 문자열로 반환한다.
// KeyGenerators 다양한 키 generator를 만들 수 있는 여러가지 편리한 팩토리 메서드를 제공한다.
StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey();
public interface BytesKeyGenerator {
// 키 길이(바이트 수)를 반환하는 메서드
// 기본 ByteKeyGenerator는 8바이트 길이의 키를 생성
int getKeyLength();
// byte[] 키를 반환하는 generateKey() 메서드
byte[] generateKey();
}
// 다른 키 길이를 길이를 지정하려면 키 생성기 인스턴스를 얻을 때 KeyGenerators.secureRandom() 메서드에 원하는 값을 전달하면 된다.
// KeyGenerators.secureRandom() 메서드는 생성한 BytesKeyGenerator는 generateKey() 메서드가 호출될 때마다 고유한 키를 생성한다.
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16);
같은 키 생성기를 호출하면 같은 키 값을 반환하는 구현이 적합 할 때가 있다. 이때는 KeyGenerators.shared(int length)
메서드로 BytesKeyGenerator
를 생성할 수 있다.
BytesKeyGenerator keyGenerator = KeyGenerators.shared(16);
byte[] key1 = keyGenerator.generateKey();
byte[] key2 = keyGenerator.generateKey();
BytesEncryptor
및 TextEncryptor
라는 두 유형이 암호기가 정의돼 있다.Encryptor
클래스는 대칭 encryptor
를 생성하는 팩토리 메소드를 제공한다.// 데이터를 문자열로 관리
public interface TextEncryptor {
// 데이터를 받는 메서드
String encrypt(String text);
// 데이터를 출력하는 메서드
String decrypt(String encryptedText);
}
TextEncryptor
는 세가지 주요 기능이 있다.
Encryptor.text()
: Encryptors.standard()
메서드로 암호화 작업을 관리Encryptor.delux()
: Encryptors.stronger()
메서드로 암호화 작업을 관리Encryptor.queryableText()
: 순차 암호화 작업에서 입력이 같으면 같은 출력을 생성하게 해주는 메서드String salt = KeyGenerators.string().generateKey();
String password = "secret";
tring valueToEncrypt = "HELLO";
// 솔트와 암호를 이용하는 TextEncryptor 객체 생성
TextEncryptor e = Encryptors.text(password, salt);
String encrypted = e.encrypt(valueToEncrypt);
String decrypted = e.decrypt(encrypted);
-----------------------------------------------------------------------------
TextEncryptor e = Encryptors.queryableText(password, salt);
String encrypted = e.encrypt(valueToEncrypt);
String decrypted = e.decrypt(encrypted);
// 데이터를 다이트로 관리
public interface BytesEncryptor {
// 데이터를 받는 메서드
byte[] encrypt(byte[] byteArray);
// 데이터를 출력하는 메서드
byte[] decrypt(byte[] encryptedByteArray);
}
BytesEncryptor
는 Encryptors.standard()
또는 Encryptor.stronger()
메서드를 사용할 수 있다.
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
BytesEncryptor e = Encryptors.standard(password, salt);
byte[] encrypted = e.encrypt(valueToEncrypt.getBytes());
byte[] decrypted = e.decrypt(encrypted);
내부적으로 표준 바이트 암호기는 256바이트 AES 암호화를 이용해 입력을 암호화한다. 더 강력한 암호기 인스턴스를 만들려면 Encryptors.stronger()
메서드를 호출하면 된다.
BytesEncryptor e = Encryptors.stronger(password, salt);