Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 중

2025. 5. 17. 18:40·Web-Spring

개요

이전 포스팅에서 Springboot의 초기화 시작점에서 SpringSecurity의 세팅이 어떻게 이뤄지는지 보았다면,

이번 포스팅에서는 SpringSecurity내에 기본 필터들이 어떻게 초기화과정을 거치는지 알 수 있어요.

 

흔히 `securityFilterChain` Bean을 설정하는 코드에서

`addFilterBefore()` 이나 `addFilterAfter()` 등을 사용하여 커스텀 시큐리티 필터를 등록하곤 해요

이 메소드들의 동작도 함께 알아봅시다.

 

사전지식

이전 포스팅의 사전지식과 동일하며, 해당 포스팅의 글을 읽고 오면 더욱 좋습니다.

이전 포스팅을 읽은 적이 없다면, SecurityArchitecture을 이해하고 오면 좋습니다.

 

저번 포스팅에서 넘어오자면...

저번 포스팅의 마지막에서 `WebSecurity`를 `build()`하여서 `springSecurityFitlerChain` Bean을 등록해요.

이 과정에서 `securityFilterChain` Bean에 대해 의존성을 갖는데, 흔히 SpringSecurity 6 버전 이상을 사용하는 사람들이 주로 등록하는 Bean입니다.

 

그리고 `this.webSecurity.build()` 는 결국 내부적으로 `performBuild()` 를 호출하게 되고,

이 과정에서 아키텍처 그림상에서 보았던 `FilterChainProxy` 도 볼 수 있었어요.

 

`springSecurityFilterChain` 을 Bean으로 등록하는 과정에서, setter 주입을 통해 사용자가 커스텀한 `securityFilterChain` Bean을 주입받은 걸 사용합니다.

// WebSecurityConfiguration.class 의 일부입니다.
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
    boolean hasFilterChain = !this.securityFilterChains.isEmpty();
    if (!hasFilterChain) {
        this.webSecurity.addSecurityFilterChainBuilder(() -> {
            this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
            this.httpSecurity.formLogin(Customizer.withDefaults());
            this.httpSecurity.httpBasic(Customizer.withDefaults());
            return this.httpSecurity.build();
        });
    }
    for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
        this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
    }
    for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
        customizer.customize(this.webSecurity);
    }
    return this.webSecurity.build();
}

 

해석해 보자면, 준비된 `securityFilterChain` Bean이 하나도 없다면, `HttpSecurity`를 세팅하여 나중에 `build()`를 필요할 때 호출하도록 람다표현식으로 넣습니다.

있다면 `securityFiltreChain` Bean들을 순회하면서 `securityFilterChainBuilder`를 만들어요.

// WebSecurity.class 의 일부입니다.
public WebSecurity addSecurityFilterChainBuilder(
        SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
    this.securityFilterChainBuilders.add(securityFilterChainBuilder);
    return this;
}

 

이게 가능한 이유는 `securityFilterChain` Bean 은 `HttpSecurity`로부터 만들어지며, `SecurityBuilder`의 구현체이기 때문이에요.

참고로 람다표현식으로 넣는 것이 가능한 이유는, `SecurityBuilder`가 인터페이스이자 하나의 추상 메소드를 갖고있기 때문입니다.

그래서 함수형 인터페이스가 가능하기에 람다표현식을 인자로 넣는것이 가능한 것이죠.

 

최종적으로 `this.webSecurity.build()`가 수행될 때 호출되는 `performBuild()`를 보면

`securityhFilterChainBuilder.build()`를 수행시키는 모습이 있습니다. 

이는 `addSecurityFilterChainBuilder()` 메서드로 등록된 람다 표현식을 실행하게 됩니다.

// WebSecurity.class 에서 FilterChainProxy를 만드는 performBuild() 메소드의 일부입니다.
DefaultSecurityFilterChain anyRequestFilterChain = null;
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
    SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
    if (anyRequestFilterChain != null) {
        String message = "A filter chain that matches any request [" + anyRequestFilterChain
                + "] has already been configured, which means that this filter chain [" + securityFilterChain
                + "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last.";
        throw new IllegalArgumentException(message);
    }
    if (securityFilterChain instanceof DefaultSecurityFilterChain defaultSecurityFilterChain) {
        if (defaultSecurityFilterChain.getRequestMatcher() instanceof AnyRequestMatcher) {
            anyRequestFilterChain = defaultSecurityFilterChain;
        }
    }
    securityFilterChains.add(securityFilterChain);
    requestMatcherPrivilegeEvaluatorsEntries
        .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
}

 

저 `build()` 부터는 `WebSecurity` 영역이 아니라 `HttpSecurity` 영역이 되겠네요.

 

그런데, `HttpSecurity`가 어느 시점에 Bean으로 등록되기에 `WebSecurityConfiguration`이 `@Autowired` 할 수 있었는지 궁금하기도 하고

`WebSecurity`전체 초기화 동작을 이해하기 위해선 `HttpSecurity`에서 어떤 활동이 일어나는지 알아야 하기에 

관심사를 `HttpSecurity`로 옮겨봅시다.

 

HttpSecurity

HttpSecurity를 Bean으로 등록하는 클래스는 `HttpSecurityConfiguration`이었습니다.

// HttpSecurityConfiguration.class 의 일부입니다.
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
    LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
    AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
            this.objectPostProcessor, passwordEncoder);
    authenticationBuilder.parentAuthenticationManager(authenticationManager());
    authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
    HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
    webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
    // @formatter:off
    http
        .csrf(withDefaults())
        .addFilter(webAsyncManagerIntegrationFilter)
        .exceptionHandling(withDefaults())
        .headers(withDefaults())
        .sessionManagement(withDefaults())
        .securityContext(withDefaults())
        .requestCache(withDefaults())
        .anonymous(withDefaults())
        .servletApi(withDefaults())
        .apply(new DefaultLoginPageConfigurer<>());
    http.logout(withDefaults());
    // @formatter:on
    applyCorsIfAvailable(http);
    applyDefaultConfigurers(http);
    return http;
}

 

저번 포스팅에서 흔히 작성하는 `securityFilterChain` Bean 등록과정과 매우 흡사하죠?

결국 사용자가 `securityFilterChain` Bean을 등록하는 것은, 이미 SpringSecurity가 기본 세팅해 둔 `HttpSecurity` Bean을 수정하여 사용하는 것입니다.

 

한 가지 차이가 있다면, 여기서 `bulid()` 를 호출하지 않고 있어요

왜냐하면 `build()`는 결국 타고 타고 수행되는 건 `doBuild()`인데

// 아래의 메소드는 HttpSecurity의 부모 클래스인 AbstractConfiguredSecurityBuilder.class 의 일부 내용입니다.
@Override
protected final O doBuild() throws Exception {
    synchronized (this.configurers) {
        this.buildState = BuildState.INITIALIZING;
        beforeInit();
        init();
        this.buildState = BuildState.CONFIGURING;
        beforeConfigure();
        configure();
        this.buildState = BuildState.BUILDING;
        O result = performBuild();
        this.buildState = BuildState.BUILT;
        return result;
    }
}

 

뭔가 하는 일이 많아요.

전체를 살펴보진 않고 우선 `performBuild()`로 넘어가 봅시다.

// HttpSecurity.class 의 일부입니다.
@Override
protected DefaultSecurityFilterChain performBuild() {
    ExpressionUrlAuthorizationConfigurer<?> expressionConfigurer = getConfigurer(
            ExpressionUrlAuthorizationConfigurer.class);
    AuthorizeHttpRequestsConfigurer<?> httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class);
    boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null;
    Assert.state((expressionConfigurer == null && httpConfigurer == null) || oneConfigurerPresent,
            "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one.");
    this.filters.sort(OrderComparator.INSTANCE);
    List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
    for (Filter filter : this.filters) {
        sortedFilters.add(((OrderedFilter) filter).filter);
    }
    return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}

 

이 메서드에서 알 수 있는 점은

`HttpSecurity`는 일종의 `Builder` 역할을 수행하는 구현체이며,

이 구현체를 통해 `DefaultSecurityFilterChain` 을 만들어내는 역할을 수행하고

`WebSecurity`에서 `build`하는 과정에서 `securityFilterChain`을 등록하여 `FilterChainProxy` 를 만드는 과정에서 사용돼요.

 

추가적으로, `WebSecurity`에서 `securityFilterChain` Bean이 없을 때 `HttpSecurity`의 일부 요소를 세팅하고

람다표현식을 사용하여 `build()`의 결과인 ` DefaultSecurityFilterChain` 을 받는데,

`securityFilterChain` Bean이 있으면 별도로 `build()` 를 호출하지 않아요.

그 이유는 사용자가 커스텀한 `securityFilterChain` Bean에서는 이미 `build()`를 호출하여 Bean으로 등록하기 때문인 거죠.

 

여기까지의 중간정리를 하자면

`WebSecurityConfiguration` 에 의하여 `FilterChainProxy`를 만드는데, 이때 필요한 것이 `securityFilterChain` Bean이며

해당 Bean 은 `HttpSecurity`를 통해 만들어지고, 최종적인 인스턴스는 기본적인 `DefaultSecurityFilterChain` 을 사용합니다.

 

`securityFilterChain` Bean 이 준비된 것이 없다면 `HttpSecurity` Bean으로 기본 세팅만 구성하여 만들게 됩니다.

 

이제 `SecurityFilterChain`이 갖고 있는 기본 필터들이 어떤 게 있고 어떻게 초기화되는지 살펴봅시다.

더보기

그럼 WebSecurity 객체는 어떻게 WebSecurityConfiguration에서 사용할 수 있나요?

 

바로 `setFilterChainProxySecurityConfigurer` 메소드가 setter 주입되도록 `@Autowired`가 되어있는데
그 내부에서 webSecurity 객체를 초기화합니다.

@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor,
        ConfigurableListableBeanFactory beanFactory) throws Exception {
    this.webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
    if (this.debugEnabled != null) {
        this.webSecurity.debug(this.debugEnabled);
    }
    List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new AutowiredWebSecurityConfigurersIgnoreParents(
            beanFactory)
        .getWebSecurityConfigurers();
    webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
    Integer previousOrder = null;
    Object previousConfig = null;
    for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
        Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
        if (previousOrder != null && previousOrder.equals(order)) {
            throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of " + order
                    + " was already used on " + previousConfig + ", so it cannot be used on " + config + " too.");
        }
        previousOrder = order;
        previousConfig = config;
    }
    for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
        this.webSecurity.apply(webSecurityConfigurer);
    }
    this.webSecurityConfigurers = webSecurityConfigurers;
}

 

기본 필터들은 어떻게 초기화되는가?

우선 어떤 필터들이 준비되어 있는지 살펴봅시다.

디버그 모드를 켜면 다음과 같이 어떤 필터들이 활성화되는지 볼 수 있어요.

디버그 모드는 `@EnableWebSecurity` 어노테이션의 `boolean debug` 속성으로 설정할 수 있어요.

2025-05-17T16:47:28.094+09:00 DEBUG 200 --- [app] [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with filters: 
DisableEncodeUrlFilter, 
WebAsyncManagerIntegrationFilter, 
SecurityContextHolderFilter, 
HeaderWriterFilter, 
CorsFilter, 
LogoutFilter, 
OAuth2AuthorizationRequestRedirectFilter, 
OAuth2LoginAuthenticationFilter, 
JwtAuthFilter, 
RequestCacheAwareFilter, 
SecurityContextHolderAwareRequestFilter, 
AnonymousAuthenticationFilter, 
SessionManagementFilter, 
ExceptionTranslationFilter, 
AuthorizationFilter

 

더보기

만약 위의 로그가 찍히지 않는다면

 

application.properties를 기준으로

logging.level.org.springframework.security=

 

에 대하여 로깅 레벨을 조정해 주시면 나올 거예요

수많은 필터들 중에서 `JwtAuthFilter`는 제가 `addFtilerBefore()`를 통해 넣은 커스텀 필터에요

우선 다 보진 못하고, `CorsFilter`를 하나의 예시로 살펴볼게요

 

사용자가 커스텀하는 `securityFilterChain`에서 `cors()`가 있어요

// HttpSecurity.class의 일부입니다.
public HttpSecurity cors(Customizer<CorsConfigurer<HttpSecurity>> corsCustomizer) throws Exception {
    corsCustomizer.customize(getOrApply(new CorsConfigurer<>()));
    return HttpSecurity.this;
}

 

// HttpSecurity.class 의 일부입니다.
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer)
        throws Exception {
    C existingConfig = (C) getConfigurer(configurer.getClass());
    if (existingConfig != null) {
        return existingConfig;
    }
    return apply(configurer); 
    // 여기서 AbstractConfiguredSecurityBuilder의 필드로 등록됩니다.
 	// 즉, SecurityConfigurer의 묶음에 포함되는 것
}

 

// CorsConfigurer.class 의 일부입니다.

@Override
public void configure(H http) {
    ApplicationContext context = http.getSharedObject(ApplicationContext.class);
    CorsFilter corsFilter = getCorsFilter(context);
    Assert.state(corsFilter != null, () -> "Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
            + CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
    http.addFilter(corsFilter);
}

private CorsFilter getCorsFilter(ApplicationContext context) {
    if (this.configurationSource != null) {
        return new CorsFilter(this.configurationSource);
    }
    boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
    if (containsCorsFilter) {
        return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
    }
    boolean containsCorsSource = context.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
    if (containsCorsSource) {
        CorsConfigurationSource configurationSource = context.getBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME,
                CorsConfigurationSource.class);
        return new CorsFilter(configurationSource);
    }
    if (mvcPresent) {
        return MvcCorsFilter.getMvcCorsFilter(context);
    }
    return null;
}

static class MvcCorsFilter {

    private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";

    /**
     * This needs to be isolated into a separate class as Spring MVC is an optional
     * dependency and will potentially cause ClassLoading issues
     * @param context
     * @return
     */
    private static CorsFilter getMvcCorsFilter(ApplicationContext context) {
        if (!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
            throw new NoSuchBeanDefinitionException(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, "A Bean named "
                    + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + " of type "
                    + HandlerMappingIntrospector.class.getName()
                    + " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.");
        }
        HandlerMappingIntrospector mappingIntrospector = context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME,
                HandlerMappingIntrospector.class);
        return new CorsFilter(mappingIntrospector);
    }

}

 

`configure()`와 `getCorsFilter()` 메소드의 동작은 간단하게 살펴보면

중복된 `CorsFilter`에 대한 Bean이 존재하는지 검증하고 (`Assert.state()`)

없다면 마지막으로 Spring MVC 기반인지 검증하여 `CorsFilter` 객체를 완성시키고

`http.addFilter()`를 통해 시큐리티 필터로 추가됩니다.

 

근데 이것도 결국 `CorsConfigurer`를 사용하는 클라이언트 클래스 혹은 메소드에서 `configure()`을 호출해야 합니다.

그럼 `CorsConfigurer`를 포함한 여러 `SecurityConfigurer` 타입의 객체들을 등록해야 하고 한 번에 호출하겠죠

 

그래서 `http.cors()` 호출 흐름을 보면 제일 먼저 `CorsConfigurer`객체를 `getOrApply()`를 통해 등록합니다.

 

호출은 `HttpSecurity`가 `build()` 메소드를 통해 초기화 되는 과정에서

일괄적으로 `configure()`을 호출하게 됩니다.

// AbstractConfiguredSecurityBuilder.class 의 일부입니다.
@Override
protected final O doBuild() throws Exception {
    synchronized (this.configurers) {
        this.buildState = BuildState.INITIALIZING;
        beforeInit();
        init();
        this.buildState = BuildState.CONFIGURING;
        beforeConfigure();
        configure();
        this.buildState = BuildState.BUILDING;
        O result = performBuild();
        this.buildState = BuildState.BUILT;
        return result;
    }
}

private void configure() throws Exception {
    Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
    for (SecurityConfigurer<O, B> configurer : configurers) {
        configurer.configure((B) this);
    }
}

 

그런데 이렇게 등록된 필터들의 순서를 어떻게 정렬할까요?

`addFilterBefore()` 와 같은 메소드를 생각해 보면 `Security` 의 기본필터들은 어떤 순서가 정해진 것을 추측할 수 있어요.

`CorsConfigurer`에서 `configure()` 메소드에 `addFilter()`를 봅시다.

// HttpSecurity.class 의 일부입니다.
@Override
public HttpSecurity addFilter(Filter filter) {
    Integer order = this.filterOrders.getOrder(filter.getClass());
    if (order == null) {
        throw new IllegalArgumentException("The Filter class " + filter.getClass().getName()
                + " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.");
    }
    this.filters.add(new OrderedFilter(filter, order));
    return this;
}

private static final class OrderedFilter implements Ordered, Filter {

    private final Filter filter;

    private final int order;

    private OrderedFilter(Filter filter, int order) {
        this.filter = filter;
        this.order = order;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        this.filter.doFilter(servletRequest, servletResponse, filterChain);
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    @Override
    public String toString() {
        return "OrderedFilter{" + "filter=" + this.filter + ", order=" + this.order + '}';
    }

}

 

`filter.getClass()`를 통해 해당 필터의 순서를 받아오는 것, 즉 이미 필터의 순서가 정해져 있단 추측이 맞았어요.

`filterOrders` 를 보면 기본 필터들의 종류와 그들의 순서번호가 어떻게 매겨지는지 알 수 있어요.

final class FilterOrderRegistration {

	private static final int INITIAL_ORDER = 100;

	private static final int ORDER_STEP = 100;

	private final Map<String, Integer> filterToOrder = new HashMap<>();

	FilterOrderRegistration() {
		Step order = new Step(INITIAL_ORDER, ORDER_STEP);
		// 수많은 필터들이 등록되요
		put(CorsFilter.class, order.next());
        put(UsernamePasswordAuthenticationFilter.class, order.next());
		// 정말 엄청 많습니다.
	}

	Integer getOrder(Class<?> clazz) {
		while (clazz != null) {
			Integer result = this.filterToOrder.get(clazz.getName());
			if (result != null) {
				return result;
			}
			clazz = clazz.getSuperclass();
		}
		return null;
	}

	private static class Step {

		private int value;

		private final int stepSize;

		Step(int initialValue, int stepSize) {
			this.value = initialValue;
			this.stepSize = stepSize;
		}

		int next() {
			int value = this.value;
			this.value += this.stepSize;
			return value;
		}

	}

}

 

결국 임의의 순서번호 100번부터 시작하여 100번씩 건너뛰는 식으로 간격을 만듭니다.

 

아까 `JwtAuthFilter`는 제가 만든 커스텀 필터입니다.

`securityFilterChain` Bean을 등록할 때 `UsernamePasswordAuthenticationFilter` 보다 앞에 오도록 `addFilterBefore()`를 호출하였거든요

그러면 추측할 수 있는 `addFilterBefore()`의 동작은 대상 `Fitler`의 순서번호에서 일부 값을 뺀 순서번호를 `JwtAuthFilter`에 부여할 것입니다.

// HttpSecurity.class
@Override
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) {
    return addFilterAtOffsetOf(filter, -1, beforeFilter);
}

 

1 정도 차감하고 있네요.

 

그런데 왜 `OrderedFilter`라는 내부 `private` 클래스를 사용하여 덧씌울까요?

`Filter` 사이에 순서번호를 저장은 하고 있지만, 실제로 필터들을 순서번호데로 호출되어야 할 겁니다.

즉, 정렬이 필요한 것이죠

 

그래서 다시금 `performBuild()` 를 가보면 

`this.filters.sort()`를 하고 정렬된 결과를 다시 `List`로 만들어서 관리하게 됩니다.

 

이제 이전 포스팅의 `FilterChainProxy`와 지금까지 과정들을 이어봅시다.

 

이전 포스팅과 이어서 이해해 보기 

`DelegatingFilterProxyBean` 을 통해 `DelegatingFilterProxy` 가 서블릿 컨텍스트 내부 필터로 등록돼요

`DelegatingFilterProxy`는 내부에 `"springSecurityFilterChain"` 이란 Bean 이름을 기억해두고 있다가,

`WebApplicationContext`에서 Bean을 꺼내와서 사용해요

 

이때 `"springSecurityFilterChain"`의 정체는 `FilterChainProxy` 로서 `WebSecurityConfiguration`에 의하여 `WebSecurity`객체가 `build()` 를 호출하면 반환되는 객체입니다.

 

즉, `WebSecurity`는 다양한 `securityFilterChain` Bean들을 소유하고 있어요.

`WebSecurity`의 커스텀은 결국 다양한 `securityFilterChain`들의 전역설정이 될 거에요.

 

사용자가 `securityFilterChain`를 커스텀하기 앞서 `HttpSecurity`를 사용하게 되는데,

이 `HttpSecurity` Bean 은 `HttpSecurityConfiguration`을 통해 기본 세팅된 채로 주입해요.

 

커스텀할 때 `cors()`와 같은 메소드를 호출하면 `SecurityConfigurer` 를 구현한 `~Configurer` 클래스의 인스턴스를 저장해요

 

기본적인 시큐리티 필터들은 순서번호가 이미 정해져 있었어요.

그리고 `addFilterBefore()`와 같은 메소드를 통해 특정 시큐리티 기본필터들 사이에 커스텀필터를 넣을 수 있어요.

 

커스텀한 `HttpSecurity`의 `build()` 를 호출하게 되면 일괄적으로 `~Configurer` 클래스들의  `configure()` 를 호출하여 필요한 `CorsFilter`와 같은 필터를 등록해요.

그리고 필터들이 순서대로 동작하도록 정렬해요.

 

어떻게 List<Filter>에 순서대로 doFilter()를 호출할 것인가?

근데 의문점이 아직 남았어요.

Security의 필터들을 다 돌고 나면 원래 서블릿 컨텍스트의 필터로 돌아가야 해요.

또한 Security의 필터들도 `doFilter()` 가 호출되면 다음 필터가 호출되기를 기대해요.

그런데 `HttpSecurity` 가 갖고 있는 `List <Filter>`속 필터들은 `List <Filter>`를 알아야 다음 필터가 누구인지 알 수 있죠.

// CorsFilter.class 일부입니다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

    // 필터 내용
    filterChain.doFilter(request, response);
}

 

여기서 `filterChain`이라는 파라미터의 `doFilter`를 호출하는 것을 알 수 있어요.

 

`FilterChain`이 위의 요구사항을 만족하고 있을 거예요.

 

우선 `FilterChain` 을 알아가기 위해 `FilterChainProxy`가 어떻게 `HttpSecurity`의 `this.filters`를 들고 오는지 봅시다.

// FilterChainProxy.class 의 일부입니다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (!clearContext) {
        doFilterInternal(request, response, chain);
        return;
    }
    try {
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        doFilterInternal(request, response, chain);
    }
    catch (Exception ex) {
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
        Throwable requestRejectedException = this.throwableAnalyzer
            .getFirstThrowableOfType(RequestRejectedException.class, causeChain);
        if (!(requestRejectedException instanceof RequestRejectedException)) {
            throw ex;
        }
        this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response,
                (RequestRejectedException) requestRejectedException);
    }
    finally {
        this.securityContextHolderStrategy.clearContext();
        request.removeAttribute(FILTER_APPLIED);
    }
}

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
    List<Filter> filters = getFilters(firewallRequest);
    // 내용들...
    this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}

private List<Filter> getFilters(HttpServletRequest request) {
    int count = 0;
    for (SecurityFilterChain chain : this.filterChains) {
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
                    this.filterChains.size()));
        }
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }
    return null;
}

 

여기서 `FilterChainProxy`는 결국 여러 `securityFilterChain` 들을 갖고 있어요.

그중에서 현재 요청에 따라 어떤 시큐리티 필터체인과 매칭되는지 찾아 `List <Filter>`를 전달해요.

// FilterChainProxy.class 의 일부입니다.
public interface FilterChainDecorator {

    FilterChain decorate(FilterChain original, List<Filter> filters);

}

public static final class VirtualFilterChainDecorator implements FilterChainDecorator {

    @Override
    public FilterChain decorate(FilterChain original) {
        return original;
    }

    @Override
    public FilterChain decorate(FilterChain original, List<Filter> filters) {
        return new VirtualFilterChain(original, filters);
    }

}

private static final class VirtualFilterChain implements FilterChain {

    private final FilterChain originalChain;

    private final List<Filter> additionalFilters;

    private final int size;

    private int currentPosition = 0;

    private VirtualFilterChain(FilterChain chain, List<Filter> additionalFilters) {
        this.originalChain = chain;
        this.additionalFilters = additionalFilters;
        this.size = additionalFilters.size();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (this.currentPosition == this.size) {
            this.originalChain.doFilter(request, response);
            return;
        }
        this.currentPosition++;
        Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
        if (logger.isTraceEnabled()) {
            String name = nextFilter.getClass().getSimpleName();
            logger.trace(LogMessage.format("Invoking %s (%d/%d)", name, this.currentPosition, this.size));
        }
        nextFilter.doFilter(request, response, this);
    }

}

 

 이 내용까지 종합해 보면

 

`FilterChainDecorator`의 구현체인 `VirtualFilterChainDecorator`를 통해 `FilterChain` 타입의 `VirtualFilterChain`을 덧씌웁니다.

덧씌우는 이유는 `getFilter()`를 통해 뽑아온 `List <Filter>`를 계속 유지할 필요가 있기 때문이에요.

 

현재 순번의 필터를 `List<Filter>`를 통해 꺼내어 `doFilter()`를 호출할 때, 자기 자신을 `FilterChain`파라미터로 넣어서 `List <Filter>`와 `currentPosition` 컨텍스트를 계속 유지하는 것이죠.

 

그리고 `originalChain`이 있는 이유는 시큐리티 필터를 전부 순회한 이후에는 서블릿 컨텍스트의 필터로 돌아가야 하기 때문이에요.

 

즉, `DelegatingFilterChainProxy` 에서부터 계속 파라미터로 넘긴 서블릿 컨텍스트 `FilterChain`에 돌아가게 됩니다.

 


 

저번 포스팅에서는 `WebSecurity`를 통해 `FilterChainProxy`까지의 전역 설정 초기화 과정을 보았다면,

이번 포스팅에서는 `FilterChainProxy`가 가질 수 있는 여러 `securityFilterChain`들이 초기화되는 공통과정을 보았어요.

 

다음 포스팅에서는 초기화가 완료되었으니, 실제 호출에서 일어나는 `SecurityContextHolder` 객체들의 활동을 살펴볼 거예요.

 

'Web-Spring' 카테고리의 다른 글

Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 상  (0) 2025.05.03
SpringSecurity에서 OAuth2 로그인 시 쿼리 파라미터 유지하기  (0) 2025.04.28
쿠키와 SameSite, Secure, HttpOnly  (0) 2025.04.17
@JsonTypeInfo 를 통해 유연하게 Json과 Java객체 매핑하기  (0) 2025.04.14
JUnit  (2) 2023.12.12
'Web-Spring' 카테고리의 다른 글
  • Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 상
  • SpringSecurity에서 OAuth2 로그인 시 쿼리 파라미터 유지하기
  • 쿠키와 SameSite, Secure, HttpOnly
  • @JsonTypeInfo 를 통해 유연하게 Json과 Java객체 매핑하기
devKhc
devKhc
  • devKhc
    개발저장소 by 회창
    devKhc
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • Web-Spring (7)
      • CS (5)
      • Infra (3)
      • DB (1)
      • Java (3)
      • CodeTree (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    인터럽트
    운영체제
    try with resources
    set-cookie
    코드트리조별과제
    JsonTypeInfo
    samesite=none
    모드비트
    Nginx #proxy #리버스 프록시
    다중 코어 시스템
    SpringBoot
    Springsecurity
    defaultoauth2authorizationrequestresolver
    이중모드와 다중모드
    코딩테스트
    springboot #jenkins #CI/CD #Docker #EC2
    코드트리
    인터럽트 #운영체제 #공룡책
    Multi-Processor
    JsonSubTypes
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
devKhc
Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 중
상단으로

티스토리툴바