Java/Spring

스프링 시큐리티 인 액션] 4장_암호처리

greatwhite 2024. 6. 21. 14:43

PasswordEncoder 인터페이스 이해

일반적으로 시스템은 암호를 그대로 저장하지 않고 해시를 사용해 저장한다. PasswordEncoder는 암호를 인코딩하고 인증 프로세스에서 암호가 유효한지 확인한다.

upgradeEncoding 메서드는 false를 반환하도록 기본 구현이 되어 있는데 이를 true 를 반환하도록 재정의하면 인코딩된 암호를 보안 향상을 위해 다시 인코딩한다. 상황에 따라 재정의해서 사용할 수 있다.

encodematches 메서드는 기능적으로 밀접한 관계가 있어 이 둘을 재정의하려면 기능 면에서 항상 일치해야 한다. 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 구현은 아주 훌륭한 유틸리티 툴이다.

BytesKeyGeneratorStringKeyGeneratorKeyGenerator의 두 가지 주요 유형을 나타내는 인터페이스이며 팩토리 클래스 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을 넣어주면 된다.

이렇게 생성한 BytesKeyGeneratorgenerateKey() 메서드가 호출될 때마다 고유한 키를 생성한다. 같은 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에는 이를 위해 BytesEncryptorTextEncryptor라는 두 유형의 암호기가 정의돼 있다.

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()를 사용한다.

deluxtext 모두 같은 입력으로 encrypt 메서드를 반복 호출해도 다른 출력이 반환된다. 암호화 프로세스에 임의의 초기화 벡터가 생성되기 때문이다.