UserDetailsService

나무·2024년 5월 7일

스프링 시큐리티

목록 보기
3/8
post-thumbnail

1. UserDetailsService 란?

UserDetailsService

사용자의 신원, 권한, 자격 증명 등과 같은 정보를 포함한 상세 데이터를 로드하는 인터페이스이며 스프링 시큐리티에서 미리 제공해주는 인터페이스이다.

즉, 단순하게 생각하면 그냥 조회용 서비스 메서드인셈이다. 사실 이런 조회 기능은 굳이 스프링에서 제공하는 인터페이스를 사용하지 않고 우리가 따로 얼마든지 서비스 클래스에 정의하고 사용할 수 있다.

하지만, 스프링에서 굳~이 인터페이스로 똑 떼어내서 제공하고 있는것은 다 이유가 있지 않을까?

관심사의 분리 (뇌피셜 90% -> 오류 지적 환영)

만일 스프링시큐리티가 제공하는 UserDetailsService 인터페이스를 사용하지 않고 기존에 사용 혹은 정의 해둔 User (혹은 Member, Account 등 유저정보를 담고있는 모든 엔티티) 엔티티에 관한 UserService 를 사용한다고 가정해보자.

이 경우, UserService 에는 조회 뿐 아니라 삽입, 삭제, 수정 등 수 많은 비즈니스 로직이 담겨 있을것이다. 문제는 이런 비즈니스 로직과 보안과 인증 로직들이 UserService 라는 하나의 객체에 전부 포진되어있다면 관심사의 분리 가 이뤄지지 못해서 UserSerivce 객체가 너무 커져서 유지보수 관리가 어려워 질 수 있다.

그냥 loadUserByUsername() 하나만 정의 하면 되는거 아니야? 그렇게 안 커질것 같은데?

라는 생각을 할 수도 있다.

하지만, 사실 실무에서는 UserDetailsService 에서는 보다 더 많은 기능을 추가 할 수 있다.

1) 사용자 정보 조회
2) 비밀번호 해싱 및 암호화
3) 사용자 상태 관리
4) 사용자 인증 로그 기록
5) 권한 부여 및 권한 검증
6) 사용자 세션 관리
7) 사용자 정보 업데이트
8) 소셜 로그인 연동
9) 다중 인증 방식 지원
10) 사용자 정보 캐싱
...

위에 언급된 모든 기능들이 비즈니스 로직과 같이 혼재되어있는 클래스에 다 정의 되어있다고 생각해보아라. 그 클래스는 감당하기 힘들 정도로 몸집이 커져 있을것이다. 그렇기 때문에 인터페이스를 분리함으로써 관심사를 분리하는것이다.

ISP (Interface Segregation Pricipal)

추가적으로 UserDetailsService 를 사용하는 객체는 AuthenticationProvider 이다. 이때 AuthenticationProvider 가 비즈니스 로직 + 보안 인증 로직 이 모두 혼재 된 객체를 가져다 쓸 경우 ISP (Interface Segregation Pricipal) 원칙에 위배된다. 클라이언트는 필요한 메서드에만 의존해야하고 이를 위해서는 인터페이스를 분리 할 수 밖에 없다.

사실 이것도 결국 관심사의 분리인 셈이다.

2. UserDetailsService 흐름

앞에서 살짝 언급 했지만 UserDetailsSerivceAuthenticationProvider 가 사용을 하는 객체이며 AuthenticationProviderUserDetailsSerivce 에게 유저정보를 DB로부터 조회해오는 것을 위임하는 것이다.

UserDetails 는 스프링 시큐리티에서 사용 하는 사용자 타입으로 사용자의 기본 정보를 저장하는 인터페이스이다. 이 UserDeatils 는 추후에 인증 절차에서 Authentication 객체에 포함 되어 사용되기 때문에 중요한 인터페이스이다.

흐름을 코드로 살펴보면 ,

UserDetailsSerivce 흐름

1) AuthenticationProviderUserDetailsSerivce 에게 유저정보 조회를 위임한다. (loadUserByUsername() 을 호출)

2) UserDetailsService 는 레포지토리를 이용해서 DB로부터 유저정보 데이터를 조회한 다음 이를 UserInfo 라는 타입의 엔티티 받아온다. (사진은 InMemoryUserDetailManager 이기 때문에 DB가 아니라 메모리에서 꺼내온다)

3) 그리고 UserInfo 로 부터 필요한 데이터들을 추출하여 UserDetails 새로 생성하고 이를 반환해준다.

※ 만일 엔티티가 없다면 UserNotFoundException 예외를 던진다.

3. UserDeatilsSerivce 사용법

기본 제공 구현체

기본적으로 스프링 시큐리티에서 제공하는 UserDetailsService 의 구현체는 다음과 같다.

  • InMemoryUserDetailsManager
    사용자 정보를 메모리에 저장하여 인증을 처리하며 테스트 용으로 주로 사용된다.

  • JdbcDaoImpl
    JDBC 기술을 사용하여 구현한 구현체이다. 만일 JDBC 기술을 이용한다면 커스텀 서비스를 정의할 때 이 클래스를 상속받으면 된다. 다만 JPA를 사용할 경우엔 필요없다.

기본적인 사용법

1) 빈으로 등록 하여 사용하기

@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 를 주입해준다.
(컴포넌트 스캔 도 가능하다.)

2) 빈으로 등록 안 하고 사용하기

@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 인스턴스를 주입해줘야한다.

UserDeatilsService 커스텀 하기

코드를 볼 때 위에서 살펴본 흐름도와 같이 보면 어느정도 이해가 갈 것이다.

본 포스트는
스프링 시큐리티 완전 정복 6.x 개정판 를 보고 정리했습니다.

profile
🍀 개발을 통해 지속 가능한 미래를 만드는데 기여하고 싶습니다 🍀

0개의 댓글