태그에 '삽질'이 붙은 글들은 앞으로 내가 공부를 하면서 이해가 잘 되지 않았던 부분(하지만 다른 개발자들은 당연히 알 만한...) 그런 부분들을 적어 보고자 한다.
Spring이라는 프레임워크가 참 거대하다보니... 아무래도 기본 사용법 부터 익히게 되는데 그러다 보니 원리를 이해하기가 힘들어서 다른 부분에 응용하기가 힘들다.
그래서 이렇게 좀 바보 같더라도 내가 납득할 수 있을 때까지 파고드는 과정을 기록으로 남겨두면 도움이 되지 않을까 싶어서 이런 글을 적게 되었다.
이 글을 적게 된 계기는 Spring Security를 공부하면서 마주친 아래의 코드이다.
@Configuration
public class SpringSecurityConfiguration {
@Bean
public InMemoryUserDetailsManager createUserDetailsManager() {
UserDetails userDetails = User.builder()
.passwordEncoder(input -> customPasswordEncoder().encode(input))
.username("userId")
.password("testPw")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder customPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
기존에 withDefaultPasswordEncoder()
메서드를 대체하기 위해서 만든 코드인데, 아래의 customPasswordEncoder() 메소들르 통해서 PasswordEncoder를 따로 빈으로 등록하는 부분이 좀 의문스러웠다.
뭔가 형태를 봤을 때 User.builder()
뒤에 따라 나오는 .passwordEncoder()
부분에서 비밀번호를 인코딩하는 람다 함수를 등록하니까, 나중에 사용자로부터 비밀번호를 입력 받으면 그 람다 함수를 통해 인코딩을 하면 된다고 생각하였다.
그래서 customPasswordEncoder()
메소드 위에 붙어 있는 @Bean
어노테이션을 제거하고 어플리케이션을 실행해 보았다.
아... 정확히는 모르겠지만 PasswordEncoder를 찾지 못한 것 같다.
빈으로 등록한 PasswordEncoder를 어찌 되었든 로그인 과정에서 사용하는 것 같은데, 이 PasswordEncoder 빈을 언제 어디서 가져오는지 궁금해져서 브레이크 포인트를 적절히 걸어가며 찾아보았다.
일단 처음 살펴본 부분은 DaoAuthenticationProvider
클래스의 additionalAuthenticationChecks
메소드이다.
userDetails
에는 위의 SpringSecurityConfiguration
클래스에서 등록한 사용자 정보가 들어간다.
그리고 authentication
에는 사용자가 로그인 페이지에서 입력한 ID, PW를 비롯한 인증 정보가 들어가는데,
String presentedPassword = authentication.getCredentials().toString();
이 코드를 통해 사용자가 로그인 페이지에서 입력한 패스워드 원문을 받아온다.
그런 다음
this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())
이 코드를 통해서 사용자가 입력한 패스워드와 서버에 저장되어 있는 유저의 패스워드가 일치하는지 확인한다.
이때, presentedPassword
는 그냥 평문인데 반해 userDetails.getPassword()
를 통해 가져온 패스워드 데이터는 암호화가 진행된 상태이다.
즉, 위 코드는 DaoAuthenticationProvider
클래스가 멤버 변수로 갖고 있는 passwordEncoder
를 이용해 입력 받은 비밀번호를 인코딩하고, 이를 서버에 저장되어 있는 것과 비교해준다.
나는 userDetails.getPassword()
에 암호문이 저장되어 있는 것을 보고
UserDetails userDetails = User.builder()
.passwordEncoder(input -> customPasswordEncoder().encode(input))
.username("userId")
.password("testPw")
.roles("USER", "ADMIN")
.build();
여기서 넣어준 람다 함수는 최초에 유저 정보를 저장할 때 1회용으로 사용되는 것임을 깨달았다.
즉... 람다 함수로 넣어줬던 비밀번호 인코딩 과정은 최초 유저 정보를 저장할 때만 사용되고, 이후 실제 인증에서는 우리가 빈으로 등록한 인코더를 사용하는데, 그걸 등록을 안해주니 에러가 발생하는 것이었다.
궁금했던 부분은 해결이 됐지만, 아직 "등록한 PasswordEncoder
빈을 어디서 가져오는지 아직 찾지 못했다. 그냥 끝내긴 아쉬우니 코드를 조금 더 살펴보기로 했다.
DaoAuthenticationProvider
오브젝트에는 멤버 변수로 passwordEncoder
를 갖고 있었다. 이는 곧 생성자에서 해당 객체를 주입 받는 것이라 생각하여 생성자를 살펴보았다.
passwordEncoder
에 대한 setter가 있음을 확인할 수 있었다.
아마 이 setter를 호출하는 코드 근처에서 우리가 등록한 인코더 빈을 가져오는 코드가 있을 것이다.
해당 setter에 브레이크 포인트를 걸고 콜스택을 뒤져보았다.
위 코드의 PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
이 부분에서 우리가 등록한 빈을 가져옴을 알 수 있다.