[Spring Security] 초기화 과정
스프링 시큐리티는 애플리케이션 시작 시 수행되는 초기화 과정에서 인증이나 인가에 관련된 여러 작업을 수행한다.
스프링 시큐리티 의존성을 추가한 후 애플리케이션을 실행하면 의존성으로 내려받은 SpringBootWebSecurityConfiguration 클래스를 통해 초기 설정을 진행한다.
@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();
}
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(DefaultWebSecurityCondition.class)
public @interface ConditionalOnDefaultWebSecurity {
}
class DefaultWebSecurityCondition extends AllNestedConditions {
DefaultWebSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
@ConditionalOnMissingBean({ SecurityFilterChain.class })
static class Beans {
}
}
소스코드에서 확인할 수 있듯, SecurityFilterChain과 HttpSecurity 클래스가 ClassPath에 존재하고 SecurityFilterChain 빈을 개발자가 생성하지 않은 경우 기본 보안 설정을 진행한다.
SecurityBuilder는 웹 보안을 구축하는 빈 객체와 설정 클래스들을 생성하고
SecurityConfigurer는 보안 관련 필터들을 생성하고 초기화 작업을 직접 수행한다.
SecurityBuilder는 SecurityConfigurer를 참조한다.
즉, SecurityBuilder가 SecurityConfigurer 같은 설정 클래스들을 생성하고 SecurityConfigurer가 가지는 여러 클래스들이 인증 / 인가 프로세스를 처리한다.
자동 설정으로 생성된 HttpSecurity 빈으로 SecurityConfigurer 타입의 여러 설정 클래스들을 생성한다.
@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;
}
애플리케이션 실행 후 디버깅 해 보면 httpSecurity 메서드를 실행함을 확인할 수 있다.
여기서 csrf, addFilter 등 다양한 메서드를 실행하는데, 메서드를 타고 들어가보면 메서드에서 사용하는 구현체들은 SecurityConfigurer 클래스들을 상속받고 있음을 확인할 수 있다.
(http를 통해서 특정 설정을 추가할 때 마다 관련된 Configurer 클래스가 생성된다고 생각하면 된다)
이렇게 생성한 Prototype HttpSecurity 빈은 SpringBootWebSecurityConfiguration 클래스에서 사용한다.
@Override
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}
@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;
}
}
해당 클래스에서 http.build() 메서드를 호출해 SecurityFilterChain 빈을 생성한다.
build 메서드 호출 시 init / configure / build 과정이 실행돼 모든 초기화 작업을 마친다.
init 과정에서는 이전에 추가해 둔 SecurityConfigurer 구현체들을 초기화하고,
configure 과정에서는 이 구현체들을 설정한다.
여기서 초기화하고 설정하는 필터들을 개발자가 직접 만들 수도 있으니.. 요구사항에 맞춰서 추가하자.
HttpSecurity의 목적은 SecurityFilterChain 빈 생성이다.
설정 클래스인 SecurityConfigurer 클래스의 구현체들은 SecurityFilterChain 빈에 Filter로 등록된다.
간혹 관리자 페이지와 사용자 페이지를 한 애플리케이션에서 관리하는 경우와 같이 다중 보안 설정이 필요해 여러 개의 SecurityFilterChain을 생성하는 경우도 있는데,
이 경우 해당 요청을 현재 SecurityFilterChain이 처리해야 하는지를 결정하는 SecurityFilterChain의 matches 메서드를 사용한다.
필터는 인증, 권한 부여 뿐만 아니라 로깅 작업도 수행할 수 있으니 모든 요청에 대해 로그를 기록하고 싶은 경우에도 필터를 사용하자.
요청 모든 필터를 거친 후 서블릿으로 전달된다.
HttpSecurity 는 애플리케이션의 HTTP 보안 구성을 담당해 특정 HTTP 요청에 대한 보안 규칙을 정의한다.
WebSecurity는 HttpSecurity의 상위 개념으로, 전역 보안 설정을 담당해 요청은 먼저 WebSecurity의 구성을 거친 후 HttpSecurity로 넘어간다.
특정 경로를 보안 필터 체인에서 제외하거나 정적 자원에 대한 보안을 설정할 수 있다.
HttpSecurity는 build 시 SecurityFilterChain 빈을 생성하고,
WebSecurity는 build 시 SecurityFilterChain을 꺼내 FilterChainProxy 빈을 생성한다.
즉, FilterChainProxy에는 SecurityFilterChain들이 가지고 있는 모든 Filter를 가지고 있어, 클라이언트의 요청이 들어오면 어떤 필터를 적용할 지 결정한다.
@SuppressWarnings("unchecked")
@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);
}
performBuild 메서드에서는 모든 HttpSecurity에서 설정한 구현체들을 바탕으로 FilterChain을 만든다.
@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();
}
HttpSecurity에서 만들어진 FilterChain은 WebSecurityConfiguration 에서 가져온 후 FilterChainProxy 객체를 생성한다.
Filter는 기본적으로 WAS에서 생성되고 종료되며, 클라이언트의 요청이 서블릿에 도달하기 전이나 응답을 클라이언트에게 보내기 전에 특정 작업을 수행할 때 사용된다.
DelegatingFilterProxy는 스프링에서만 사용되는 서블릿 필터로, 서블릿 컨테이너와 스프링 컨테이너와의 연결고리 역할을 한다.
앞서 말했듯 스프링 시큐리티는 Filter기반으로 동작하고, 여기서 사용되는 Filter는 WAS와 연관되어있고 스프링 컨테이너와는 연관되지 않아 DI, AOP 등 스프링 컨테이너가 제공하는 편의 기능을 사용할 수 없다.
이 부분을 해결하기 위해 DelegatingFilterProxy가 도입됐다.
요청이 DelegatingFilterProxy를 거치면 springSecurityFilterChain 이름으로 생성된 빈을 스프링 컨테이너에서 찾고 요청을 해당 빈에게 넘겨버린다.
여기서 springSecurityFilterChain 빈이 바로 FilterChainProxy이다.
public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";
// SecurityFilterAutoConfiguration
@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;
}
// AbstractFilterRegistrationBean
@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
Filter filter = getFilter();
return servletContext.addFilter(getOrDeduceName(filter), filter);
}
// FilterChainProxy
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);
}
}
DelegatingFilterProxy 필터를 springSecurityFilterChain 이름으로 등록한 후 WAS의 필터로 등록하는 부분이다.
FilterChainProxy 클래스는 VirtualFilterChain으로 스프링 전용 Filter를 관리한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.withUsername("user")
.password("{noop}1111")
.roles("USER").build();
UserDetails user2 = User.withUsername("user")
.password("{noop}1111")
.roles("USER").build();
return new InMemoryUserDetailsManager(user1, user2);
}
}
실제 애플리케이션에서는 SecurityFilterChain 빈을 직접 만들어 기본으로 제공되는 설정 대신 사용한다.
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security] 인증 아키텍처 (0) | 2024.06.08 |
---|---|
[Spring Security] 인증 메커니즘 (0) | 2024.05.30 |
[Spring Security6] 인증 서버 구축 (0) | 2023.08.15 |
[Spring Security6] OAuth2와 OpenID Connect (0) | 2023.08.13 |
[Spring Security 6] 메서드 단위 권한 요청 (0) | 2023.08.12 |
댓글
이 글 공유하기
다른 글
-
[Spring Security] 인증 아키텍처
[Spring Security] 인증 아키텍처
2024.06.08 -
[Spring Security] 인증 메커니즘
[Spring Security] 인증 메커니즘
2024.05.30 -
[Spring Security6] 인증 서버 구축
[Spring Security6] 인증 서버 구축
2023.08.15 -
[Spring Security6] OAuth2와 OpenID Connect
[Spring Security6] OAuth2와 OpenID Connect
2023.08.13