SecurityBuilder와 SecurityConfigurer

mainimg.jpg

본 포스팅 시리즈에서 Spring Security는 Spring Boot 2.7.x 버전을 기준으로한 강의를 최신 스프링 공식 문서를 참고하여 수정하였습니다. Deprecated된 여러 메서드들을 최대한 최신에 맞게 바꾸었지만 완벽하지 않을 수 있습니다.

Spring Security OAuth 시리즈
  1. 포인터를 잘 알아야 하는 이유

개념 및 구조 이해

a1.png

  • SecurityBuilder는 빌더 클래스로서 웹 보안을 구성하는 빈 객체와 설정 클래스들을 생성하는 역할을 하며 WebSecurity, HttpSecurity가 있다.

a2.png

  • SecurityConfigurer는 Http요청과 관련된 보안처리를 담당하는 필터들을 생성하고 여러 초기화 설정에 관여한다.

a3.png

  • SecurityBuilderSecurityConfigurer를 포함하고 있으며 인증 및 인가 초기화 작업은 SecurityConfigurer에 의해 진행된다.

시퀀스 다이어그램 a4.png

SecurityBuilder에서 build()를 호출하면, SecurityConfigurerinit()configure()가 호출되어 초기화 작업이 진행된다.


a5.png

  • WebSecuritybuild()가 반환하는 최종 클래스는 build()내부의 performBuild()가 반환한 FilterChainProxy이다.
  • HttpSecuritybuild()가 반환하는 최종 클래스는 build()내부의 performBuild()가 반환한 SecurityFilterChain이다.
  • FilterChainProxy는 내부에 SecurityFilterChain을 가지고 있다. SecurityFilterChain내부의 여러가지 필터들을 사용자 요청을 처리할때 실행시킨다.

코드 분석

SecurityFilterChain

SpringBootWebSecurityConfiguration클래스를 살펴보면, 내부 클래스로 SecurityFilterChainConfiguration을 가지고 있다. 그 안에는 SecurityFilterChain이 등록되어 있다.

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {

	@Bean
	@Order(SecurityProperties.BASIC_AUTH_ORDER)
	SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
		http.formLogin(withDefaults());
		http.httpBasic(withDefaults());
		return http.build();
	}
	//...
}
  • @Configuration이므로 스프링 서버 시작과 함께 빈으로 등록된다.
    • 이때 @Configuration(proxyBeanMethods = false)는 해당 클래스의 빈에 프록시 패턴을 적용하지 않고, CGLib을 사용하지 않을 것임을 명시한다. 따라서 매번 다른 빈 인스턴스를 생성한다.
  • @Bean으로 등록된 SecurityFilterChain은 파라미터로 HttpSecurity를 갖는다.
  • 해당 빈은 build()의 반환값이다. => build()를 통해 SecurityFilterChain이 반환된다.
  • build()내부에는 doBuild()메서드가 있으며, 코드는 다음과 같다.
    • @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;
          }
      }
    • init(), configure() 그리고 performBuild()가 있음을 확인할 수 있다.

인자인 HttpSecurity는 어디서 받는가?

HttpSecurity

HttpSecurityConfiguration클래스에 HttpSecurity빈이 등록되어 있다.

HttpSecurity

@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
    // !!해당 HttpSecurity에 SecurityConfigurer의 구현체들을 등록하고 있다.
    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;
}
  • 해당 HttpSecuritySecurityConfigurer구현체를 등록하고 있다.

WebSecurityConfiguration

WebSecurityConfiguration에는 다음과 같이 SecurityFilterChains을 Setter주입하는 부분이 있다.

@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
    this.securityFilterChains = securityFilterChains;
}

그럼 주입된 SecurityFilterChains는 어디서 사용될까?

@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();
}
  • 해당 부분에서 for문을 통해 SecurityFilterChains에서 SecurityFilterChain을 추출한다. (여기서는 Default 체인필터 한 개)
    • 추출된 각 SecurityFilterChain마다 빌더를 붙여서 등록하는데, 해당 부분은 다음 WebSecurity에 있다.
    • public WebSecurity addSecurityFilterChainBuilder(
              SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
          this.securityFilterChainBuilders.add(securityFilterChainBuilder);
          return this;
      }
  • 마지막 반환값인 this.webSecurity.build()SecurityFilterChainbuild()와 똑같이 동작한다. (doBuilder, init, configure, …)
    • 그러나 performBuild()의 경우 구현체에 따라 다르게 동작하는데, 실제로 구현체를 보면 다음과 같이 나온다.
    • a6.png
    • AuthenticationManagerBuilderProviderManager를 반환한다.
    • HttpSecurityDefaultSecurityFilterChain을 반환한다(앞서 본 것과 같이)
    • WebSecurityFilter를 반환한다.

WebSecurity

WebSecurity에서 Filter를 반환하는 performBuild()는 다음과 같다.

@Override
protected Filter performBuild() throws Exception {
    Assert.state(!this.securityFilterChainBuilders.isEmpty(),
            () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
                    + "Typically this is done by exposing a SecurityFilterChain bean. "
                    + "More advanced users can invoke " + WebSecurity.class.getSimpleName()
                    + ".addSecurityFilterChainBuilder directly");
    int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
    List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
    List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
    for (RequestMatcher ignoredRequest : this.ignoredRequests) {
        WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
                + ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
        SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
        securityFilterChains.add(securityFilterChain);
        requestMatcherPrivilegeEvaluatorsEntries
            .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
    }
    for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
        SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
        securityFilterChains.add(securityFilterChain);
        requestMatcherPrivilegeEvaluatorsEntries
            .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
    }
    if (this.privilegeEvaluator == null) {
        this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
                requestMatcherPrivilegeEvaluatorsEntries);
    }
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
    if (this.httpFirewall != null) {
        filterChainProxy.setFirewall(this.httpFirewall);
    }
    if (this.requestRejectedHandler != null) {
        filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
    }
    else if (!this.observationRegistry.isNoop()) {
        CompositeRequestRejectedHandler requestRejectedHandler = new CompositeRequestRejectedHandler(
                new ObservationMarkingRequestRejectedHandler(this.observationRegistry),
                new HttpStatusRequestRejectedHandler());
        filterChainProxy.setRequestRejectedHandler(requestRejectedHandler);
    }
    filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
    filterChainProxy.afterPropertiesSet();

    Filter result = filterChainProxy;
    if (this.debugEnabled) {
        this.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);
    }

    this.postBuildAction.run();
    return result;
}
  • 로직을 보면 FilterChainProxy를 반환하는 것을 알 수 있다.
  • FilterChainProxySecurityFilterChain을 가지고 관리한다.

💡결국, 위 모든 과정은 FilterChainProxySecurityFilterChain(여기서는 defaultSecurityFilterChain)을 속성으로 가지기 위한 과정이다.

커스텀 SecurityConfigurer 만들기

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {

	@Bean
	@Order(SecurityProperties.BASIC_AUTH_ORDER)
	SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
		http.formLogin(withDefaults());
		http.httpBasic(withDefaults());
		return http.build();
	}
	//...
}

기존에는 위에서 만들던 설정을 별도의 클래스를 만들어서 해보자.

SecurityConfig

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated()
                )
                .formLogin(withDefaults())
                .with(new CustomSecurityConfigurer().setFlag(true), withDefaults());

        return http.build();
    }

}

그리고 위에서 사용할 CustomSecurityConfigurer도 작성해야 한다.

CustomSecurityConfigurer

public class CustomSecurityConfigurer extends AbstractHttpConfigurer<CustomSecurityConfigurer, HttpSecurity> {

    private boolean isSecure;

    @Override
    public void init(HttpSecurity builder) throws Exception {
        super.init(builder);
        System.out.println("init method started...");
    }

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        super.configure(builder);
        System.out.println("configure method started...");
        if(isSecure) {
            System.out.println("https is required");
        } else {
            System.out.println("https is optional");
        }
    }

    public CustomSecurityConfigurer setFlag(boolean isSecure) {
        this.isSecure = isSecure;
        return this;
    }
}

AbstractHttpConfigurer

public abstract class AbstractHttpConfigurer<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>

로,
T에는 AbstractHttpConfigurer를 상속받는 CustomSecurityConfigurer를 넣고, B에는 HttpSecurityBuilder를 상속받는 HttpSecurity를 넣어주면 된다.