UserDetailsService
사용자의 신원, 권한, 자격 증명 등과 같은 정보를 포함한 상세 데이터를 로드하는 인터페이스이며 스프링 시큐리티에서 미리 제공해주는 인터페이스이다.
즉, 단순하게 생각하면 그냥 조회용 서비스 메서드인셈이다. 사실 이런 조회 기능은 굳이 스프링에서 제공하는 인터페이스를 사용하지 않고 우리가 따로 얼마든지 서비스 클래스에 정의하고 사용할 수 있다.
하지만, 스프링에서 굳~이 인터페이스로 똑 떼어내서 제공하고 있는것은 다 이유가 있지 않을까?
만일 스프링시큐리티가 제공하는 UserDetailsService 인터페이스를 사용하지 않고 기존에 사용 혹은 정의 해둔 User (혹은 Member, Account 등 유저정보를 담고있는 모든 엔티티) 엔티티에 관한 UserService 를 사용한다고 가정해보자.
이 경우, UserService 에는 조회 뿐 아니라 삽입, 삭제, 수정 등 수 많은 비즈니스 로직이 담겨 있을것이다. 문제는 이런 비즈니스 로직과 보안과 인증 로직들이 UserService 라는 하나의 객체에 전부 포진되어있다면 관심사의 분리 가 이뤄지지 못해서 UserSerivce 객체가 너무 커져서 유지보수 관리가 어려워 질 수 있다.
그냥
loadUserByUsername()하나만 정의 하면 되는거 아니야? 그렇게 안 커질것 같은데?
라는 생각을 할 수도 있다.
하지만, 사실 실무에서는 UserDetailsService 에서는 보다 더 많은 기능을 추가 할 수 있다.
1) 사용자 정보 조회
2) 비밀번호 해싱 및 암호화
3) 사용자 상태 관리
4) 사용자 인증 로그 기록
5) 권한 부여 및 권한 검증
6) 사용자 세션 관리
7) 사용자 정보 업데이트
8) 소셜 로그인 연동
9) 다중 인증 방식 지원
10) 사용자 정보 캐싱
...
위에 언급된 모든 기능들이 비즈니스 로직과 같이 혼재되어있는 클래스에 다 정의 되어있다고 생각해보아라. 그 클래스는 감당하기 힘들 정도로 몸집이 커져 있을것이다. 그렇기 때문에 인터페이스를 분리함으로써 관심사를 분리하는것이다.
추가적으로 UserDetailsService 를 사용하는 객체는 AuthenticationProvider 이다. 이때 AuthenticationProvider 가 비즈니스 로직 + 보안 인증 로직 이 모두 혼재 된 객체를 가져다 쓸 경우 ISP (Interface Segregation Pricipal) 원칙에 위배된다. 클라이언트는 필요한 메서드에만 의존해야하고 이를 위해서는 인터페이스를 분리 할 수 밖에 없다.
사실 이것도 결국 관심사의 분리인 셈이다.
앞에서 살짝 언급 했지만 UserDetailsSerivce 는 AuthenticationProvider 가 사용을 하는 객체이며 AuthenticationProvider 가 UserDetailsSerivce 에게 유저정보를 DB로부터 조회해오는 것을 위임하는 것이다.
UserDetails 는 스프링 시큐리티에서 사용 하는 사용자 타입으로 사용자의 기본 정보를 저장하는 인터페이스이다. 이 UserDeatils 는 추후에 인증 절차에서 Authentication 객체에 포함 되어 사용되기 때문에 중요한 인터페이스이다.
흐름을 코드로 살펴보면 ,
UserDetailsSerivce 흐름
1)
AuthenticationProvider가UserDetailsSerivce에게 유저정보 조회를 위임한다. (loadUserByUsername()을 호출)![]()
2)
UserDetailsService는 레포지토리를 이용해서 DB로부터 유저정보 데이터를 조회한 다음 이를UserInfo라는 타입의 엔티티 받아온다. (사진은InMemoryUserDetailManager이기 때문에 DB가 아니라 메모리에서 꺼내온다)3) 그리고
UserInfo로 부터 필요한 데이터들을 추출하여UserDetails새로 생성하고 이를 반환해준다.![]()
※ 만일 엔티티가 없다면
UserNotFoundException예외를 던진다.
기본적으로 스프링 시큐리티에서 제공하는 UserDetailsService 의 구현체는 다음과 같다.
InMemoryUserDetailsManagerJdbcDaoImpl@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/images/**", "/js/**", "/favicon/**", "/*/icon-*").permitAll()
.requestMatchers("/", "/signup").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll());
return http.build();
}
// 이렇게 빈으로 등록만 해주면 따로 작업 안 해도됨.
@Bean
public UserDetailsService customUserDetailsService() {
return new CustomUserDetailsSerivce();
}
}
그냥 빈으로 등록 만 해주면 알아서 스프링시큐리티가 UserDetailsService 를 주입해준다.
(컴포넌트 스캔 도 가능하다.)
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// UserDetailsService 인스턴스 생성
UserDetailsService customUserDetailsService = new CustomUserDetailsSerivce();
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/images/**", "/js/**", "/favicon/**", "/*/icon-*").permitAll()
.requestMatchers("/", "/signup").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll()
.userDetailsService(customUserDetailsService)); // 명시적으로 userDetailsService 등록해주기
return http.build();
}
}
먼저 인스턴스를 생성해준 다음, 명시적으로 customUserDetailsService 인스턴스를 주입해줘야한다.
코드를 볼 때 위에서 살펴본 흐름도와 같이 보면 어느정도 이해가 갈 것이다.
본 포스트는
스프링 시큐리티 완전 정복 6.x 개정판 를 보고 정리했습니다.