스프링 시큐리티 - 초기화 과정 이해
인프런 정수원님의 스프링 시큐리티 완전 정복 6.x - 초기화 과정 이해를 정리한다.
스프링 시큐리티 6.x 이전 버전과 바뀐 점, 앞으로 무엇이 변할지도 설명하시는데
스프링 시큐리티 내부 코드까지 다 파헤치면서 매우 자세한 설명을 하시기 때문에 수강하는 걸 추천한다.
스프링 시큐리티 완전 정복 [6.x 개정판] 강의 | 정수원 - 인프런
정수원 | 스프링 시큐리티 6.x 최신 버전으로 제작된 개정판 강의로 초급에서 중.고급에 이르기까지 스프링 시큐리티의 기본 개념부터 API 사용법과 내부 아키텍처를 학습하게 되고 이를 바탕으
www.inflearn.com
프로젝트 구성 및 의존성
강의를 위한 프로젝트 구성은 아래와 같다.
- 스프링 부트 3.x
- JDK 17
- Gradle - Groovy
의존성은 2가지를 추가한다.
- Spring Web
- Spring Security
자동 설정에 의한 보안 기능
- 스프링 시큐리티는 서버가 기동되면 자동으로 스프링 시큐리티의 초기화 작업 및 보안 설정이 이뤄진다.
- 별도의 설정이나 코드가 없어도 기본적인 웹 보안 기능이 시스템에 연동되어 작동된다.
- 기본적인 모든 요청에 인증여부를 검증하고 인증이 승인되어야 자원에 접근이 가능하다.
- 인증 방식은 FormLogin 방식과 HttpBasic 로그인 방식을 제공한다.
- 인증을 할 수 있는 로그인 페이지가 자동적으로 생성되어 렌더링 된다.
- 인증 승인할 수 있게 1개의 계정이 제공된다.
- SecurityProperties 설정 클래스에서 생성한다.
- username : user
- password : 랜덤문자열
자동으로 생성되는 보안 클래스 SpringBootWebSecurityConfiguration 다.
내부 클래스로 SecurityFilterChainConfiguration이 존재하고 @Bean으로 SecurityFilterChain 가 등록되는 걸 알 수 있다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class 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();
}
}
...
}
빈에 등록되는 SecurityFilterChain 코드를 간략하게 설명하자면
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()) 코드는
requests.anyRequest() 모든 요청은 authenticated() 인증을 받게 만드는 내용이다.
다른 의미로는 인증이 없으면 자원에 접근할 수 없다.
formLogin(), httpBasic()는 인증방식을 의미한다.
간단한 구현을 통해 인증 여부가 어떻게 되는지 확인해 보자.
@RestController
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
localhost:8080/ 로 접근하면 index 문자열이 나와야 한다.
하지만 localhost:8080/login 로 바뀌면서
스프링 시큐리티가 제공하는 로그인 화면이 나오게 된다.
기본적으로 제공되는 보안 설정 SecurityFilterChain 때문이다.
제공하는 계정명과 애플리케이션 실행할 때 나오는 랜덤 문자열을 패스워드로 입력하면 인증이 가능하다.
SecurityBuilder 와 SecurityConfigurer
- SecurityBuilder는 빌더 클래스로서 웹 보안을 구성하는 빈 객체와 설정클래스들을 생성하는 역할을 한다.
- 이를 구현하는 WebSecurity, HttpSecurity가 있다.
- SecurityBuilder는 인증 및 인가 초기화 작업을 진행하는 SecurityConfigurer를 참조한다.
- SecurityConfigurer를 통해서 인증 & 인가를 위한 필터를 생성한다.
public interface SecurityBuilder<O> {
/**
* Builds the object and returns it or null.
* @return the Object to be built or null if the implementation allows it.
* @throws Exception if an error occurred when building the Object
*/
O build() throws Exception;
}
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
/**
* Initialize the {@link SecurityBuilder}. Here only shared state should be created
* and modified, but not properties on the {@link SecurityBuilder} used for building
* the object. This ensures that the {@link #configure(SecurityBuilder)} method uses
* the correct shared objects when building. Configurers should be applied here.
* @param builder
* @throws Exception
*/
void init(B builder) throws Exception;
/**
* Configure the {@link SecurityBuilder} by setting the necessary properties on the
* {@link SecurityBuilder}.
* @param builder
* @throws Exception
*/
void configure(B builder) throws Exception;
}
실행 과정
전체적인 흐름은 다음과 같다.
- 스프링부트 AutoConfiguration을 통해 SecurityBuilder를 생성한다.
- SecurityBuilder는 설정클래스인 SecurityConfigurer를 생성한다.
- SecurityConfigurer의 init(), configure() 로 초기화 작업을 진행한다.
HttpSecurity 코드로 보는 과정
HttpSecurity 빈을 생성하고 인증 및 인가를 설정하는 건 HttpSecurityConfiguration 클래스다.
@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;
}
...
}
코드를 살펴보면 빈 스코프가 프로토타입으로 생성하는데 별도의 보안 설정을 위해서다.
인증 및 인가에 관한 설정은 // @formatter 주석으로 구분되는 영역이다.
여기서 설정된 인증 및 인가는 10개다. 각 설정에 대한 내용은 추후 강의로 배움
// @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
※ DefaultLoginPageConfigurer는 자동으로 로그인 페이지를 생성해 준다.
※ 강의에서는 설정이 11개 생성되는데, 버전 차이인지 CorsConfigurer 설정 없이 생성되었다.
이렇게 생성된 빈은 SpringBootWebSecurityConfiguration에서 사용된다.
SpringBootWebSecurityConfiguration
이전에 생성된 HttpSecurity 빈을 받아서 설정을 더 추가하고
최종적으로 build()를 통해 SecurityFilterChain 생성해서 빈으로 등록한다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class 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();
}
}
...
}
http.build() 코드를 따라가 보면 doBuild()가 실행되는데 코드를 간단히 설명하자면
- init() : 각 configure의 초기화 작업을 진행한다
- configure() : 보안 설정을 적용한다.
- performBuild() : SecurityFilterChain를 생성한다.
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;
}
}
/**
* Subclasses must implement this method to build the object that is being returned.
* @return the Object to be buit or null if the implementation allows it
*/
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);
}
}
...
}
이렇게 HttpSecurity.build() 과정은 SecurityFilterChain를 생성하고 반환하는 것으로 마무리된다.
HttpSecurity
- HttpSecurityConfiguration에서 HttpSecurity를 생성하고 초기화를 진행함.
- HttpSecurity는 보안에 필요한 각 설정 클래스와 필터들을 생성하고 최종적으로 SecurityFilterChain 빈을 생성함.
SecurityFilterChain
boolean matches(HttpServletRequest request)
- 요청이 현재 필터 체인에 의해 처리되는지 여부를 결정한다.
- true 반환하면 해당 필터 체인에 처리됨을 의미하고, false를 반환하면 다른 처리 로직이 처리됨을 의미한다.
- 이를 통해 특정 요청에 적절한 보안 필터링 로직이 적용될 수 있다.
List<Filter> getFilters()
- 현재 필터 체인에 포함된 Filter 객체의 리스트를 반환한다.
- 어떤 필터들이 포함되어 있는지 확인할 수 있고, 각 필터는 요청 처리 과정에서 특정 작업(인증, 권한 부여, 로깅 등)을 수행한다.
WebSecurity
- WebSecurityConfiguration에서 WebSecurity를 생성하고 초기화를 진행함.
- WebSecurity는 HttpSecurity에서 생성한 SecurityFilterChain 빈을 SecurityBuilder 에 저장함.
- WebSecurity가 build()를 실행하면 SecurityBuilder에서 SecurityFilterChain을 꺼내어 FilterChainProxy 생성자에 전달함.
코드로 보는 WebSecurity
WebSecurityConfiguration에서 살펴볼 코드 일부분이다.
디버깅으로 흐름을 파악해 보면 HttpSecurity를 통해 SecurityFilterChain 빈이 생성된 후 이를 WebSecurity에서 FilterChainProxy 생성하는 것을 볼 수 있다.
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
...
/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@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();
}
/**
* Sets the {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>}
* instances used to create the web configuration.
* @param objectPostProcessor the {@link ObjectPostProcessor} used to create a
* {@link WebSecurity} instance
* @param beanFactory the bean factory to use to retrieve the relevant
* {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to
* create the web configuration
* @throws Exception
*/
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor,
ConfigurableListableBeanFactory beanFactory) throws Exception {
this.webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
if (this.debugEnabled != null) {
this.webSecurity.debug(this.debugEnabled);
}
List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new AutowiredWebSecurityConfigurersIgnoreParents(
beanFactory)
.getWebSecurityConfigurers();
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException("@Order on WebSecurityConfigurers must be unique. Order of " + order
+ " was already used on " + previousConfig + ", so it cannot be used on " + config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
this.webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
@Autowired(required = false)
void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
this.securityFilterChains = securityFilterChains;
}
...
}
Filter
- 서블릿 필터는 웹 애플리케이션에서 클라이언트의 요청과 서버의 응답을 가공하거나 검사하는 데 사용되는 구성 요소다.
- 서블릿 필터는 클라이언트의 요청이 서블릿에 도달하기 전이나 서블릿이 응답을 클라이언트에게 보내기 전에 특정 작업을 수행할 수 있다
- 서블릿 필터는 서블릿 컨테이너(WAS)에서 생성되고 실행되고 종료된다.
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {
}
}
DelegatingFilterProxy
- 스프링에서 사용하는 특별한 서블릿 필터로, 서블릿 컨테이너와 스프링 애플리케이션 컨텍스트 간의 연결고리 역할을 하는 필터다
- 서블릿 필터의 기능을 수행하는 동시에 스프링의 의존성 주입 및 빈 관리 기능과 연동되도록 설계된 필터다.
- springSecurityFilterChain 이름으로 생성된 빈을 ApplicationContext에서 찾아 요청을 위임한다.
- 실제 보안 처리를 수행하지 않는다.
FilterChainProxy
- springSecurityFilterChain의 이름으로 생성되는 필터 빈으로서 DelegatingFilterProxy 에게 요청을 위임받고 보안 처리 역할을 한다.
- 내부적으로 하나 이상의 SecurityFilterChain 객체들을 가지고 있으며 요청 URL 정보를 기준으로 적절한 SecurityFilterChain를 선택하여 필터들을 호출한다
- HttpSecurity 를 통해 API 추가 시 관련 필터들을 추가된다.
- 사용자의 요청을 필터 순서대로 호출함으로 보안 기능을 동작시키고 필요시 직접 필터를 생성해서 기존 필터의 전, 후로 추가 가능하다.
코드로 보는 DelegatingFilterProxy 생성 과정
애플리케이션이 실행되면 SecurityFilterAutoConfiguration에서 자동으로 빈을 생성하는데
@ConditionalOnBean 으로 springSecurityFilterChain 빈이 있어야 생성되는 걸 알 수 있다.
FIlterChainProxy 생성은 위의 코드로 보는 WebSecurity 를 참고하자
실습으로 보는 작동 과정
디버깅 모드로 애플리케이션을 실행하고 자원에 접근하면 DelegatingFilterProxy이 어떻게 작동하는지 알아보자.
아무 자원이나 요청하면 서블릿 필터인 DelegatingFilterProxy를 무조건 거치게 되어있다.
FilterChainProxy 내부에는 VirtualFilterChain이 존재한다.
여기서 필터가 0번부터 마지막까지 순서대로 수행된다.
사용자 정의 보안 기능 구현
사용자가 정의하는 보안 기능 구현은 한 개 이상의 SecurityFilterChain 타입의 빈을 정의한 후 인증 API 및 인가 API 를 설정한다
기본 구현 코드
- @EnableWebSecuriy를 선언해 줘야 자동 설정으로 어노테이션 안에 있는 여러 가지 설정이 초기화된다.
- 사용자 정의로 SecurityFilterChain 빈을 생성하면 자동 설정에 의한 SecurityFilterChain 빈은 생성되지 않는다.
- 모든 설정 코드는 람다 형식으로 작성해야 한다 (스프링 시큐리티 7 버전부터는 람다 형식만 지원 예정)
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
public @interface EnableWebSecurity {
/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}
자동 설정으로 기본으로 제공되는 SecurityFilterChain 빈은 @ConditionalOnMissingBean 으로 더 이상 생성되지 않는다.
class DefaultWebSecurityCondition extends AllNestedConditions {
DefaultWebSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
@ConditionalOnMissingBean({ SecurityFilterChain.class })
static class Beans {
}
}
사용자 추가 설정
기본으로 제공하는 사용자 정보도 application.properties 혹은 application.yml 파일로 정의할 수 있다.
spring:
security:
user:
name: user
password: 1111
roles: USER
자바 설정 클래스에서 직접 정의도 가능하다.
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}1111")
.roles("USER")
.build();
UserDetails user2 = User.withUsername("user2")
.password("{noop}1111")
.roles("USER")
.build();
UserDetails user3 = User.withUsername("user3")
.password("{noop}1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user, user2, user3);
}