본문 바로가기
개발 이론/Spring Security

[Spring Security] PasswordEncoder

by dal_been 2023. 11. 2.
728x90

 

PasswordEncoder 계약의 이해

 

인증 공급자에서 AuthenticationProvider는 인증 논리를 구현한다

사용자의 암호를 검증하기 위해 PasswordEncoder가 필요하다

 

사용자 세부정보 서비스를 통해 사용자를 찾은후 AuthenticationProvider는 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 구현해보자

 

 

위의 encode() 메서드와 mathes() 메서드는 밀접한 관계가 있다

encode()에서 반환된 문자열은 항상 같은 PasswordEncoder의 matches() 메서드로 검증할 수 있어야한다

 

 

1. NoOpPasswordEncoder

 

가장 직관적인 암호 인코더 구현으로 암호를 인코딩하지 않고 일반 텍스트로 간주한다

아마 클래스로 직접 작성하면 아래와 같을 것이다

 

public PlainTestPasswordEncoder implements PasswordEncoder{
	
    @Override
    public String encode(CharSequence rawPassword){
    	return rawPassword.toString();
    }
    
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword){
    	return rawPassword.equals(encodedPassword);
    }
}

 

 

2. SHA - 512 해싱 알고리즘을 이용한 PasswordEncoder

 

encode 파라미터로 제공된 문자열 값을 SHA-512로 해시하고

matches에서는 입력된 원시 암호를 해시하고 주어진 해시와 비교해 검증할것이다

 

 

 

PasswordEncoder 제공되는 구현체 (암호화가능,복호화 불가)

 

 

1. NoOpPasswordEncoder

- 암호를 인코딩하지 않음

- 이론적인 예제 용도로만 적당하며 실제 시나리오에는 절대 쓰지 말아야함

 

 

2. StandardPasswordEncoder

- SHA-256 로 암호를 해싱함

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

 

- 빈 문자열을 넣으면 키값으로 이용됨

- 하지만 SHA-256은 약한 해싱 알고리즘이라 잘 사용하지 않는다

 

 

3. Pbkdf2PasswordEncoder

PasswordEncoder p = new Pbkdf2PasswordEncoder();
PasswordEncoder p = new Pbkdf2PasswordEncoder("test");
PasswordEncoder p = new Pbkdf2PasswordEncoder("test",18500,256);

- 반복 횟수만큼 HMAC를 수행하는 아주 단순하고 느린 해싱 함수

- 마지막 생성자를 보면 차례대로 키의 값, 암호 인코딩의 반복 횟수, 해시의 크기

- 반복 횟수와 길이를 늘리면 암호가 강력해짐

- 다만 성능에 영향을 주며 반복횟수를 늘리면 애플리케이션이 소비하는 리소스가 증가 → 해시 생성에 사용되는 리소스와 필요한 인코딩 강도 사이에서 신중하게 절충해야함

 

 

4. BCryptPasswordEncoder

- bcrypt 강력 해싱 함수로 암호를 인코딩함

- 인수가 없는 생성자를 호출해도 되지만, 인코딩 프로세스에 이용되는 로그 라운드를 나타내는 강도 계수를 지정할 수 있음

- 또한 인코딩에 이용되는 SecureRandom 인스턴스를 변경할 수 있음

 

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

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

 

로그라운드

- 지정한 로그라운드 값은 해싱 작업이 이용하는 반복 횟수에 영향을 줌

- 로그라운드 값은 4에서 31 사이여야한다 (두번째, 세번재 생성자처럼 호출하면됨)

 

 

5. SCryptPasswordEncoder

-scrypt 해싱 함수를 이용 

 

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

 

두번째 생성자를 보면

→ 첫번째 매개변수 : CPU 비용

→ 두번째 매개변수 : 메모리 비용

→ 세번째 매개변수 : 병렬화 계수

→ 네번째 매개변수 : 키 길이

→ 다섯번째 매개변수 : 솔트 길이

 

 

 

 

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

 

다양한 방식의 암호화 알고리즘을 적용할 수 있게 해줌

 

- PasswordEncoder 인터페이스의 한 구현이며 자체 인코딩 알고리즘을 구현하는 대신 같은 계약의 다른 구현 인스턴스에 작업을 위임함

- 해시는 해당 해시를 믜미하는 알고리즘의 이름으로 나타내는 접두사로 시작

 

@Configuration
public class Config{

	
   ....
   
   @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);
   }
}

 

- 해시 접두사를 기준으로 암호를 비교하기 위해 올바른 PasswordEncoder구현을 선택

-만약 접두사가 없다면 기본 인코더를 이용 → 기본 인코더는 인스턴스 만들때 첫 번째 매개변수로 지정한 "bcrypt" 임

 

 

 

★ 잠깐 인코딩, 암호화, 해싱이 헷갈리는가 ??

인코딩
→ 주어진 입력에 대한 모든 변환이다

암호화 
→ 출력을 얻기 위해 입력 값과 키를 모두 지정하는 특정한 유형의 인코딩
→ (x, k ) -> y : x는 입력, k는 키 , y 는 암호화 결과이다
→ 여기서 역함수 복호화는 (y, k) -> x
→ 암호화와 복호화에 쓰는 키가 같으면 대칭키라고 한다
→ 비대칭 키 :  암호화(x, k1 )-> y     복호화(y, k2) ->  x   (k1,k2)를 키 쌍이라고 하고, k1을 공개키 k2를 개인키라고 한다

해싱
→ 함수가 한 방향으로만 작동하는 특정한 유형의 인코딩
→ 해싱 함수의 결과 y에서 x값을 얻을 수 없다(복호화 불가능)
→ 다만 해시는 인코딩과 일치를 위한 한 쌍의 함수를 볼 수 있다 (x,y) -> 일치하는가? boolean
→ 해싱함수는 입력에 임의 값을 추가할 수 있는데 솔트라고 한다. (솔트 : 함수를 더 강하게 만들어 결과에서 입력을 얻는 역함수의 적용 난도를 높임)

 

 

 

정리

이때까지 설명한 스프링 시큐리티에서 인증흐름을 위한 주 인터페이스들 간단하게 정리해보자

 

인터페이스 설명
UserDetails 스프링 시큐리티가 관리하는 사용자를 나타냄
GrantedAuthority 애플리케이션의 목적 내에서 사용자에게 허용된 작업을 정의
UserDetailsService 사용자 이름으로 사용자 세부 정보를 검색하는 객체
UserDetailsManager UserDetailsService보다 더 구체적. 사용자 이름으로 검색 외에도 사용자 컬렉션이나 특정 사용자를 변경가능
PasswordEncoder 암호를 암호화,해시,비교하는 방법을 지정

 

 

 


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

 

암호화,복호화 , 키생성 기능은 자바에서 제공하지 않지만 스프링 시큐리티에서 자체 솔루션을 제공한다

 

SSCM

-키 생성기 : 해싱 및 암호화 알고리즘을 위한 키를 생서하는 객체

- 암호기 : 데이터를 암호화 및 복호화하는 객체

 

키 생성기이용

 

특정한 종류의 키를 생성하는 객체로서 일반적으로 암호화나 해싱 알고리즘에 필요하다

 

BytesKeyGenerator, StringKeyGenerator는 키 생성기의 두가지 주요 유형을 나타내는 인터페이스다.

팩터리 클래스 KeyGenerator로 직접 만들 수 있다.

 

StringKeyGenerator 

계약으로 나타내느 문자열 키 생성기를 이용해 문자열 키를 얻을 수 있다

- 이 키는 해싱 또는 암호화 알고리즘의 솔트 값으로 이용된다

 

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

 

 

BytesKeyGenerator

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

 

-KeyGenerators.secureRandom() 은 generateKey() 호출할때 마다 고유한 키를 생성

- 같은 키 생성기를 호출하면 같은 키를 반환하는 구현이 적합할때는 → KeyGenerators.shared(Int length)

                                                                                               

 

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

 

암호기는 암호화 알고리즘을 구현한 객체

암호화와 복호화 작업을 지원하는 SSCM에는 이를 위해 BytesEncryptor 및 TextEncryptor라는 두 유형의 암호기가 정의돼있음

 

역할은 비슷하지만 다른 데이터 형식으로 처리한다

 

TextEncryptor는 데이터를 문자열로 관리

public interface TextEncryptor{

	String encrypt(String text);
     String decrypt(String encryptedText);
    
}

 

BytesEncryptor은 더 범용적이며 바이트 배열로 입력 데이터를 받음

public interface BytesEncryptor{

	byte[] encrypt(byte[] byteArray);
    byte[] decrypt(byte[] encryptByteArray);
}

 


 

 

암호기를 구축하고 이용하는 옵셥을 알아보자

팩터리 클래스 Encryptors는 여러 가능성을 제공하며,

 

BytesEncryptor의 경우 Encryptors.standard() 또는 Encryptors.stronger() 이용할 수 있다

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

BytesEncryptor e= Encryptors.standard(password,salt);
byte [] encrypted= e.encrypt(valueToEncrypt.getBytes());
byte [] decrypted = e.decrypt(encrypted);

 

- 내부적으로 표준 바이트 암호기 256바이트 AES암호화를 이용해 입력을 암호화 한다

- 더강력한 암호기를 원하면 Encryptors.stronger(password,salt) 호출하면 된다

 

 

TextEncryptors 는 Encryptors.text(), Encryptors.delux(), Encryptors.queryableText() 메서드를 호출해 암호화를 할 수 있다. 이외에도 암호화 하지 않는 값을 주는 더미 TextEncryptors가 있는데 암호화 시간을 소비하지 않고 애플리케이션 성능을 테스트하기 원할때마다 사용된다

 

String valueToEncrypt="HARU";
TextEncryptor e = Encryptors.noOpText();
String encrypted = e.encrypt(valueToEncrypt);

- 더미 TextEncryptor이기에 encrypted 와 valueToEncrypt는 같다

 

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

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

- Encryptors.tex()나 Encryptors.delux() 의경우 같은 입력으로 encrypt() 해도 다른 출력이 반환됨

→ 암호화 프로세스에 임의의 초기화 벡터가 생성되기 때문

→ 하지만 실제 상황에서 이러한 작동 방식을 원치 않을 수 있음 → Encryptors.queryableText() 하면 같은 값으로 여러번 메서드 호출해도 값이 같음

 

 

 

요약

- PasswordEncoder는 인증 논리에서 암호를 처리하는 가장 중요한 책임을 담당

- 스프링 시큐리티는 해싱 알고리즘에 여러 대안을 제공하믕로 필요한 구현을 선택하기만 하면 됨

- 스프링 시큐리티 암호화 모듈에는 키생성기, 암호기를 구현하는 여러 대안이 존재

- 키 생성기는 암호화 알고리즘에 이용되는 키를 생성하도록 도와주는 유틸리티 객체

- 암호기는 데이터 암호화 , 복호화를 수행하도록 도와주는 유틸리티 객체