PasswordEncoder 인터페이스 이해
일반적으로 시스템은 암호를 그대로 저장하지 않고 해시를 사용해 저장한다. PasswordEncoder
는 암호를 인코딩하고 인증 프로세스에서 암호가 유효한지 확인한다.
upgradeEncoding
메서드는 false
를 반환하도록 기본 구현이 되어 있는데 이를 true
를 반환하도록 재정의하면 인코딩된 암호를 보안 향상을 위해 다시 인코딩한다. 상황에 따라 재정의해서 사용할 수 있다.
encode
와 matches
메서드는 기능적으로 밀접한 관계가 있어 이 둘을 재정의하려면 기능 면에서 항상 일치해야 한다. PasswordEncoder
로 인코딩된 암호가 주어진 암호와 같은지 PasswordEncoder
로 확인할 수 있어야 한다.
제공된 구현 선택
스프링 시큐리티에서 이미 몇 가지 유용한 구현을 제공한다.
- Pbkdf2PasswordEncoder - PBKDF2를 사용한다.
- BcryptPasswordEncoder - bcrypt 강력 해싱 함수로 인코딩한다.
- ScryptPasswordEncoder - scrypt 해싱 함수로 인코딩한다.
PasswordEncoder p = new Pbkdf2PasswordEncoder();
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret");
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 185000, 256);
PBKDF2
는 반복 횟수만큼 HMAC을 돌리는 아주 단순하고 느린 해싱 함수이다. 마지막 호출의 세 매개 변수는 각각 인코딩 프로세스에 이용되는 키 값, 암호 인코딩의 반복 횟수, 해시의 크기이다. 해시의 크기가 늘어날 수록 암호가 강력해지지만 리소스 역시 함께 늘어나므로 절충해야 한다.
또 다른 훌륭한 옵션은 bcrypt
강력 해싱 함수로 암호를 인코딩하는 BcryptPasswordEncoder
가 있다. 매개변수가 없는 생성자로 BcryptPasswordEncoder
를 생성해도 되지만 인코딩 프로세스에 이용되는 로그 라운드를 나타내는 강도 계수를 지정할 수도 있다. 또한, 인코딩에 이용되는 SecureRandom 인스턴스를 변경할 수도 있다.
PasswordEncoder p = new BcryptPasswordEncoder();
PasswordEncoder p = new BcryptPasswordEncoder(4);
SecureRandom s = SecureRandom.getInstanceString();
PasswordEncoder p = new BcryptPasswordEncoder(4, s);
지정하는 로그 라운드 값은 해싱 작업에 이용하는 반복 횟수에 영향을 준다. 반복 횟수는 2로그 라운드로 계산된다. 반복 횟수를 계산하기 위한 로그 라운드 값은 4 ~ 31 사이여야 한다.
DelegatingPasswordEncoder를 이용한 여러 인코딩 전략
인증 흐름에 암호 일치를 위해 다양한 구현을 적용해야 할 때가 있다. DelegatingPasswordEncoder
는 자체 구현은 없고 PasswordEncoder
인터페이스를 구현하는 다른 객체에 위임한다. 운영 단계에서 특정 애플리케이션 버전부터 인코딩 알고리즘이 변경된 경우에 DelegatingPasswordEncoder
객체를 사용할 수 있다.
DelegatingPasswordEncoder
를 사용해 해싱하면 해시에 해싱 알고리즘이 {사용된 알고리즘}
형태로 접두사가 추가된다. matches
메서드를 호출하면 이 접두사를 기준으로 적절한 해싱 알고리즘을 선택해 주어진 비밀번호와 일치하는지 확인한다.
이를 직접 구현하면 아래와 같다.
@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());
return new DelegatingPasswordEncoder("bcrypt", encoders());
}
접두사가 없으면 기본 인코더를 이용하고 DelegatingPasswordEncoder
를 생성할 때 첫 번째 매개변수로 지정한다. 위 DelegatingPasswordEncoder
는 기본적으로 BcryptPasswordEncoder
구현에 위임한다.
또는 이미 구현된 PasswordEncoderFactories.creatDelegatingPasswordEncoder
를 호출할 수도 있다.
설명 | 설명 |
---|---|
UserDetails | 스프링 시큐리티가 관리하는 사용자를 나타낸다. |
GrantedAuthority | 애플리케이션의 목적 내에서 사용자에게 허용되는 작업을 정의한다(예: 읽기, 쓰기, 삭제 등). |
UserDetailsService | 사용자의 이름으로 사용자 세부 정보를 검색하는 객체를 나타낸다. |
UserDetailsManager | UserDetailsService를 확장한 인터페이스이다. 사용자 컬렉션이나 특정 사용자를 변경할 수 있다. |
PasswordEncoder | 암호를 암호화 또는 해시하는 방법과 주어진 인코딩된 문자열을 일반 텍스트 암호와 비교하는 방법을 지정한다. |
스프링 시큐리티 암호화 모듈에 관한 추가 정보
이전까지 SSCM(Spring Security Crypto Module)을 살펴봤다. 암호화 및 복호화 함수와 키 생성 기능은 자바 언어에서 기본 제공하지 않기 때문에 이러한 기능에 보다 쉽게 접근하기 위한 종속성을 추가할 때 제약이 있다. PasswordEncoder도 SSCM의 일부이다.
SSCM의 두 가지 필수 기능
- KeyGenerator - 해싱 및 암호화 알고리즘을 위한 키를 생성하는 객체
- Encryptor - 데이터를 암호화 및 복호화하는 객체
Key Generator 이용
KeyGenerator는 특정한 종류의 키를 생성하는 객체로서 일반적으로 암호화나 해싱 알고리즘에 필요하다. 스프링 시큐리티의 KeyGenerator 구현은 아주 훌륭한 유틸리티 툴이다.
BytesKeyGenerator
와 StringKeyGenerator
는 KeyGenerator
의 두 가지 주요 유형을 나타내는 인터페이스이며 팩토리 클래스 KeyGenerators
로 직접 만들 수 있다.
public interface StringKeyGenerator {
String generateKey();
}
StringKeyGenerator
를 통해 문자열 키를 얻을 수 있고 이 키는 해싱 또는 암호화 알고리즘의 솔트 값으로 이용된다.
StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey();
위 코드에서 KeyGenerators.string()
메서드는 8바이트 키를 생성하고 keyGenerator.generateKey()
메서드는 생성된 키를 16진수 문자열로 인코딩하여 문자열로 반환한다.
BytesKeyGenerator
public interface BytesKeyGenerator {
int getKeyLength();
byte[] generateKey();
}
해당 인터페이스에는 키 길이를 바이트 수로 반환하는 getKeyLength()
메서드가 있다.
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte[] key = keyGenerator.generateKey();
int keyLength = keyGenerator.getKeyLength();
KeyGenerators.secureRandom()
은 내부적으로 SecureRandomBytesKeyGenerator
인스턴스를 생성하는데 8바이트 길이의 키를 생성한다. 여기에 16바이트 키를 생성하고 싶다면 인자로 16을 넣어주면 된다.
이렇게 생성한 BytesKeyGenerator
는 generateKey()
메서드가 호출될 때마다 고유한 키를 생성한다. 같은 KeyGenerator를 호출했을 때 같은 키값을 반환하는 구현이 필요하다면 KeyGenerators.shared(int length)
메서드로 BytesKeyGenerator
를 생성할 수 있다.
// given
BytesKeyGenerator shared = KeyGenerators.shared(8);
//when
byte[] generatedKey1 = shared.generateKey();
byte[] generatedKey2 = shared.generateKey();
// then
assertEquals(generatedKey1, generatedKey2); // true
Encryptor
는 암호화 알고리즘을 구현하는 객체다. 암호화와 복호화는 보안을 위한 공통적인 기능이므로 애플리케이션에 이러한 기능이 필요할 가능성이 크다.
시스템의 구성 요소 간에 데이터를 전송하거나 데이터를 저장할 때 암호화가 필요할 때가 많다. Encryptor
는 암호화와 복호화 작업을 지원하며 SSCM에는 이를 위해 BytesEncryptor
와 TextEncryptor
라는 두 유형의 암호기가 정의돼 있다.
두 Encryptor
는 다른 형식으로 데이터를 처리하며 BytesEncryptor
가 더 범용적이다.
BytesEncryptor
Encryptor
를 사용하는 옵션에 관해 알아보자. 팩토리 클래스 Encryptors
는 여러 가능성을 제공하며, BytesEncryptor
의 경우 다음과 같이 Encryptors.standard()
또는 Encryptors.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암호화를 이용해 입력을 암호화한다. 더 강력한 바이트 암호화 Ecryptor
인스턴스는 Encryptors.stronger()
메서드를 호출하면 된다. 차이는 AES 암호화의 작업 모드로 GCM(갈루아/카운터 모드)을 이용한다. 표준 모드는 이보다 약한 방식인 CBC(암호 블록 체인)을 이용한다.
TextEncryptor
세 가지 주요 형식이 있고 Encryptors.text()
, Encryptors.delux()
, ~~Encryptors.queryableText()~~
메서드를 호출해 이러한 형식을 생성할 수 있다.
값을 암호화하지 않는 더미 TextEncryptor
를 반환하는 메서드도 있다. Encryptors.noOpText()
메서드를 통해 애플리케이션 성능만을 테스트하거나 데모 예제에서 사용할 수 있다.
String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.noOpText();
// valueToEncrypt == encrypted
String encrypted = e.encrypt(valueToEncrypt);
위에서 볼 수 있듯이 Encryptors.delux()
는 Encryptors.stronger()
를, Encryptors.text()
는 Encryptors.standard()
를 사용한다.
delux
와 text
모두 같은 입력으로 encrypt
메서드를 반복 호출해도 다른 출력이 반환된다. 암호화 프로세스에 임의의 초기화 벡터가 생성되기 때문이다.
'Java > Spring' 카테고리의 다른 글
스프링 시큐리티 인 액션] 5장_인증 (0) | 2024.06.28 |
---|---|
스프링 시큐리티 인 액션] 3장_사용자 관리 (0) | 2024.06.19 |
MVC 패턴이란? (0) | 2023.11.05 |