SecurityBuilder와 SecurityConfigurer
SecurityBuilder와 SecurityConfigurer
본 포스팅 시리즈에서 Spring Security는 Spring Boot 2.7.x 버전을 기준으로한 강의를 최신 스프링 공식 문서를 참고하여 수정하였습니다. Deprecated된 여러 메서드들을 최대한 최신에 맞게 바꾸었지만 완벽하지 않을 수 있습니다.
Spring Security OAuth 시리즈
개념 및 구조 이해
SecurityBuilder
는 빌더 클래스로서 웹 보안을 구성하는 빈 객체와 설정 클래스들을 생성하는 역할을 하며WebSecurity
,HttpSecurity
가 있다.
SecurityConfigurer
는 Http요청과 관련된 보안처리를 담당하는 필터들을 생성하고 여러 초기화 설정에 관여한다.
SecurityBuilder
는SecurityConfigurer
를 포함하고 있으며 인증 및 인가 초기화 작업은SecurityConfigurer
에 의해 진행된다.
시퀀스 다이어그램
SecurityBuilder
에서build()
를 호출하면,SecurityConfigurer
의init()
과configure()
가 호출되어 초기화 작업이 진행된다.
WebSecurity
의build()
가 반환하는 최종 클래스는build()
내부의performBuild()
가 반환한FilterChainProxy
이다.HttpSecurity
의build()
가 반환하는 최종 클래스는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;
}
- 해당
HttpSecurity
에SecurityConfigurer
구현체를 등록하고 있다.
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()
는SecurityFilterChain
의build()
와 똑같이 동작한다. (doBuilder, init, configure, …)- 그러나
performBuild()
의 경우 구현체에 따라 다르게 동작하는데, 실제로 구현체를 보면 다음과 같이 나온다. AuthenticationManagerBuilder
는ProviderManager
를 반환한다.HttpSecurity
는DefaultSecurityFilterChain
을 반환한다(앞서 본 것과 같이)WebSecurity
는Filter
를 반환한다.
- 그러나
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
를 반환하는 것을 알 수 있다. - 이
FilterChainProxy
가SecurityFilterChain
을 가지고 관리한다.
💡결국, 위 모든 과정은
FilterChainProxy
가SecurityFilterChain
(여기서는 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
를 넣어주면 된다.