Spring/스프링 시큐리티 인 액션

[4장] 암호 처리

꾸준함. 2025. 5. 19. 03:40

주의

이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.

 

1. PasswordEncoder 계약의 이해

 

https://jaimemin.tistory.com/2700

 

  • 일반적으로 시스템은 암호를 일반 텍스트로 관리하지 않고 공격자가 암호를 읽고 훔치기 어렵게 하기 위한 일종의 변환 과정을 거침
    • 스프링 시큐리티에는 이 책임을 위해 정의된 별도의 계약이 있음

 

PasswordEncoder 계약의 정의

  • PasswordEncoder는 인증 프로세스에서 암호가 유효한지를 확인함
  • 모든 시스템은 어떤 방식으로든 인코딩 된 암호를 저장하며 아무도 암호를 읽을 수 없도록 해싱 후 저장하는 것을 권장
  • PasswordEncoder도 암호를 인코딩할 수 있으며 계약에 선언된 encode() 및 matches() 메서드는 사실상 계약의 책임을 정의함
    • 이 둘은 서로 강력하게 연결되어 있으며 같은 계약의 일부 

 

 

 

  • PasswordEncoder 인터페이스는 두 개의 추상 메서드와 기본 구현이 있는 메서드를 하나를 정의함
    • 추상 encode() 및 matches() 메서드는 또한 PasswordEncoder 구현을 다룰 때 가장 자주 접하는 메서드
    • encode(CharSequence rawPassword) 메서드는 주어진 문자열을 변환해 반환하며 스프링 시큐리티 기능의 관점에서 해당 메서드는 주어진 암호의 해시를 제공하거나 암호화를 수행하는 일을 함
    • 인코딩 된 문자열이 원시 암호와 일치하는지 나중에 확인하는 데는 matches(CharSequence rawPassword, String encodedPassword) 메서드를 이용할 수 있음, matches() 메서드는 지정된 암호를 인증 프로세스에서 알려진 자격 증명의 집합을 대상으로 비교함
    • upgradeEncoding(CharSequence encodedPassword)은 계약에서 기본값 false를 반환하는데 true를 반환하도록 메서드를 재정의하면 인코딩 된 암호를 보안 향상을 위해 다시 인코딩함

 

PasswordEncoder 계약의 구현

  • encode() 메서드에서 반환된 문자열은 항상 같은 PasswordEncoder의 match() 메서드로 검증할 수 있어야 함
  • PasswordEncoder를 구현할 수 있으면 애플리케이션이 인증 프로세스에서 암호를 관리하는 방법을 선택할 수 있음
    • 가장 직관적인 암호 인코더 구현은 암호를 인코딩하지 않고 일반 텍스트로 간주하는 것

 

 

 

  • 해싱 알고리즘 SHA-512를 이용하는 PasswordEncoder의 구현 예제는 아래와 같음
    • encode() 메서드에서 호출되고 주어진 입력의 해시 값을 반환
    • matches() 메서드는 입력된 원시 암호를 해시하고 주어진 해시와 비교해 검증

 

 

 

PasswordEncoder의 제공된 구현 선택

  • PasswordEncoder를 구현하는 방법을 알면 도움이 되지만 스프링 시큐리티에 이미 몇 가지 유용한 구현이 있다는 것도 알아야 함
    • 이러한 구현 중 애플리케이션에 적합한 구현이 있으면 직접 작성할 필요가 없음

 

  • 이 절에서는 스프링 시큐리티에 있는 PasswordEncoder 구현 옵션을 다룸
    • NoOpPasswordEncoder: 암호를 인코딩하지 않고 일반 텍스트로 유지, 암호를 해싱하지 않기 때문에 실제 시나리오에는 절대 쓰지 말아야 함
    • StandardPasswordEncoder: SHA-256를 이용해 암호를 해시, 해당 구현은 이제는 구식이기 때문에 새 구현에는 쓰지 않는 것을 권장
    • Pbkdf2PasswordEncoder: PBKDF2를 이용
    • BCryptPasswordEncoder: bcrypt 강력 해싱 함수로 암호를 인코딩
    • SCryptPasswordEncoder: scrypt 해싱 함수로 암호를 인코딩

 

NoOpPasswordEncoder

  • 암호를 인코딩하지 않기 때문에 이론적인 예제에만 이용
  • NoOpPasswordEncoder 클래스는 싱글톤으로 설계돼서 클래스 바깥에서는 생성자를 직접 호출할 수 없지만, 다음과 같이 NoOpPasswordEncoder.getInstance() 메서드로 클래스의 인스턴스를 얻을 수 있음

 

PasswordEncoder p = NoOpPasswordEncoder.getInstance();

 

StandardPasswordEncoder

  • StandardPasswordEncoder 구현은 SHA-256으로 암호를 해싱함
  • StandardPasswordEncoder를 이용하면 해싱 프로세스에 적용할 비밀을 지정할 수 있으며 비밀의 값은 생성자의 매개변수로 전달되고 인수가 없는 생성자를 호출하면 빈 문자열이 키 값으로 이용됨
  • 앞서 언급했다시피 이제 구식이므로 새 구현에는 쓰지 않는 것을 권장

 

PasswordEncoder p = new StandardPasswordEncoder();
PasswordEncoder p = new StandardPasswordEncoder("secret");

 

Pbkdf2PasswordEncoder

  • 스프링 시큐리티에는 PBKDF2로 암호를 인코딩
  • Pbkdf2PasswordEncoder의 인스턴스를 만들 때는 다음과 같은 옵션이 있음

 

PasswordEncoder p = new Pbkdf2PasswordEncoder();
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret");
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 185000, 256);

 

 

  • PBKDF2는 반복 횟수 인수만큼 HMAC를 수행하는 아주 단순하고 느린 해싱 함수
  • 마지막 호출의 세 매개 변수는 각각 인코딩 프로세스에 이용되는 키의 값, 암호 인코딩의 반복 횟수, 해시의 크기
    • 두 번째와 세 번째 매개변수는 결과의 강도에 영향을 주고 반복 횟수와 결과 길이를 늘이거나 줄일 수 있으며 해시가 길수록 암호는 더 강력해짐
    • 그러나 이러한 값은 성능에 영향을 주며 반복 횟수를 늘리면 애플리케이션이 소비하는 리소스가 증가함
    • 따라서 해시 생성에 사용되는 리소스와 필요한 인코딩 강도 사이에서 신중하게 절충해야 함

 

BCryptPasswordEncoder

  • bcrypt 강력 해싱 함수로 암호를 인코딩
  • BCryptPasswordEncoder의 인스턴스를 만들려면 인수가 없는 생성자를 호출해도 되지만, 인코딩 프로세스에 이용되는 로그 라운드를 나타내는 강도 계수를 지정할 수도 있음
  • 또한 인코딩에 이용되는 SecureRandom 인스턴스를 변경할 수도 있음

 

PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4);

SecureRandom s = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, s);

 

  • 지정하는 로그 라운드 값은 해싱 작업이 이용하는 반복 횟수에 영향을 줌
    • 반복 횟수는 2 로그 라운드로 계산됨
    • 반복 횟수를 계산하기 위한 로그 라운드 값은 4 ~ 31 사이여야 하며 해당 값을 지정하려면 이전 코드에 나온 것처럼 두 번째나 세 번째 오버로딩되니 생성자를 호출하면 됨

 

SCryptPasswordEncoder

  • 해당 암호 인코더는 scrypt 해싱 함수를 이용
  • ScryptPasswordEncoder의 인스턴스를 만드는 데는 두 가지 옵션이 있음

 

PasswordEncoder p = new SCryptPasswordEncoder();
PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);

https://livebook.manning.com/book/spring-security-in-action/chapter-4/

 

DelegatingPasswordEncoder를 이용한 여러 인코딩 전략

  • 일부 애플리케이션에는 다양한 암호 인코더를 갖추고 특정 구성에 따라 선택하는 방식이 유용할 수 있음
  • 운영 단계 애플리케이션에 DelegatingPasswordEncoder가 사용되는 일반적인 시나리오로는 특정 애플리케이션 버전부터 인코딩 알고리즘이 변경되는 경우가 있음
  • 현재 사용되는 알고리즘에서 취약점이 발견되어 신규 등록 사용자의 자격 증명을 변경하고 싶지만, 기존 자격 증명을 변경하고 싶을 때 DelegatingPasswordEncoder 객체가 좋은 해결 방법 중 하나
    • DelegatingPasswordEncoder는 PasswordEncoder 인터페이스의 한 구현이며 자체 인코딩 알고리즘을 구현하는 대신 같은 계약의 다른 구현 인스턴스에 작업을 위임함
    • 해시는 해당 해시를 의미하는 알고리즘의 이름을 나타내는 접두사로 시작하며 DelegatingPasswordEncoder는 암호의 접두사를 기준으로 올바른 PasswordEncoder 구현에 작업을 위임함
    • i.g. 암호에 접두사 {noop}가 있으면 DelegatingPasswordEncoder가 작업을 NoOpPasswordEncoder 구현에 위임하며 {bcrypt}에 대해 BCryptPasswordEncoder, {scrypt}에 대해 SCryptPasswordEncoder를 등록하고 해당 PasswordEncoder에 위임함

 

https://livebook.manning.com/book/spring-security-in-action/chapter-4/

 

  • DelegatingPasswordEncoder를 정의하기 위해서는 원하는 PasswordEncoder 구현의 인스턴스 컬렉션을 만들고, 이를 DelegatingPasswordEncoder에 넣으면 됨
    • 아래 예제 코드는 BCryptPasswordEncoder를 기본 구현으로 정의했기 때문에 접두사가 없을 때 애플리케이션이 작업을 BCryptPasswordEncoder에 위임함

 

 

 

  • 스프링 시큐리티는 편의를 위해 모든 표준 제공 PasswordEncoder의 구현에 대한 맵을 가진 DelegatingPasswordEncoder를 생성하는 방법을 제공
    • PasswordEncoderFactories 클래스에는 bcrypt가 기본 인코더인 DelegatingPasswordEncoder의 구현을 반환하는 정적 메서드 createDelegatingPasswordEncoder()가 있음

 

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

 

2. 스프링 시큐리티 암호화 모듈에 관한 추가 정보

  • SSCM의 두 가지 필수 기능을 이용하는 방법은 다음과 같음
    • 키 생성기: 해싱 및 암호화 알고리즘을 위한 키를 생성하는 객체
    • 암호기: 데이터를 암호화 및 복호화하는 객체

 

키 생성기 이용

  • 키 생성기는 특정한 종류의 키를 생성하는 객체로서 일반적으로 암호화나 해싱 알고리즘에 필요함
  • 스프링 시큐리티의 키 생성기 구현은 아주 훌륭한 유틸리티 툴
  • BytesKeyGenerator 및 StringKeyGenerator는 키 생성기의 두 가지 주요 유형을 나타내는 인터페이스이며 팩토리 클래스 KeyGenerators로 직접 만들 수 있음
    • StringKeyGenerator 계약으로 나타내는 문자열 키 생성기를 이용해 문자열 키를 얻을 수 있음
    • 일반적으로 해당 키는 해싱 또는 암호화 알고리즘의 솔트 값으로 이용

 

 

 

  • 생성기에는 키 값을 나타내는 문자열 하나를 반환하는 generate() 메서드 하나만 있음
    • 해당 생성기는 8바이트 키를 생성하고 이를 16진수 문자열로 인코딩하며 메서드는 이러한 작업의 결과를 문자열로 반환함

 

StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey();

 

  • 두 번째 키 생성기 인터페이스 BytesKeyGenerator는 다음과 같이 정의됨


 

  • 위 인터페이스에는 byte [] 키를 반환하는 generateKey() 메서드 외에도 바이트 수를 반환하는 메서드가 있음
    • 기본 ByteKeyGenerator는 8바이트 길이의 키를 생성
    • 다른 키 길이를 지정하려면 키 생성기 인스턴스를 얻을 때 KeyGenerators.secureRandom() 메서드에 원하는 값을 전달하면 됨
    • KeyGenerators.secureRandom() 메서드로 생성한 BytesKeyGenerator는 generateKey() 메서드가 호출될 때마다 고유한 키를 생성함

 

BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte[] key = keyGenerator.generateKey();
int keyLength = keyGenerator.getKeyLength();

BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(10);

 

  • 같은 키 생성기를 호출하면 같은 키 값을 반환하는 구현이 적합할 때가 있음
    • 이때는 KeyGenerators.shared(int length) 메서드로 BytesKeyGenerator를 생성할 수 있음
    • 다음 코드에 key1 및 key2는 값이 같음

 

BytesKeyGenerator keyGenerator = KeyGenerators.shared(10);
byte[] key1 = keyGenerator.generateKey();
byte[] key2 = keyGenerator.generateKey();

 

암호화와 복호화 작업에 암호기 이용

  • 암호기는 암호화 알고리즘을 구현하는 객체
    • 암호화와 복호화는 보안을 위한 공통적인 기능이므로 애플리케이션에 이러한 기능이 필요할 가능성이 큼

 

  • 시스템의 구성 요소 간에 데이터를 전송하거나 데이터를 저장할 때 암호화가 필요할 때가 많음
  • 암호기는 암호화와 복호화 작업을 지원하며 SSCM에는 이를 위해 BytesEncryptor 및 TextEncryptor라는 두 유형의 암호기가 정의되어 있음
    • 이들 암호기는 역할은 비슷하지만 다른 데이터 형식을 처리함
    • TextEncryptor는 데이터를 문자열로 관리함

 

 

  • 암호기를 구축하고 이용하는 옵션은 다음과 같음
    • 팩토리 클래스 Encryptors는 여러 가능성을 제공하며, BytesEncryptor의 경우 아래와 같이 Encryptors.standard() 또는 Encryptors.stronger() 메서드를 이용할 수 있음


 

  • 내부적으로 표준 바이트 암호기는 256 바이트 AES 암호화를 이용해 입력을 암호화함
  • 더 강력한 바이트 암호기 인스턴스를 만들려면 Encryptors.stronger() 메서드를 호출하면 됨

 

BytesEncryptor e = Encryptors.stronger(password, salt);

 

 

  • TextEncryptors는 세 가지 주요 형식이 있으며 Encryptors.text(), Encryptors.delux(), Encryptors.queryableText() 메서드를 호출해 이러한 형식을 생성할 수 있음
    • 암호기를 생성하는 이러한 메서드 외에도 값을 암호화하지 않는 더미 TextEncryptor를 반환하는 메서드도 있음
    • 암호화에 시간을 소비하지 않고 애플리케이션의 성능을 테스트하기를 원할 때나 데모 예제에는 더미 TextEncryptor를 이용할 수 있음

 

String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.noOpText(); // 더미 암호기
String encrypted = e.encrypt(valueToEncrypt);

 

 

  • Encryptors.text() 암호기는 Encryptors.standard() 메서드로 암호화 작업을 관리하고 Encryptors.delux() 메서드는 다음과 같이 Encryptors.stronger() 인스턴스를 이용함

 

String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";

TextEncryptor e = Encryptors.text(password, salt);
String encrypted = e.encrypt(valueToEncrypt);
String decrypted = e.decrypt(encrypted);

 

  • Encryptors.text() 및 Encryptors.delux()의 경우 같은 입력으로 encrypt() 메서드를 반복 호출해도 다른 출력이 반환되는데 그 이유는 암호화 프로세스에 임의의 초기화 벡터가 생성되기 때문
    • 만약 같은 출력이 반환되길 원한다면 쿼리 기능 텍스트를 사용하면 되고 이 상황에서는 Encryptors.queryableText() 인스턴스를 이용

 

String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";

// 쿼리 기능 텍스트 암호기 생성
TextEncryptor e = Encryptors.queryableText(password, salt);
String encrypted1 = e.encrypt(valueToEncrypt);
String encrypted2 = e.encrypt(valueToEncrypt);

 

 

참고

스프링 시큐리티 인 액션

반응형

'Spring > 스프링 시큐리티 인 액션' 카테고리의 다른 글

[5장] 인증 구현  (1) 2025.05.21
[3장] 사용자 관리  (0) 2025.05.18
[2장] 스프링 시큐리티 기본 구성  (0) 2025.05.18