Spring Security ArchitectureConceptual Architecture실제 구현전체 흐름 구조도 & Filter 목록FilterChainProxy (springSecurityFilterChain — BeanName)웹 요청은 어떻게 FilterChainProxy로 전달될까? ← DelegatingFilterProxy코드로 살펴보는 흐름Spring Security Configuration — 필터 등록 설정configure(HttpSecurity http)configure(WebSecurity web)configure (AuthenticationManagerBuilder auth)user 정보 만들기 (InMemoryUserDetailsManager → UserDetailsService) using DelegatingPasswordEncoderrequestMachers 와 antMachers
Spring Security Architecture
- 스프링 시큐리티는 간단히 말하면
서블릿 필터
의 집합
서블릿 필터
는 서블릿(디스패처 서블릿
) 실행 전에 실행되는 클래스들
- 스프링이 구현하는 서블릿의 이름 :
디스패처 서블릿

Conceptual Architecture
- 거시적인 관점에서 Spring Security는 웹 요청을 가로챈 후 사용자를 인증하고, 인증된 사용자가 적절한 권한을 지니고 있는 확인함
실제 구현
전체 흐름 구조도 & Filter 목록

- 왼쪽의 Filter chain 목록은 spring security와는 상관없는 Filter chain임
DelegatingFilterProxy
: springSecurityFilterChain(FilterChainProxy) 자동으로 등록 & 해당 SecurityFilterChain으로 요청 전달- 여러 Security 정책이 공존할 수 있기에(url 패턴에 따라 FilterChain이 달라질 수 있음) DelegatingFilterProxy를 통하여 특정 FilterChain에 요청을 전달할 수 있게 구성됨
- 각각의 필터는 단일책임원칙(SRP) 처럼, 각기 서로 다른 관심사를 해결함
FilterChainProxy를 구성하는 Filter 목록
- 정말 다양한 필터 구현을 제공함
- 결국 Spring Security를 잘 이해하고 활용한다는 것은 이들 Filter 를 이해하고, 적절하게 사용한다는 것을 의미함
FilterChainProxy (springSecurityFilterChain — BeanName)
- Spring Security의 실제적인 구현은 서블릿 필터 (javax.servlet.Filter 인터페이스 구현체) 를 통해 이루어짐. Spring Security는 필터 구현체의 집합임
- 서블릿 필터는 웹 요청을 가로챈 후 전처리 또는 후처리를 수행하거나, 요청 자체를 리다이렉트 하기도 함
- FilterChainProxy 세부 내용은 WebSecurityConfigurerAdapter 추상 클래스를 상속하는 구현체에서 설정함 (보통 @EnableWebSecurity 어노테이션도 함께 사용)
- 웹 요청은 이러한 필터 체인을 차례로 통과하게 됨
- 웹 요청은 모든 필터를 통과하게 되지만, 모든 필터가 동작하는 것은 아님
- 각 필터는 웹 요청에 따라 동작 여부를 결정할 수 있고, 동작할 필요가 없다면 다음 필터로 웹 요청을 즉시 넘김
- 요청을 처리하고 응답을 반환하면 필터 체인 호출 스택은 모든 필터에 대해 역순으로 진행
- 보통 springSecurityFilterChain 이라는 이름으로 Bean 등록됨
웹 요청은 어떻게 FilterChainProxy로 전달될까? ← DelegatingFilterProxy
- 웹 요청을 수신한 서블릿 컨테이너는 해당 요청을 DelegatingFilterProxy (javax.servlet.Filter 인터페이스 구현체) 로 전달함
- DelegatingFilterProxy Bean은 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; }
public class DelegatingFilterProxy extends GenericFilterBean { @Nullable private String targetBeanName; @Nullable private volatile Filter delegate;
@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 @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 void invokeDelegate( Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { delegate.doFilter(request, response, filterChain); }
Spring Security Configuration — 필터 등록 설정
@EnableWebSecurity
어노테이션 추가,WebSecurityConfigurerAdapter
클래스를 확장하는 Configuration 클래스를 생성- 기본적인 Spring Security 설정이 자동으로 추가되며, 개별 설정을 override 할수 있음
@EnableWebSecurity
→ Spring security의 configuration을 책임지는 클래스에 대해 붙이는 annotation임. 이 어노테이션이 붙으면 해당 필터를 필터체인에서 동작하도록 해줌
@@EnableWebSecurity(debug=true) @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); } @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/api/**") // 해당 메서드를 통해 어떤 url에 대해 필터들이 적용될 것인지 정의 가능함 super.configure(http); } }
WebSecurityConfigurerAdatper
를 상속한 클래스를 통해 Security FilterChain을 구성할 수 있음
- 필터체인에서 어떤 필터들이 적용되고 있는지를 보려면
@EnableWebSecurity(debug=true)
를 설정하면 됨
- 필터체인을 여러개 만들고 싶을때는
WebSecurityConfigurerAdatper
를 상속한 클래스를 여러 개 만들기. 그 후 @Order(1), @Order(2)와 같은 식으로 숫자가 낮은 것 부터 적용되도록 해야함
configure(HttpSecurity http)
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(request->{ request .antMatchers("/**").permitAll() ; }) ; // 사용예시 http .authorizeRequests() .antMatchers("/me").hasAnyRole("USER", "ADMIN") .anyRequest().permitAll() .and() .formLogin() .defaultSuccessUrl("/") .permitAll() ; }
- authorizeRequests 메서드를 활용하여 특정한 url에 대해 인가를 할 수 있음
- .formLogin() 을 통해 폼 로그인
- HttpSecurity 클래스는 세부적인 웹 보안기능 설정을 처리할 수 있는 API를 제공
- 필터체인에 들어갈 필터를 configure할 수 있음
HttpSecurity 주요 메소드
configure(WebSecurity web)
@Override public void configure(WebSecurity web) throws Exception { web.ignoring() .requestMatchers( PathRequest.toStaticResources().atCommonLocations() ); }
- WebSecurity 클래스는 필터 체인 관련 전역 설정을 처리할 수 있는 API 제공
- ignoring()
- Spring Security 필터 체인을 적용하고 싶지 않은 리소스에 대해 설정
- 일반적으로 정적 리소스(*.html, *.css, *.js 등)을 예외 대상으로 설정함
- 불필요한 서버 자원 낭비를 방지함
- atCommonLocations() 를 살펴보면, js, static 등등 디폴트 정적리소스 경로들에 대해 SpringSecurity접근을 안할 수 있도록 설정해주는 것임
configure (AuthenticationManagerBuilder auth)
- 기본 로그인 계정 추가 가능함
- 기본 user 정보는 UserDetailsServiceAutoConfiguration 클래스를 참고하면 어떻게 생성되는지 확인이 가능함
user 정보 만들기 (InMemoryUserDetailsManager
→ UserDetailsService
) using DelegatingPasswordEncoder
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { Map<String,PasswordEncoder> encoders = new HashMap<>(); encoders.put("noop", NoOpPasswordEncoder.getInstance()); auth.inMemoryAuthentication() .passwordEncoder(new DelegatingPasswordEncoder("noop", encoders)) .withUser("user").password("{noop}user123").roles("USER").and() .withUser("admin").password("{noop}admin123").roles("ADMIN"); }
- DelegatingPasswordEncoder에 들어가는 map에 사용할 PasswordEncoder들을 다 등록함
- DelegatingPasswordEncoder를 생성할 시, 어떤 PasswordEncoder를 쓸 것인지 key의 이름을 명시해주고 map을 파라미터로 넘겨줌
- password( ) 파라미터에 어떠한 PasswordEncoder를 쓸 것인지 id를 { } 안에 명시해주어야 함.