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

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내부의 여러가지 필터들을 사용자 요청을 처리할때 실행시킨다.
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는 어디서 받는가?
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에는 다음과 같이 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();
}
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에서 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)을 속성으로 가지기 위한 과정이다.
@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();
}
//...
}
기존에는 위에서 만들던 설정을 별도의 클래스를 만들어서 해보자.
@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도 작성해야 한다.
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를 넣어주면 된다.