3. Spring Security 기본 로직 이해하기

우선 이 글에서는 Spring Security의 흐름을 이해하기 위해 전체 동작 원리만 정리해보았다. 그리고 다음 글에서는 코드를 기능 단위(회원 가입/로그인/권한 처리)로 정리해보았다.

Spring Security 로직

스프링 시큐리티는 서블릿 필터를 기반으로 동작한다. 즉, 요청이 서블릿으로 보내지기 전에 시큐리티가 동작한다. 로그인은 전체 어플리케이션 시작전에 항상 이루어져야 하므로, 필터(시큐리티)를 통해 로그인 인증을 한다.

시큐리티는 여러 인터페이스들을 거쳐 진행된다. 각각의 인터페이스를 상속받아서 Custom하여 구현할 수 있다. 시큐리티를 사용하면 스프링만으로 구현할 때보다 로그인 페이지로 이동/인증/권한 처리를 쉽게 할 수 있다. 일단 각각의 인터페이스의 역할을 글로 간단히 정리하면 다음과 같다.

  1. AuthenticationFilter : 필터 등록하고, 이미 로그인 세션이 있는지 세션 ID로 SecurityContextHolder에서 확인한다.

  2. AuthenticationManager : 입력받은 정보로 Authentication 객체를 생성한다. Manager에 Provider를 등록해서 사용한다.

  3. AuthenticationProvider : 입력받은 Authentication 객체와 DB의 User 객체가 같은 지 검증한다.

  4. UserDetailsService : DB 접근해서 User 정보 가져온다. (Repository을 통해 DB 접근)

  5. UserDetails : User 정보를 담고 있다.

각각은 인터페이스이므로, 추상 메소드를 가진다. 각 인터페이스를 상속받아 추상 메소드를 오버라이딩하여 각자 프로젝트에 맞게 구현해주면 된다. 우선 이 글에서는 인터페이스의 코드를 정리해보고, 다음 글에서 기능(회원가입/로그인/권한처리) 위주로 코드를 다시 정리해보았다.

위의 모든 인터페이스 + Handler 인터페이스를 상속받아서 Custom하여 구현해줄 수도 있지만, 여기서는 시큐리티의 흐름을 간단히 알아보기 위해 이 프로젝트에서 필요한 인터페이스(AuthenticationProvider, UserDetailsService, UserDetails)들만 직접 구현헤보았다. 글에 올린 코드는 설명을 위해 일부만 첨부했고, 전체 코드는 깃허브에 있다.

SpringSecurityConfig

@EnableWebSecurity으로 SpringSecurityFilterChain에 등록해줄 수 있다.

@Configuration
@EnableWebSecurity // SpringSecurityFilterChain 에 등록
@RequiredArgsConstructor
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/login", "/join").permitAll()     // 모두 접근 가능
                .antMatchers("/admin").hasRole("ADMIN")         // ADMIN 만 접근 가능
                .antMatchers("/main").authenticated()           // 인증해야 접근 가능
                /* 로그인 폼 */
                .and().formLogin()
                .loginPage("/login")
                .usernameParameter("name")
                .passwordParameter("password")
                .defaultSuccessUrl("/main")   // 성공 시 /main
                .failureUrl("/fail")          // 실패 시 /fail
                .permitAll()
		// ...
    }
    // Manager 에 Provider 등록
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider());
    }

    // Provider 생성
    @Bean
    public CustomAuthenticationProvider customAuthenticationProvider() {
        return new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder());
    }
    // ...
  1. AuthenticationFilter : 필터 등록하고, 이미 로그인 세션이 있는지 세션 ID로 SecurityContextHolder에서 확인한다.

  2. AuthenticationManager : 입력받은 정보로 Authentication 객체를 생성한다. Manager에 Provider를 등록해서 사용한다.

위의 Filter와 Mananger도 Custom해줄 수 있지만, 이 프로젝트에서는 생략하였다.

AuthenticationManager와 AuthenticationProvider의 관계는 이름그대로 Manager와 Provider의 관계로 볼 수 있다. AuthenticationManager에 실제 일을 수행하는 AuthenticationProvider을 등록해서 사용한다. 이때 등록은 SpringSecurityConfig 클래스에서 AuthenticationManagerBuilder을 사용해서 한다.

3. AuthenticationProvider

AuthenticationProvider는 입력받은 Authentication 객체와 DB의 User 객체가 같은 지 검증한다. 이 인터페이스를 상속받아 CustomAuthenticationProvider을 구현하였다.

authenticate()는 입력받은 Authentication 객체와 DB의 User 객체의 아이디/비번이 같은지 확인하는 메소드이다. CustomAuthenticationProvider에서는 이 메소드를 자기 프로젝트에 맞게 구현하면 된다. (이 프로젝트에서 아이디는 name 필드이다.)

public class CustomAuthenticationProvider implements AuthenticationProvider {
    // 인증 메소드
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String name = authentication.getName();
        String password = (String) authentication.getCredentials();

        User user = (User) userDetailsService.loadUserByUsername(name);

        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException(user.getUsername() + "비밀번호를 다시 입력해주세요.");
        }
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }
    // ...
}

4. UserDetailsService

UserDetailsService은 DB 접근해서 User 정보 가져온다. (Repository을 통해 DB 접근) 이 인터페이스를 상속받아 CustomUserDetailsService을 구현하였다.

loadUserByUsername()는 DB에 접근하여 User 객체를 가져오는 메소드이다. 이 또한 각자의 프로젝트에 맞게 오버라이딩해주면 된다.

public class CustomUserDetailsService implements UserDetailsService {
    // ...
    @Override
    public UserDetails loadUserByUsername(String name) {

        User user = userRepository.findByName(name);
        if (user == null) {
            throw new UsernameNotFoundException("Can't find user");
        }
        return user;
    }
}

5. UserDetails

UserDetails는 User 정보를 담고 있다. 이 인터페이스를 상속 받아 엔티티 User을 구현했다.

UserDetails의 추상 메소드들,

  • getAuthorities : 해당 계정의 권한 목록 리턴

  • getUsername : 유저이름 리턴

  • isAccountNonExpired : 계정 만료 여부 리턴

등등 오버라이딩해서 구현해주었다.

이번 글에서는 스프링 시큐리티의 전체 흐름을 이해하기 위해 각 인터페이스 코드를 알아보았다. 다음 글에서는 같은 코드를 기능 단위(회원 가입/로그인/권한 처리)로 로직과 코드를 정리해보았다.

Last updated