프레임워크(Framework)/Spring

[Spring Security] 스프링 시큐리티 DelegatingPasswordEncoder - 암호화 알고리즘 변경하기

잇트루 2022. 12. 31. 16:13
반응형

DelegatingPasswordEncoder

DelegatingPasswordEncoder는 스프링 시큐리티에서 지원하는 PasswordEncoder 인터페이스의 구현 객체를 생성해주는 컴포넌트이다.

DelegatingPasswordEncoder를 통해 애플리케이션에서 사용할 PasswordEncoder를 결정하고, 사용자가 입력한 패스워드를 단방향으로 암호화한다.

 

DelegatingPasswordEncoder는 스프링 시큐리티 5.0부터 등장했으며, 이전 버전에서는 평문 텍스트(Plain text) 패스워드를 그대로 사용하는 NoOpPasswordEncoder가 PasswordEncoder의 default로 고정되어 있었다.

하지만, NoOpPasswordEncoder를 사용하면서 발생하는 문제를 해결하기 위해 등장한 것이 DelegatingPasswordEncoder이며, PasswordEncoder를 더욱 유연한 구조로 사용할 수 있게 되었다.

 

DelegatingPasswordEncoder 도입 전 문제점

패스워드 인코딩 방식을 마이그레이션하기 쉽지 않은 오래된 방식을 사용하고 있는 경우

  • 패스워드 단방향 암호화에 사용되는 해시 알고리즘은 지속적으로 더 안전한 해시 알고리즘이 등장한다.
  • 따라서 항상 고정된 암호화 방식을 사용하는 것은 바람직한 사용방식이 아니다.
  • 오래된 방식의 경우 보안에 취약한 해시 알고리즘을 사용하고 있을 가능성이 있어 해커에 타겟이 될 수 있다.

 

스프링 시큐리티는 프레임워크이기 때문에 하위 호환성을 보장하지 않는 업데이트를 자주 할 수 없다.

오래된 하위 버전의 기술은 더이상 사용하지 않는(Deprecated)것처럼 보안에 취약한 오래된 방식의 암호화 알고리즘은 언젠가 관리 대상이 아니게 될 수 있다.

 

DelegatingPasswordEncoder의 장점

  • DelegatingPasswordEncoder를 사용하여 다양한 방식의 암호화 알고리즘을 적용할 수 있다.
  • 암호화 알고리즘을 특별히 지정하지 않아도 스프링 시큐리티에서 권장하는 최신 암호화 알고리즘을 사용하여 패스워드 암호화를 할 수 있도록 해준다.
  • 패스워드 검증에 있어 오래된 레거시 방식의 암호화 알고리즘으로 암호화된 패스워드의 검증을 지원한다.
  • 암호화 방식을 변경하고 싶다면 언제든지 암호화 방식을 변경할 수 있다.
    • 단, 기존에 암호화되어 저장된 패스워드에 대한 마이그레이션 작업이 진행되어야 한다.

 

 

DelegatingPasswordEncoder를 이용한 PasswordEncoder 생성

다음과 같이 DelegatingPasswordEncoder 사용하여 PasswordEncoder를 생성할 수 있다.

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
  • PasswordEncoder는 PasswordEncoderFactories를 통해 생성한다.
  • PasswordEncoderFactories에는 DelegatingPasswordEncoder 객체를 생성하는 createDelegatingPasswordEncoder() 메서드가 있다.
  • 따라서 createDelegatingPasswordEncoder()를 통해 DelegatingPasswordEncoder 객체를 생성하고, 내부적으로 DelegatingPasswordEncoder가 적절한 PasswordEncoder 객체를 생성하는 구조이다.

 

Custom DelegatingPasswordEncoder

스프링 시큐리티에서 제공하는 PasswordEncoderFactories 클래스를 이용하여 스프링에서 권장하는 PasswordEncoder를 사용할 수 있게 된다.

하지만, 필요에 따라 DelegatingPasswordEncoder로 직접 PasswordEncoder를 지정할 수 있다.

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);

위 코드와 같이 encoders에 원하는 유형에 PasswordEncoder를 추가하여 DelegatingPasswordEncoder의 생성자를 통해 디폴트 값으로 지정한 PasswordEncoder를 사용할 수 있다.

 

 

Custom DelegatingPasswordEncoder 사용

@SpringBootApplication
public class DelegatingTest {
    public static void main(String[] args) {

        String idForEncode = "bcrypt";
        Map encoders = new HashMap<>();
        encoders.put(idForEncode, new BCryptPasswordEncoder());
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("sha256", new StandardPasswordEncoder());

        PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
        String password1 = passwordEncoder.encode("hello");

        passwordEncoder = new DelegatingPasswordEncoder("noop", encoders);
        String password2 = passwordEncoder.encode("hello");

        passwordEncoder = new DelegatingPasswordEncoder("pbkdf2", encoders);
        String password3 = passwordEncoder.encode("hello");

        passwordEncoder = new DelegatingPasswordEncoder("scrypt", encoders);
        String password4 = passwordEncoder.encode("hello");

        passwordEncoder = new DelegatingPasswordEncoder("sha256", encoders);
        String password5 = passwordEncoder.encode("hello");

        System.out.println("password1 = " + password1);
        System.out.println("password2 = " + password2);
        System.out.println("password3 = " + password3);
        System.out.println("password4 = " + password4);
        System.out.println("password5 = " + password5);

        System.out.println(passwordEncoder.matches("hello", password1));
        System.out.println(passwordEncoder.matches("hello", password2));
        System.out.println(passwordEncoder.matches("hello", password3));
        System.out.println(passwordEncoder.matches("hello", password4));
        System.out.println(passwordEncoder.matches("hello", password5));
    }
}
password1 = {bcrypt}$2a$10$BmPysafaHDFOA5JrFOb1RubKYBT.zoz3rPZm40Q.pVjZ2DdJXNW/G
password2 = {noop}hello
password3 = {pbkdf2}ad8f4d9ba2cf013d7fcd83981a99604f4e09efac078c79f77fdb924ae9aaaf98515ce969163a91e2
password4 = {scrypt}$e0801$c4+Llq6McLlS4q4pL/JFNkXNbh9peIin+4B2RFm3SAsYj2Ek+rqedYb05wHv3tYsa2ZxxRmYT6IxWj8ezoojdg==$x3BDwoqtrAMihkWqD8zZhnx5xSiPbEmtMRG/kR2BbvI=
password5 = {sha256}abd4d618da0e75c9b678788336fd457f0a2acf249b5a0fea25b9edcc79c2266f7fd869dfdaaac0a7

 

암호화된 패스워드 포맷

스프링 시큐리티에서는 패스워드를 암호화할 때, 암호화 알고리즘 유형을 prefix로 추가한다.

따라서, 암호화된 패스워드의 포맷은 다음과 같이 나타낸다.

{id}encodedPassword

띠라서 Custom DelegatingPasswordEncoder에서 지원하는 단방향 암호화 알고리즘은 위에서 출력한 결과처럼 나타난 것이다.

 

검증

PasswordEncoder 인터페이스의 matches() 메서드를 통해 패스워드가 암호화된 패스워드와 일치하는지에 대하여 알 수 있다.

System.out.println(passwordEncoder.matches("hello", password1));
System.out.println(passwordEncoder.matches("hello", password2));
System.out.println(passwordEncoder.matches("hello", password3));
System.out.println(passwordEncoder.matches("hello", password4));
System.out.println(passwordEncoder.matches("hello", password5));
true
true
true
true
true

 

참고사항

SCrypt 암호화 알고리즘은 bouncycastle이라는 라이브러리를 추가하여 사용할 수 있다.

build.gradle

dependencies {
    ...
    implementation 'org.bouncycastle:bcprov-jdk15to18:1.72' // 추가
    ...
}
반응형