1. 서론
본격적인 OAuth, JWT 구현에 앞서, 이전 포스팅들에서 생략하고 넘어갔었던 본 프로젝트의 Security Configuration과 이 구성이 적용되는 정확한 동작 과정을 파악해보고자 한다.
2. Custom Security Configuration
Security Configuration은 Spring security 동작에 대한 환경 설정을 구성하고 있다. 즉, 인증과 인가에 대한 모든 흐름과 그에 대한 설정을 관리한다. 웹 서비스가 로드 될 때 Spring Container(https://ittrue.tistory.com/220)에 의해 관리된다.
SecurityConfig.java
package com.urcarcher.be.blkwntr.auth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.urcarcher.be.blkwntr.auth.jwt.JwtAuthenticationFilter;
import com.urcarcher.be.blkwntr.auth.jwt.JwtCookieProvider;
import com.urcarcher.be.blkwntr.auth.jwt.JwtTokenProvider;
import com.urcarcher.be.blkwntr.auth.service.UrcarcherOAuth2Service;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final JwtCookieProvider jwtCookieProvider;
private final UrcarcherOAuth2Service urcarcherOAuth2Service;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
private static final String[] BLACK_LIST = {"/api/paymentPlace/**, /api/exchange/**, /api/reserve/**"};
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 웹 어플리케이션이 아닌 브라우저가 직접 ID와 PW를 검증하여 인가를 제한하는 방법 => JWT를 사용하므로 필요하지 않음, 비활성화
.httpBasic(basicConfig->
basicConfig
.disable()
)
// JWT를 사용하는 REST API 서버이므로 서버에 인증 정보가 존재하지 않는다. 따라서 위조요청 방지를 위한 추가 설정이 필요치 않음, 비활성화
.csrf(csrfConfig->
csrfConfig
.disable()
)
// UsernamePasswordAuthenticationFilter 비활성화, formLogin은 기본적으로 세션을 이용하는 방식인데, JWT를 사용할 것이므로 필요없음
.formLogin(formLoginConfig->
formLoginConfig
.disable()
)
// JSESSIONID(세션 유지를 위한 쿠키) 비활성화, JWT를 사용하므로 필요없음
.sessionManagement(sessionConfig->
sessionConfig
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// BLACK_LIST 경로에 존재하는 자원에 대한 접근을 제한한다.(인증 필요) 이외에는 모두 허용
.authorizeHttpRequests(authConfig->{
authConfig
.requestMatchers(BLACK_LIST).authenticated()
.anyRequest().permitAll();
})
// OAuth 로그인에 대한 흐름 설정
.oauth2Login(oauth2Config -> {
oauth2Config
// Backend가 다중 서버로 구성될 경우 필요한 설정, OAuth 인증 과정에서 사용되는 여러 파라미터를 쿠키로 저장하여 진행시킴
.authorizationEndpoint(authEndPoint -> {
authEndPoint.authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository);
})
// 로그인 성공 시 urcarcherOAuth2Service에서 제공받은 유저 정보를 처리함
.userInfoEndpoint(load->load.userService(urcarcherOAuth2Service))
// 로그인 성공 시 수행해야할 필요한 로직을 처리함
.successHandler(oAuth2SuccessHandler);
})
// UsernamePasswordAuthenticationFilter보다 앞 순위에 필터를 배치한다는 뜻, 우리는 이 필터가 비활성화 돼 있으므로
// 실질적으로 사용자는 JwtAuthenticationFilter를 거치게 됨.
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtCookieProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
사실상 Spring security의 본체이자 중추라고 할 수 있다. Spring security는 여러 개의 Filter를 거치게 하는 Filter Chain으로 인증 및 인가에 대한 처리를 수행한다. Filter Chain이 없다면,
Http Request > API Server > Servlet > Controller
이와 같이 Servlet에 바로 도달하게 될 것이다. 하지만 Spring security를 사용할 시 Filter Chain을 구성한다면
Http Request > API Server > Filter 1 > Filter 2 > ... > Filter n > Servlet > Controller
다음과 같이 표준 Servlet에 도달하기 전, 여러 필터를 먼저 거치게 하여 부적절한 접근을 제한할 수 있다.
이를 가능하게 하는 메서드가 Spring Bean으로 정의된 filterChain()이다. 매개변수로 들어오는 HttpSecurity 객체의 설정을 기반으로 Chain을 생성한다고 이해하면 된다. 따라서 어플리케이션 구동 시, 이에 해당하는 설정에 따라 Filter Chain이 구성된다.
3. Spring security 동작 시작점(SecurityFilterAutoConfiguration)
필자는 상기된 SecurityConfig class에서 Spring Bean으로 정의된 filterChain()이 정확히 언제 어떻게 주입되는지 궁금했다. 또한 Spring security의 정확한 동작 과정을 파악하고 싶었다. 그렇게 Spring security의 소스 코드를 하나하나 뜯어보고 여러 자료를 참고하면서 거슬러 올라가 Spring security가 이용하는 DelegatingFilterProxy에 도달하게 되었다.
거시적인 흐름은 사용자 요청 > Servlet Container > Spring Container 와 같다. Servlet Container는 Servlet Filter를 관리하고 Spring Container는 Spring Bean을 관리한다. 따라서 Servlet Container에서는 Spring Container에서 정의된 Bean을 주입해서 사용할 수 없다. DelegatingFilterProxy class(Spring이 제공)는 Servlet Container와 Spring Container간의 다리 역할을 한다. DelegatingFilterProxy는 Spring Container에 종속된 Filter Bean을 가질 수 있으며, Spring security에서 목적으로 하는 Filter Bean이 FilterChainProxy이다. 그리고 바로 이 객체가 Spring security의 동작 수행을 가능하게 만드는 것이다.
소스 코드와 함께 Spring security 동작 지점의 처음부터 살펴보자.
...
@AutoConfiguration(after = SecurityAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.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;
}
...
Spring boot Project에 security 의존성을 추가하면, 위와 같은 SecurityFilterAutoConfiguration가 생성이 된다. @ConditionalOnWebApplication(type = Type.SERVLET) 어노테이션의 의미는 WebApplication의 type이 SERVLET일 경우 이 class가 등록이 된다는 의미이다.
...
@EnableScheduling
@SpringBootApplication
public class UrcarcherBeApplication {
public static void main(String[] args) {
SpringApplication.run(UrcarcherBeApplication.class, args);
}
}
Project 최상단의 Application class에는 생략이 돼 있지만, WebApplicationType의 default 값은 SERVLET이므로, Spring boot 어플리케이션 실행 시 SecurityFilterAutoConfiguration 객체는 DelegatingFilterProxyRegistrationBean을 bean으로 생성하게 된다. 이름에서 보여지다시피 해당 bean은 DelegatingFilterProxy를 생성한다. 따라서 의존성 추가만으로 Spring boot 환경의 자동 설정에 의해 DelegatingFilterProxy를 간편하게 설정할 수 있는 것이다. 이 때 @ConditionalOnBean(name = DEFAULT_FILTER_NAME) 어노테이션에서 알 수 있듯이, DEFAULT_FILTER_NAME(="springSecurityFilterChain")라는 이름을 가진 bean이 존재해야 DelegatingFilterProxyRegistrationBean을 정의할 수 있다. 여기서 springSecurityFilterChain 이름을 가진 bean이 바로 FilterChainProxy이다.
...
public class DelegatingFilterProxyRegistrationBean extends AbstractFilterRegistrationBean<DelegatingFilterProxy>
implements ApplicationContextAware {
...
public DelegatingFilterProxyRegistrationBean(String targetBeanName,
ServletRegistrationBean<?>... servletRegistrationBeans) {
super(servletRegistrationBeans);
Assert.hasLength(targetBeanName, "TargetBeanName must not be null or empty");
this.targetBeanName = targetBeanName;
setName(targetBeanName);
}
...
@Override
public DelegatingFilterProxy getFilter() {
return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {
@Override
protected void initFilterBean() throws ServletException {
// Don't initialize filter bean on init()
}
};
}
...
DelegatingFilterProxyRegistrationBean은 AbstractFilterRegistrationBean를 상속 받고 있고, 계속해서 상속 구조를 파악하면 이는 ServletContextInitializer를 구현하고 있다. 즉 Spring boot 어플리케이션 실행시 Servlet Container에 DelegatingFilterProxyRegistrationBean의 getFilter() 메서드가 반환하는 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;
}
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
...
등록된 DelegatingFilterProxy는 doFilter()를 수행하는데, 이 안에서 initDelegate()로 ApplicationContext에서 "springSecurityFilterChain"라는 이름을 가진 bean(FilterChainProxy)을 가져오고 invokeDelegate()로 사용자의 request를 위임 대상인 FilterChainProxy에게 위임하게 된다. String targetBeanName = getTargetBeanName(); 은 DelegatingFilterProxyRegistrationBean으로부터 전달받은 FilterChainProxy의 bean 이름이다.
4. WebSecurityConfiguration
결국 FilterChainProxy가 존재해야 DelegatingFilterProxy를 생성할 수 있는 것인데, 그렇다면 FilterChainProxy bean은 어떻게 생성되는가? 답은 WebSecurityConfiguration에 있다.
...
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
...
private WebSecurity webSecurity;
private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();
@Autowired(required = false)
private HttpSecurity httpSecurity;
...
@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();
}
...
@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
this.securityFilterChains = securityFilterChains;
}
...
...
@Configuration(proxyBeanMethods = false)
class HttpSecurityConfiguration {
...
@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;
}
...
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) 여기서 반가운 변수가 나온다. 바로 DEFAULT_FILTER_NAME이다. 즉 springSecurityFilterChain 이름의 bean은 WebSecurityConfiguration class로부터 정의된다.
부연을 하자면 먼저 HttpSecurityConfiguration에서 HttpSecurity가 bean 등록이 된다(@Scope("prototype")는 bean을 주입 받을 때마다 새로 생성된다는 뜻 = 싱글톤으로 관리X). 우리가 커스텀한 SecurityConfig.java에서 매개변수로 HttpSecurity를 bean 주입받고, SecurityFilterChain을 bean으로 등록한다. 이렇게 우리가 직접 만든 SecurityFilterChain은 WebSecurityConfiguration의setFilterChains() setter dependency injection으로 securityFilterChains에 주입이 된다. 이후 springSecurityFilterChain은 SecurityFilterChain이 담긴 securityFilterChains를 WebSecurity에 담은 뒤, webSecurity.build()를 반환하게 된다.
5. AbstractConfiguredSecurityBuilder & AbstractSecurityBuilder
Spring security에서 자주 등장하는 build() 메서드를 먼저 설명하겠다. WebSecurity가 상속 받고 있는 AbstractConfiguredSecurityBuilder는 doBuild() 메서드를 가지고 있다. doBuild()는 이 추상클래스의 자식들이 각각의 초기화(init())와 구성(configure())을 하고 build한 결과(performBuild())를 반환한다. 또 AbstractConfiguredSecurityBuilder는 AbstractSecurityBuilder를 상속받는데, 바로 이 추상클래스가 build() 메서드를 가지고 있다.
...
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>>
extends AbstractSecurityBuilder<O> {
...
@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;
}
}
...
protected abstract O performBuild() throws Exception;
@SuppressWarnings("unchecked")
private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.init((B) this);
}
for (SecurityConfigurer<O, B> configurer : this.configurersAddedInInitializing) {
configurer.init((B) this);
}
}
@SuppressWarnings("unchecked")
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}
...
...
public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> {
private AtomicBoolean building = new AtomicBoolean();
private O object;
@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");
}
...
즉, webSecurity.build()를 수행하게 되면, this.object에 doBuild()로 인해 webSecurity가 수행하는 performBuild()가 담기게 된다.
그렇다면 이제 WebSecurity의 performBuild()를 보자.
...
public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity>
implements SecurityBuilder<Filter>, ApplicationContextAware, ServletContextAware {
...
private final List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<>();
...
@Override
protected Filter performBuild() throws Exception {
...
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);
...
filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
...
return result;
}
...
WebSecurityConfiguration로부터 전달받은 SecurityFilterChain은 WebSecurity의 field 변수인 securityFilterChainBuilders에 담기게 된다.(SecurityFilterChain은 SecurityBuilder에 담겨 있는 형태) 따라서 performBuild()가 실행되면, 전달받은 SecurityFilterChain을 FilterChainProxy에 담아 FilterChainProxy를 반환한다.
6. SecurityFilterChain
...
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
...
@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);
}
...
...
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
this(requestMatcher, Arrays.asList(filters));
}
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
if (filters.isEmpty()) {
logger.debug(LogMessage.format("Will not secure %s", requestMatcher));
}
else {
List<String> filterNames = new ArrayList<>();
for (Filter filter : filters) {
filterNames.add(filter.getClass().getSimpleName());
}
String names = StringUtils.collectionToDelimitedString(filterNames, ", ");
logger.debug(LogMessage.format("Will secure %s with filters: %s", requestMatcher, names));
}
this.requestMatcher = requestMatcher;
this.filters = new ArrayList<>(filters);
}
public RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
@Override
public List<Filter> getFilters() {
return this.filters;
}
@Override
public boolean matches(HttpServletRequest request) {
return this.requestMatcher.matches(request);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " [RequestMatcher=" + this.requestMatcher + ", Filters=" + this.filters
+ "]";
}
}
개발자가 커스터마이징하여 Bean 등록한 SecurityFilterChain(SecurityConfig.java)은 결국 FilterChainProxy에 담긴다. SecurityConfig.java를 보면 Bean 주입이 되는 대상은 http.build()이다. 마찬가지로 HttpSecurity는 AbstractConfiguredSecurityBuilder > AbstractSecurityBuilder를 상속 받고 있는데, 5. AbstractConfiguredSecurityBuilder & AbstractSecurityBuilder에서 설명한 것처럼 http.build()는 HttpSecurity performBuild() 메서드의 반환값과 같다. 즉 performBuild()가 수행되며 개발자가 설정한대로 filter들을 만든 뒤 filter들을 순서대로 배치한 다음 filters를 SecurityFilterChain의 구현체인 DefaultSecurityFilterChain에 담아 반환하게 된다. SecurityFilterChain는 Interface이므로 FilterChainProxy가 넘겨 받은 SecurityFilterChain의 정체가 DefaultSecurityFilterChain인 것이다. 따라서 getFilters() 메서드로 DefaultSecurityFilterChain의 filters를 얻을 수 있다.
7. FilterChainProxy
...
public class FilterChainProxy extends GenericFilterBean {
...
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
catch (Exception ex) {
...
}
finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
List<Filter> filters = getFilters(firewallRequest);
...
FilterChain reset = (req, res) -> {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
}
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
this.filterChains.size()));
}
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
public List<Filter> getFilters(String url) {
return getFilters(this.firewall.getFirewalledRequest(new FilterInvocation(url, "GET").getRequest()));
}
...
private static final class VirtualFilterChain implements FilterChain {
...
private VirtualFilterChain(FilterChain chain, List<Filter> additionalFilters) {
...
}
...
public static final class VirtualFilterChainDecorator implements FilterChainDecorator {
...
@Override
public FilterChain decorate(FilterChain original, List<Filter> filters) {
return new VirtualFilterChain(original, filters);
}
}
...
다시 돌아와서 DelegatingFilterProxy로부터 요청을 위임받은 FilterChainProxy는 doFilter()로 필터링을 수행하게되면 doFilter()는 doFilterInternal()을 호출한다. doFilterInternal()에서 실질적인 필터링 로직이 수행되는데, getFilters()가 실행되면 SecurityFilterChain의 getFilters()로 전달받은 SecurityFilterChain에 대한 필터 목록을 가져온다. 이후 내부 class인 VirtualFilterChain을 통해 필터링을 수행하게 된다.
8. 요약
- 어플리케이션 실행 시 SecurityFilterAutoConfiguration이 DelegatingFilterProxyRegistrationBean bean주입
- DelegatingFilterProxyRegistrationBean은 getFilter() 메서드로 Servlet Container에 DelegatingFilterProxy 등록
- DelegatingFilterProxy는 user request를 FilterChainProxy에게 위임
- WebSecurityConfiguration로부터 bean 주입되는 FilterChainProxy는 SecurityFilterChain을 가지고 있음
- FilterChainProxy는 개발자가 커스터마이징한 SecurityFilterChain을 주입받고 필터링 수행
Project Github Repo.
https://github.com/1nthebleakmidwinter/Urcarcher_Card-Service_ShinhanDS-Academy
References
- https://velog.io/@jjya_3562/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-SecurityConfig
- https://velog.io/@choidongkuen/Spring-Security-Spring-Security-Filter-Chain-%EC%97%90-%EB%8C%80%ED%95%B4
- https://velog.io/@yaho1024/spring-security-delegatingFilterProxy
- https://velog.io/@platinouss/Spring-Security-%EB%8F%99%EC%9E%91-%EA%B3%BC%EC%A0%95-%EB%B0%8F-%EA%B4%80%EB%A0%A8-%ED%95%84%ED%84%B0
- https://velog.io/@jeongyunsung/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%95%B4%EB%B6%80%ED%95%99-Security1-
- https://velog.io/@alsry922/WebSecurity-HttpSecurity-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SecurityFilterChain
'Spring boot > Spring security' 카테고리의 다른 글
[Spring boot] OAuth(Google) & JWT Login (6) - JWT + 쿠키 (0) | 2024.11.11 |
---|---|
[Spring boot] OAuth(Google) & JWT Login (5) - OAuth 동작 ② (14) | 2024.11.06 |
[Spring boot] OAuth(Google) & JWT Login (4) - OAuth 동작 ① (1) | 2024.11.04 |
[Spring boot] OAuth(Google) & JWT Login (2) - 자체 로그인 동작 구조 (6) | 2024.10.28 |
[Spring boot] OAuth(Google) & JWT Login (1) - 프로젝트 기본 구조 (7) | 2024.10.22 |