Spring/시큐리티 기본원리

시큐리티 기본 원리( 3 ) - SecurityFilterChain( 다중 필터 경로 매핑 )

_Jin_ 2024. 6. 15.

 

 

 

 

앞서 시큐리티의 전체적인 흐름을 살펴본 내용에 의하면,

 

DelegatingFilterProxy를 이용해서 요청을 가로채고 FilterChainProxy 에 도달하면 각 SecurityFilterChain의 시큐리티 로직을 통해서 필요한 인증 인가 기능을 구현하게된다.

 

그리고 SecurityFilterChain의 부분이 실제 우리가 시큐리티에서 필요한 로직과 기능을 구현하는 부분이다. 

 

커스텀 SecurityFilterChain 등록법

먼저, 스프링 시큐리티 의존성을 추가하면 기본적인 DefaultSecurityFilterChain 하나가 등록된다.

 

이후에 우리가 SecurityFilterChain 을 등록하면 DefaultSecurityFilterChain 이 사라지고 커스텀 마이징한 SecurityFilterChain이 등록된다.

 

등록방법은 SecurityFilterChain을 리턴하는 @Bean 메소드를 등록하면 된다. (한 개 이상 등록가능)

실습을 위해서 먼저 Config 패키지에 SecurityConfig클래스를 추가해준다.

 

 

해당 클래스는 @Configuration 어노테이션을 사용해 빈으로 등록할 것임을 적용하고, @EnableWebSecurity 어노테이션은 시큐리티 관련 어노테이션의 활성화를 위해서 작성한다.

 

그리고 빈으로 등록하는 메소드의 타입은 SecurityFilterChain 타입으로 잡아준다. 그러면 SecurityFilterChain 메소드의 갯수는 곧 SecurityFilterChain의 갯수가 된다.

 

 

커스텀 SecurityFilterChain 한 개 등록 형태

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{
        
        return http.build();
    }
}

 

커스텀 SecurityFilterChain 두 개 등록 형태 ( 메소드 명칭이 다르다. )

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

        return http.build();
    }

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

        return http.build();
    }
}

각 필터 체인마다 원하는 동작을 다르게 할 수 있기 때문에 다수의 SecurityFilterChain 를 등록한다.

 

등록된 SecurityFilterChain 확인 방법

이렇게 등록된 SecurityFilterChain 을 확인하기 위해서는 filterChainProxy를 통해 가능하다.

 

FilterChainProxy : break 포인트 후 debug 모드 실행

 

디버그 로그를 통한 SecurityFilterChain 사이즈 확인

2개가 등록되어 있다.

 

이처럼 2개 이상의 SecurityFilterChain 을 등록하였지만, 모든 SecurityFilterChain를 사용하지는 않는다.

FilterChainProxy는 N개의 SecurityFilterChain 중 하나를 택하여 요청을 전달하기 때문이다.

FilterChainProxy가 SecurityFilterChain을 선택하는 기준은 아래와 같다.

등록 인덱스 순서
필터 체인에 대한 RequestMatcher 값이 일치하는지 확인

 

각 필터에 맵핑되는 플로우

 

만약 admin이라는 경로로 요청이 들어왔다. 우선 0번째 인덱스의 필터체인이 admin 경로에 매핑이 되지 않았다면, 해당 필터로 요청을 보내지 않는다.

 

그러면 1번 인덱스로 이동한 뒤에 admin 경로가 매핑되어 있다면, 해당 필터로 요청을 보낸다.

만약 두 개의 필터 모든 admin 경로에 매핑되었다면, 인덱스 순서가 더 빠른 필터로 요청을 보낸다.

 

다수의 FilterChain과 경로 설정

이처럼, SecurityFilterChain이 2개 이상인 경우에는 경로 설정이 필수이다. 

만약 경로를 설정하지 않는다면,  N개의 SecurityFilterChain이 모두 “/**” 경로에서 매핑되기 때문이다.

그럼, 당연하게도 가장 먼저 등록된 Filter만 요청이 통과된다. 

아래의 코드를 통해 이해해보자.

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

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/user").permitAll());

    return http.build();
}

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

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/admin").permitAll());

    return http.build();
}

위처럼 2개의 SecurityFilterChain을 등록하고 “/user”, “/admin” 경로에 대해서 permitAll() 설정을 진행한다.

 

이후에 “/admin” 경로로 요청을 보내면 별다른 인증 과정 없이 “/admin” 컨트롤러의 데이터를 획득할 수 있다고 생각하지만,

authorizeHttpRequests()는 클라이언트의 요청에 따른 필터 맵핑의 기능을 제공하지 않는다. 

 

authorizeHttpRequests()는 클라이언트의 모든 요청에 따른 경로를 처리하는데, 위와 같은 경우 filterChain1이 먼저 등록되었기에

/admin으로 요청을 보낸 경우에도 filterChain1의 필터를 타게된다. 

 

그리고 내부에는 /admin 경로에 대한 인가 설정이 없기에 요청이 거부된다.

( 요청에 하나의 SecurityFilterChain 로직만 사용하게 되는데, 이미 인덱스가 더 빠른 filterChain1 필터를 거치기 때문에 뒤에 나오는 “/admin” 경로는 사용할 수 없다. )

 

 

따라서 특정 경로를 사용하기 위한 조건에 따라 요청을 처리해줘야 하는데, 이를 위한 방법이 securityMatchers 이다 .

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

    http
            .securityMatchers((auth) -> auth.requestMatchers("/user"));

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/user").permitAll());

    return http.build();
}

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

    http
            .securityMatchers((auth) -> auth.requestMatchers("/admin"));

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/admin").authenticated());

    return http.build();
}

securityMatcher는 특정 SecurityFilterChain이 어떤 경로에 대해 활성화될지를 정의해주는 기능을 담당한다. 

그리고 경로에 따라 맵핑된(활성화) 필터 내의 authorizeHttpRequests와 requestMatchers는 각 인증 인가 규칙을 설정할 수 있는 것이다. 

 

이 두 설정을 사용해서 각 SecurityFilterChain 이 명확한 경로에 대해 반응하도록 만들고,

각 경로에 대해 접근 제어 규칙을 만들 수 있다. 

 

멀티 SecurityFilterChain 순서 설정 방법(선택 사항)

다수의 SecurityFilterChain을 등록 한 뒤 등록되는 순서를 직접 선정하고 싶은 경우 @Order() 어노테이션에 값을 명시할 수 있다.

@Bean
@Order(1)
public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{

    return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception {

    return http.build();
}

 

 

특정 요청은 필터를 거치지 않도록 하는 설정 방법

SecurityFilterChain을 거치게 된다면 내부적으로 여러 가지 필터를 거치게 된다.

이때 서버의 자원을 사용하고 상주 시간이 발생하기 때문에 원하는 값은 필터를 통과하지 못하도록 설정할 수 있다.

 

보통 정적 자원 (이미지, CSS)의 경우 필터를 통과하지 않도록 아래 구문을 통해 설정할 수 있다.

 

설정 시 해당 경로는 SecurityFilterChain으로 등록되며 0번 순서로 설정되며 해당 필터 체인 내부에는 필터가 없는 상태로 생성된다.

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {

    return web -> web.ignoring().requestMatchers("/img/**");
}

 

댓글