[Spring Security] 인증과 인가, SecurityFilterChain

olsohee·2023년 8월 4일
1

Spring Security

목록 보기
1/6
post-thumbnail

1. 인증과 인가

Spring Security에서 애플리케이션 보안을 구성하는 영역은 인증과 인가 두 가지이다. Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행한다. 그리고 인가 과정에서는 해당 리소스에 접근 권한이 있는지를 확인한다.

  • 인증(Authentication): 사용자의 신원을 증명하는 과정으로 로그인이 그 예이다.
    인증은 로그인 정보를 받아서 인증된 Authentication을 만드는 과정이다. Authentication은 pricipal, credentials, authorities로 구성되어 있으며, 인증된 Authentication은 SecurityContextHolder에 저장된다.
public interface Authentication extends Principal, Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();

	Object getCredentials();

	Object getDetails();
    
	Object getPrincipal();

	boolean isAuthenticated();

	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
  • 인가(Autorization): 인증된 사용자가 특정 리소스에 접근 가능한지를 결정하는 과정이다.
    인가는 Authenticaion의 authorities, 즉 역할을 이용해서 접근을 제어하는 것이다.

2. SecurityFilterChain

2.1. SecurityFilterChain이란

Spring Security는 인증 처리 과정을 서블릿이 제공하는 필터를 기반으로 처리한다.

Filter 구조는 위와 같다. FilterChain과 SecurityFilterChain으로 나뉘어지는데, FilterChain은 기존 WAS의 필터 체인이고 오른쪽의 SecurityFilterChain스프링 시큐리티에서 사용하는 필터 체인이다.

이때 DelegationgFilterProxy는 무엇일까? SecurityFilterChain이라는 기존 필터 체인과 분리된 필터 체인이 존재하는데, HTTP 요청을 가져와야 하기 때문에 특별한 필터(DelegationgFilterProxy)를 FilterChain에 추가하여 SecurityFilterChain과 연결시켜 주는 역할을 한다.

요청은 ApplicationFilterChain의 순서대로 전달되다가 DelegatinFilterProxy에서 SecurityFilterChain으로 전달되어 그 내부 필터들을 순서대로 지난다. 그리고 다시 ApplicationFilterChain의 나머지 필터들로 전달된다.

정리하자면 다음과 같다.

  • FilterChain: 기존 WAS의 필터 체인

  • SecurityFilterChain: 스프링 시큐리티에서 사용하는 필터 체인으로, 인증과 인가에 대한 필터들이 정의되어 있다.

  • DelegatingFilterProxy: WAS 필터에서 SecurityFilterChain을 호출하기 위한 프록시 패턴의 필터

2.2. SecurityFilterChain을 빈으로 등록

SecurityFilterChain은 @Configuration 어노테이션을 붙인 설정 정보 클래스를 통해 스프링 빈으로 등록할 수 있다. 그리고 다음과 같이 SecurityFilterChain에 필터들을 등록할 수 있다.

@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtTokenFilter jwtTokenFilter; 
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable) // 보안 관련 필터 등록과 설정
            .authorizeHttpRequests(authHttp -> authHttp // 권한 관련 필터 등록과 설정
                           		.requestMatchers("/login").permitAll()
                                .anyRequest().authenticated()) 
            .addFilterBefore(jwtTokenFilter, AuthorizationFilter.class); // 커스텀 필터 등록

        return http.build();
    }
}

2.3. SecurityFilterChain에 커스텀 필터 등록


SecurityFilterChain은 여러 개 존재할 수 있다. 그리고 경로에 따라 다른 SecurityFilterChain을 거치도록 할 수 있다. 예를 들어 위의 그림처럼 /api/** 경로에 대한 SecurityFilterChain과 나머지 경로(/**) 에 대한 SecurityFilterChain을 다르게 설정하면, 각 경로에 따라 서로 다른 필터 체인을 통과하게 된다.

2.3.1. 스프링 부트의 Filter 등록 방법과 등록 위치

스프링 부트에서 Filter를 등록하는 방법은 다음과 같다.

  1. Filter 인터페이스를 구현하고, @Component를 붙여서 필터를 등록하는 방법

  2. @Configuration을 붙인 설정 정보 클래스를 만들고, 내부에 FilterRegistrationBean을 반환하는 메서드를 통해 필터를 등록하는 방법

중요한 점은 이렇게 등록한 필터는 SecurityFilterChain이 아닌 WAS의 FilterChain에 등록된다는 것이다.

예제로 살펴보자. 다음과 같이 (간접적으로) Filter 인터페이스를 구현하고 @Component 어노테이션을 붙여서 필터로 등록한 JwtFilter 클래스가 있다. 이때 JwtFilter는 SecurityFilterChain이 아닌 WAS의 FilterChain에 등록된다.

@Component
public class JwtFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {...}
}

특정 경로에 대한 요청은 SecurityFilterChain을 타지 않게 하기 위해 다음과 같이 web.ignoring().requestMatchers();를 설정했다.

@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtExceptionFilter jwtExceptionFilter;
    private final JwtFilter jwtFilter;

    /*
     * Spring Security 필터 설정
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authHttp ->
                        authHttp.requestMatchers(POST, "/join", "/login").permitAll())
                .addFilterBefore(jwtFilter, AuthorizationFilter.class) // JwtFilter는 SecurityChainFilter가 아닌, FilterChain에 등록된다. 
        ;

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> {
            web.ignoring()
                    .requestMatchers(POST, "/join", "/login"); // 해당 경로는 security filter chain을 생략
        };
    }
}

그런데 JwtFilter가 SecurityFilterChain이 아닌 FilterChain에 등록되었기 때문에 해당 경로로 접근했을 때, 여전히 JwtFilter가 적용된다.

따라서 JwtFilter를 @Component를 붙여 WAS의 FilterChain에 등록할 것이 아니라, 다음과 같이 SecurityFilterChain에 등록해야 한다. 그러면 web.ignoring().requestMatchers();이 제대로 적용되어, 해당 경로로 접근했을 때 JwtFilter가 적용되지 않는다.

public class JwtFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {...}
}
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtExceptionFilter jwtExceptionFilter;
    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authHttp ->
                        authHttp.requestMatchers(POST, "/join", "/login").permitAll())
                .addFilterBefore(new JwtFilter(jwtTokenProvider), AuthorizationFilter.class) //JwtFilter가 SecurityChainFilter에 등록된다.
        ;

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> {
            web.ignoring()
                    .requestMatchers(POST, "/join", "/login"); // 해당 경로는 security filter chain을 생략
        };
    }
}
  • WAS의 FilterChain에 등록
    addFilterBefore(jwtFilter, AuthorizationFilter.class)

  • Spring Security의 SecurityChainFilter에 등록
    addFilterBefore(new JwtFilter(jwtTokenProvider))

2.3.2. FilterChain에 등록한 필터와 SecurityFilterChain의 순서

SecurityFilterChain으로 등록한 JwtFilter에서 발생한 예외를 처리하기 위한 JwtExceptionFilter가 있다. 그리고 이 필터를 SecurityFilterChain의 앞단에 위치시켜야 SecurityFilterChain에서 발생한 예외를 처리할 수 있다.

만약 다음과 같이 JwtExceptionFilter를 @Component를 통해 FilterChain에 등록하면, JwtFilter를 포함한 SecurityFilterChain이 먼저 동작하고, 그 뒤로 FilterChain에 등록된 JwtExceptionFilter가 동작한다. 따라서 SecurityFilterChain에서 예외가 발생하면, JwtExceptionFilter가 호출되지 않는다.

@Component
public class JwtExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
        try {
            filterChain.doFilter(request, response);
        } catch (JwtException e) {
            ...
        } 
    }
}

따라서 다음과 같이 JwtExceptionFilter를 @Component를 통해 FilterChain에 등록하는 것이 아니라, .addFilterBefore()를 통해 SecurityFilterChain에 등록해야 한다. 이렇게 되면 JwtExceptionFilter와 JwtFilter이 순서대로 SecurityFilterChain에 등록된다.

public class JwtExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
        try {
            filterChain.doFilter(request, response);
        } catch (JwtException e) {
            ...
        } 
    }
}
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    /*
     * Spring Security 필터 설정
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authHttp ->
                        authHttp.requestMatchers(POST, "/join", "/login").permitAll())
                .addFilterBefore(new JwtFilter(jwtTokenProvider), AuthorizationFilter.class) //JwtFilter를 SecurityChainFilter에 등록
                .addFilterBefore(new JwtExceptionFilter(), JwtFilter.class) //JwtExceptionFilter를 SecurityChainFilter에 등록
        ;

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> {
            web.ignoring()
                    .requestMatchers(POST, "/join", "/login"); // 해당 경로는 security filter chain을 생략
        };
    }
}

Reference

https://bactoria.github.io/2021/02/12/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-%28%ED%95%B8%EB%93%A4%EB%9F%AC,-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0,-%ED%95%84%ED%84%B0%29/

profile
공부한 것들을 기록합니다.

0개의 댓글