이 영역을 누르면 첫 페이지로 이동
천천히 꾸준히 조용히 블로그의 첫 페이지로 이동

천천히 꾸준히 조용히

페이지 맨 위로 올라가기

천천히 꾸준히 조용히

천천히 꾸준히 조용히.. i3months 블로그

[Spring Security] 초기화 과정

  • 2024.05.27 21:56
  • Spring/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

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

  • [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
다른 글 더 둘러보기

정보

천천히 꾸준히 조용히 블로그의 첫 페이지로 이동

천천히 꾸준히 조용히

  • 천천히 꾸준히 조용히의 첫 페이지로 이동

검색

방문자

  • 전체 방문자
  • 오늘
  • 어제

카테고리

  • 분류 전체보기 (677)
    • Algorithm (205)
      • Data Structure (5)
      • Theory && Tip (33)
      • Baekjoon (166)
      • ALGOSPOT (1)
    • Spring (123)
      • Spring (28)
      • Spring Web MVC (20)
      • Spring Database (14)
      • Spring Boot (6)
      • Spring 3.1 (11)
      • Spring Batch (6)
      • Spring Security (16)
      • JPA (12)
      • Spring Data JPA (5)
      • QueryDSL (4)
      • eGovFramework (1)
    • Programming Language (74)
      • C (25)
      • C++ (12)
      • Java (19)
      • JavaScript (15)
      • Python (1)
      • PHP (2)
    • Computer Science (142)
      • Machine Learning (38)
      • Operating System (18)
      • Computer Network (28)
      • System Programming (22)
      • Universial Programming Lang.. (8)
      • Computer Architecture (4)
      • Compiler Design (11)
      • Computer Security (13)
    • Database (21)
      • Database (7)
      • MySQL (3)
      • Oracle (3)
      • Redis (5)
      • Elasticsearch (3)
    • DevOps (20)
      • Docker && Kubernetes (8)
      • Jenkins (4)
      • Amazon Web Service (8)
    • Mobile (28)
      • Android (21)
      • Flutter (7)
    • 💡 솔루션 (17)
    • 👥 모각코 (9)
    • 💬 기록 (7)
    • 📚 공부 (6)
    • -------------- (25)

최근 글

나의 외부 링크

메뉴

  • 홈
반응형

정보

i3months의 천천히 꾸준히 조용히

천천히 꾸준히 조용히

i3months

블로그 구독하기

  • 구독하기
  • RSS 피드

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / Kakao. Copyright © i3months.

티스토리툴바