[Spring Security] Spring Security Basic - Filter Chain Proxy

Updated:

들어가며

Spring Security는 여러개의 Filter를 사용하여, 인증 및 인가를 진행 한다.

해당 필터들은 chain형태로 연속적으로 진행되는데 그때 사용 되는 class가 FilterChainProxy이다.

FilterChainProxy가 언제 생성 및 호출 되고 무엇을 담당하고 있는지 알아 보자.

FilterChainProxy 생성 과정

WebSecurityConfiguration

@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
	...
	@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
	public Filter springSecurityFilterChain() throws Exception {
		boolean hasConfigurers = webSecurityConfigurers != null
				&& !webSecurityConfigurers.isEmpty();
		if (!hasConfigurers) {
			WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
					.postProcess(new WebSecurityConfigurerAdapter() {
					});
			webSecurity.apply(adapter);
		}
		return webSecurity.build();
	}
}
  • FilterChainProxyspringSecurityFilterChain bean이 등록될 때 생성 된다.
@Override
protected final O doBuild() throws Exception {
  synchronized (configurers) {
    buildState = BuildState.INITIALIZING;

    beforeInit();
    init();
    buildState = BuildState.CONFIGURING;

    beforeConfigure();
    configure();
    buildState = BuildState.BUILDING;

    O result = performBuild();
    buildState = BuildState.BUILT;

    return result;
  }
}
  • webSecurity.build()AbstractConfiguredSecurityBuilder.doBuild로 넘어가게 되는데, 이때 init, configure, performBuild가 진행 된다.
  • 우리가 원하는 FilterChainProxy의 생성은 performBuild시에 이뤄진다.
@Override
protected Filter performBuild() throws Exception {
  Assert.state(!securityFilterChainBuilders.isEmpty(),
				() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
						+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
						+ "More advanced users can invoke "
						+ WebSecurity.class.getSimpleName()
						+ ".addSecurityFilterChainBuilder directly");
  int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
  List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
				chainSize);
  for (RequestMatcher ignoredRequest : ignoredRequests) {
    securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
  }
  for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
    securityFilterChains.add(securityFilterChainBuilder.build());
  }
  FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
  if (httpFirewall != null) {
    filterChainProxy.setFirewall(httpFirewall);
  }
  filterChainProxy.afterPropertiesSet();

  Filter result = filterChainProxy;
  if (debugEnabled) {
			logger.warn("\n\n"
					+ "********************************************************************\n"
					+ "**********        Security debugging is enabled.       *************\n"
					+ "**********    This may include sensitive information.  *************\n"
					+ "**********      Do not use in a production system!     *************\n"
					+ "********************************************************************\n\n");
			result = new DebugFilter(filterChainProxy);
  }
  postBuildAction.run();
  return result;
}
  • FilterChainProxySecurityFilterChain도 여기서 생성이 된다.
  • debugMode를 활성화 한 경우는 DebugFilterFilterChainProxy를 한번 더 감싸서 DelegatingFilterProxy에서 호출시 DebugFilter -> FilterChainProxy 순으로 호출이 된다.

FilterChainProxy 호출 과정

  • FilterChainProxy의 호출 과정은 DelegatingFilterProxy -> FilterChainProxy로 이뤄진다.
  • DelegatingFilterProxy가 언제 생성되서 FilterChainProxy랑 연결되는지 알아 본다.

SecurityFilterAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {
	private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

	@Bean
	@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
	public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
			SecurityProperties securityProperties) {
		DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
				DEFAULT_FILTER_NAME);
		registration.setOrder(securityProperties.getFilter().getOrder());
		registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
		return registration;
	}
    ...
}
  • SecurityFilterAutoConfiguration에서 DelegatingFilterProxyRegistrationBean을 bean으로 등록하는데 그때 DEFAULT_FILTER_NAMEspringSecurityFilterChain으로 전달 한다.
  • springSecurityFilterChain은 앞에서 확인했던 WebSecurityConfiguration에서 bean으로 등록 되었고 결과 값은 FilterChainProxy이다.

DelegatingFilterProxyRegistrationBean

public class DelegatingFilterProxyRegistrationBean extends AbstractFilterRegistrationBean<DelegatingFilterProxy>
		implements ApplicationContextAware {
    ...
	@Override
	public DelegatingFilterProxy getFilter() {
		return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {

			@Override
			protected void initFilterBean() throws ServletException {
				// Don't initialize filter bean on init()
			}

		};
	}
}
  • DelegatingFilterProxyRegistrationBeangetFilter가 호출될 때 DelegatingFilterProxy를 생성하게 된다.

DelegatingFilterProxy

public class DelegatingFilterProxy extends GenericFilterBean {
    ...
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		// Lazily initialize the delegate if necessary.
		Filter delegateToUse = this.delegate;
		if (delegateToUse == null) {
			synchronized (this.delegateMonitor) {
				delegateToUse = this.delegate;
				if (delegateToUse == null) {
					WebApplicationContext wac = findWebApplicationContext();
					if (wac == null) {
						throw new IllegalStateException("No WebApplicationContext found: " +
								"no ContextLoaderListener or DispatcherServlet registered?");
					}
					delegateToUse = initDelegate(wac);
				}
				this.delegate = delegateToUse;
			}
		}

		// Let the delegate perform the actual doFilter operation.
		invokeDelegate(delegateToUse, request, response, filterChain);
	}
    ...
	protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		String targetBeanName = getTargetBeanName();
		Assert.state(targetBeanName != null, "No target bean name set");
		Filter delegate = wac.getBean(targetBeanName, Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}
}
  • DelegatingFilterProxy는 첫 요청이 들어올 때까지 delegateToUse를 초기화 하지 않고 첫 요청 시에 Lazy 초기화를 이용 한다.
  • initDelegate시에 springSecurityFilterChain를 delegate로 등록하게 되는데, delegate로 등록된 Filter가 FilterChainProxy이다.
  • debugMode를 활성화 한경우는 DebugFilterFilterChainProxy를 한번 더 감싼 형태로 되어 있다.
  • 모든 요청은 DelegatingFilterProxy -> FilterChainProxy 순으로 들어 와서 FilterChainProxy에 있는 SecurityFilterChain을 확인하여 동작하게 된다.

AbstractSecurityWebApplicationInitializer

  • 만약 SpringBoot를 사용하지 않는 다면, AbstractSecurityWebApplicationInitializer를 이용하여 DelegatingFilterProxy 를 생성할 수 있다.
public abstract class AbstractSecurityWebApplicationInitializer
		implements WebApplicationInitializer {

	public final void onStartup(ServletContext servletContext) {
		beforeSpringSecurityFilterChain(servletContext);
		if (this.configurationClasses != null) {
			AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
			rootAppContext.register(this.configurationClasses);
			servletContext.addListener(new ContextLoaderListener(rootAppContext));
		}
		if (enableHttpSessionEventPublisher()) {
			servletContext.addListener(
					"org.springframework.security.web.session.HttpSessionEventPublisher");
		}
		servletContext.setSessionTrackingModes(getSessionTrackingModes());
		insertSpringSecurityFilterChain(servletContext); // 여기
		afterSpringSecurityFilterChain(servletContext);
	}

	private void insertSpringSecurityFilterChain(ServletContext servletContext) {
		String filterName = DEFAULT_FILTER_NAME;
		DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(
				filterName);
		String contextAttribute = getWebApplicationContextAttribute();
		if (contextAttribute != null) {
			springSecurityFilterChain.setContextAttribute(contextAttribute);
		}
		registerFilter(servletContext, true, filterName, springSecurityFilterChain);
	}
}

FilterChainProxy 역할

  • WebSecurityConfigurerAdapter를 bean으로 등록하면, security RequestPath 마다 어떻게 처리할지를 결정하게 되는데 RequestPath를 확인하여 어떤 WebSecurityConfigurerAdapter를 이용하게 할지를 결정 한다.
  • 즉 Request마다 어떤 SecurityFilterChain을 이용하게 될지를 선택하는 역할!
public class FilterChainProxy extends GenericFilterBean {
	...
	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
		if (clearContext) {
			try {
				request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
				doFilterInternal(request, response, chain);
			}
			finally {
				SecurityContextHolder.clearContext();
				request.removeAttribute(FILTER_APPLIED);
			}
		}
		else {
			doFilterInternal(request, response, chain);
		}
	}

	private void doFilterInternal(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {

		FirewalledRequest fwRequest = firewall
				.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse fwResponse = firewall
				.getFirewalledResponse((HttpServletResponse) response);

		List<Filter> filters = getFilters(fwRequest);

		if (filters == null || filters.size() == 0) {
			if (logger.isDebugEnabled()) {
				logger.debug(UrlUtils.buildRequestUrl(fwRequest)
						+ (filters == null ? " has no matching filters"
								: " has an empty filter list"));
			}

			fwRequest.reset();

			chain.doFilter(fwRequest, fwResponse);

			return;
		}

		VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
		vfc.doFilter(fwRequest, fwResponse);
	}

    private List<Filter> getFilters(HttpServletRequest request) {
	  for (SecurityFilterChain chain : filterChains) {
	    if (chain.matches(request)) {
	      return chain.getFilters();
	    }
	  }
	  return null;
	}

}
  • Filter의 시작은 doFilter를 통해서 시작되며, doFilterInternal에서 getFilters를 통해서 Request에 맞는 SecurityFilterChain 에 Filters을 가져 온다.
  • chain.matches는 HttpSecurity, WebSecurity에서 설정한 mvcMatchers, antMatchers, regexMatkchers, requestMatchers에 해당 된다면 해당 HttpSecurity, WebSecurity에 설정된 Filter를 사용하게 된다.
@Override
public void configure(WebSecurity web) throws Exception {
  web.ignoring()
     .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
     .antMatchers("/info");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.authorizeRequests()
      .mvcMatchers("/", "/info", "/account/**").permitAll()
      .mvcMatchers("/admin").hasAnyRole("ADMIN")
      .mvcMatchers("/user").hasAnyRole("USER")
      .anyRequest().authenticated()
      .accessDecisionManager(accessDecisionManager());
  http.formLogin();
  http.httpBasic();
}
  • 만약 WebSecurityConfigurerAdapter의 configure를 이렇게 셋팅 한경우 SecurityFilterChain은 총 3개가 등록이 된다.
  • WebSecurity에서 requestMatchers, antMatchers로 2개, HttpSecurity에서 1개이다.

security-filter-chain

  • WebSecurityConfigurerAdapter가 여러개를 사용한다면, SecurityFilterChain 역시 많이 등록될 것이고, 순서는 @Order에 따라서 다르게 적용이 된다.

VirtualFilterChain

private static class VirtualFilterChain implements FilterChain {
    ...
	@Override
	public void doFilter(ServletRequest request, ServletResponse response)
		throws IOException, ServletException {
      if (currentPosition == size) {
        if (logger.isDebugEnabled()) {
          logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
						+ " reached end of additional filter chain; proceeding with original chain");
        }
        // Deactivate path stripping as we exit the security filter chain
			this.firewalledRequest.reset();
				originalChain.doFilter(request, response);
      }
      else {
        currentPosition++;
        Filter nextFilter = additionalFilters.get(currentPosition - 1);

        if (logger.isDebugEnabled()) {
          logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
						+ " at position " + currentPosition + " of " + size
						+ " in additional filter chain; firing Filter: '"
						+ nextFilter.getClass().getSimpleName() + "'");
        }

        nextFilter.doFilter(request, response, this);
      }
    }
}

  • FilterChainProxy에서 Filters를 가져오고 실제 동작은 VirtualFilterChain에서 진행 된다.
  • VirtualFilterChain에서 doFilter시에 this를 넘겨주고 있어 실행되는 Filter에서 또 VirtualFilterChain의 doFilter를 호출하여 연속적으로 호출이 되는 방식으로 설정되어 있는 Filters를 전체 실행하도록 되어 있다.
  • VirtualFilterChain.doFilter -> nextFilter.doFilter -> VirtualFilterChain.doFilter -> nextFilter.doFilter -> …

마치며

  • FilterChainProxy는 Request마다 원하는 Filter Set을 URI를 보고 셋팅해주고, 실제로는 WebSecurityConfigurerAdapter configure에서 Filter Set을 등록하게 된다.
  • Security에서 Filter가 자동으로 만들어지는게 생각보다 많아서 나중에 각 Filter마다 역할을 알아보면 더 좋을 것 같다!

Leave a comment