[개발지식] Web Application의 GateKeeper 完 - Spring Security의 부가적인 기능들과 설정에 대한 추가분석

Hyo Kyun Lee·2025년 12월 11일

개발지식

목록 보기
119/131

1. 개요

Spring Security를 분석하는 과정의 마지막 단계로, 부가적인 기능들과 함께 보안흐름을 더 강화할 수 있는 설정에 대해 분석해보고자 한다.

지금까지 Spring Security의 핵심 원리가 Filter에 근거하였다면, Servlet에 적용할 수 있는 방법을 알아보거나 혹은 비동기 통합, 다중 보안 설정 등 다양한 보안강화 방식에 대해 이해하고 살펴보고자 한다.

또한 단순히 부가적인 기능과 설정에 대해 살펴보는 것을 넘어, Spring Security의 인증 및 인가처리와 함께 실무에서 어떻게 활용할 수 있을지 그 방안에 대해 분석해보는 과정으로 그 기록을 남긴다.

2. Servlet API

Spring Security는 Filter뿐만 아니라 Servlet 관점에서도 유용한 보안 정책을 구성할 수 있도록 한다.

이를 넓게 보면 다양한 프레임워크, API와의 통합, 더불어 Servlet3, SpringMVC와의 통합을 할 수 있도록 지원하면서 전반적으로 유연하게 Spring Security의 보안정책을 제공할 수 있도록 한다.

Servlet3에서 제공하는 핵심적인 기능들은 다음과 같은데,

기능설명
플러그형 웹 앱 구성(WebFragment)web.xml 없이 자바 기반으로 Filter, Servlet, Listener 를 등록 가능
비동기 요청 처리(Async Servlet)긴 작업을 비동기적으로 처리
ServletRequest#getUserPrincipal, isUserInRole 강화컨테이너 기반 인증과의 통합을 강화
HttpServletRequestWrapper 손쉬운 확장request object 확장 기능 강화

앞서 보았던 Spring Security Listener또한 Servlet3.0과 함께 연계되어 보안흐름을 제공해주는 기능 중 하나이다.

이 중 지금 살펴볼 Servlet3에서 제공하는 통합 핵심 컴포넌트의 요점은, 필터에서 처리하던 인증과정을 세션과 Request 영역까지 확장하여 Spring Security의 인증 프레임워크와 동일한 근거로 동작할 수 있도록 확장성을 제공한다는 점이다.

이에 대한 핵심 컴포넌트가 아래 3가지이다.

  • SecurityContextHolderAwareRequestFilter
  • HttpServlet3RequestFactory
  • Servlet3SecurityContextHolderAwareRequestWrapper

2-1. Servlet3 통합 인증기능 제공을 위한 핵심 컴포넌트 3가지

  • SecurityContextHolderAwareRequestFilter

HTTP 요청이 처리될때 HttpServletRequest에 보안 API를 제공하는 Wrapper클래스인 Servlet3SecurityContextHolderAwareRequestWrapper로 감싸주는 "필터"이다.

즉, 이로 인해 필터 내부에서만 인증객체를 활용할 수 있었던 기존 동작에서, Http 요청객체에 인증정보를 반영할 수 있도록 클래스를 한층 업그레이드하는 역할을 해주는 필터이다.

기존 Servlet API의 request.isUserInRole(), request.getUserPrincipal() 등을 호출할때, 이를 Spring Security와 "통합"함으로써 Spring Security의 Authentication 객체 기반으로 동작하도록 변환한다.

Servlet 컨테이너 기반 인증 API를 Spring Security 인증과 통합하여, 인증객체를 가진 request를 기반으로 필터동작을 지속한다.

따라서, Servlet API와 Spring Security의 Authentication 정보를 일관되게 유지하여(특히 서블릿 기반 레거시 환경에서), 보안 프레임워크 간 인증정보 불일치를 방지하고 인증/인가 로직이 모든 레이어에서 동일하게 동작하도록 유도한다.

  • HttpServlet3RequestFactory

Servlet 3.0 기능을 지원하는 Request Wrapper를 생성하는, 말 그대로 "팩토리"이다.

이 factory는 SecurityContextHolderAwareRequestFilter가 사용할 RequestWrapper를 어떤 방식으로 만들지 결정하는데, Servlet3 API 지원 여부를 체크하여 적절한 wrapper를 선택하는 기능을 제공한다.

API설명
create(HttpServletRequest request, Authentication auth)인증정보를 반영한 Request Wrapper 생성

위와 같이, create api를 제공하여 wrapper 클래스를 생성할 수 있도록 도움을 주며, 그 필요 시점은 SecurityContextHolderAwareRequestFilter 내부에서 request wrapping을 하는 시점이다.

  • Servlet3SecurityContextHolderAwareRequestWrapper

HttpServletRequestWrapper를 확장하여, 기존 Servlet API의 인증 API 호출을 Spring Security 인증 정보 기반으로 재구현할 수 있도록 도와주는 역할을 한다.

기존 Servlet 기반으로 동작하는 Wrapper 클래스는

Servlet APIWrapper가 제공하는 동작
getUserPrincipal()→ Spring Security Authentication.getPrincipal() 반환
isUserInRole(role)→ Spring Security의 Authorities 와 비교
getRemoteUser()→ Authentication.getName()
authenticate() / login() (Servlet3 API)→ Spring Security 방식으로 인증 수행

의 인증수행기능을 제공해주었다면,

API설명
getUserPrincipal()Spring Security Principal로 재정의
isUserInRole(String role)GrantedAuthority 기반으로 재정의
authenticate(HttpServletResponse response)Spring Security로 인증 처리
login(String username, String password)Programmatic login 지원

Servlet3SecurityContextHolderAwareRequestWrapper는 말 그대로 인증정보를 Spring Security와 비교하는 것이 아닌, 직접 그 내부로 들어가서 Security Context의 authentication 인증객체 및 grantedAuthority와 같은 인스턴스를 직접 참조하는 것이 가능해진댜.

컨트롤러(JSP 포함)에서 Servlet API 인증 메서드를 사용하거나, 필터/인터셉터/기타 서블릿 기반 프레임워크에서 인증정보 활용할 때, 특히 Spring Security 필터기반의 인증처리에서 보이는 한계점을 보완하기 위해, Programmatic login로 인증처리를 구현하고자 할때 많이 사용한다(즉, 확장성 측면에서 인증처리를 할 수 있다는 점이 강력하다).

2-2. 동작원리

결론적으로 말하면, factory에서 생성한 wrapper클래스에 Servlet3SecurityContextHolderAwareRequestWrapper 클래스를 겹쳐서, 최종적으로 Servlet 기반 인증처리 시 Spring Security에서 제공하는 인증객체와 함께 연계하여 처리할 수 있도록 한다.

[1] 요청 진입
     |
     V
[2] Spring Security Filter Chain 시작
     |
     V
[3] SecurityContextPersistenceFilter
     - HttpSession에서 Authentication 로딩
     |
     V
[4] SecurityContextHolderAwareRequestFilter
     - 현재 Authentication 조회
     - HttpServletRequest → Servlet3SecurityContextHolderAwareRequestWrapper 로 감싸기
     |
     V
[5] DispatcherServlet 및 Controller
     - request.getUserPrincipal() 호출 → Spring Security Principal 반환
     - request.isUserInRole("ADMIN") → Spring Security 권한 기반 결과 반환
     |
     V
[6] 응답 및 SecurityContext 저장

위와 같은 방식으로, Servlet 기반(프로그래밍)의 인증처리를 진행함과 동시에 Spring Security 프레임워크에 인증흐름을 바꾸지 않고 서로 연계하도록 하여 보안처리를 확장성있게, 유연하게 가능하도록 구현해준 것이 핵심이다.

2-3. 실무적용방안

먼저 간단하게 살펴보면 아래와 같다.

@GetMapping("/login")
public String login(HttpServletReqeust reqeust) {
	request.login(....);
}

핵심은 HttpServletRequest에 Spring Security가 사용하는 인증객체 정보가 반영이 되어있다는 점이고, 이를 활용하여 인증관련 api(request.login)를 적용할 수 있다는 것이다.

이 프로그래밍 기반 인증처리 시 Filter에서의 formLogin은 더이상 필요가 없다.

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, ApplicationContext context) throws Exception {

        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/user").hasAuthority("ROLE_USER")
                        .requestMatchers("/manager").hasAuthority("ROLE_MANAGER")
                        .requestMatchers("/admin").hasAnyAuthority("ROLE_ADMIN", "ROLE_WRITE")
                        //.anyRequest().authenticated() //secured/jsr보다 더 우선순위
                        .anyRequest().permitAll()
                )
                //.formLogin(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
        ;

        return http.build();
    }

이처럼 formLogin API는 주석처리가 가능해진다.

이를 구체화하여 실무적용방안을 살펴보면 아래 두가지 관점이 유용하겠다.

@GetMapping("/login")
    public String login(HttpServletRequest request, Member member) throws ServletException {
        request.login(member.getUsername(), member.getPassword());
        return "login";
    }

첫번째는 위와 같이 login시 HttpServletRequest를 이용하여, 프로그래밍 방식의 login(request.login)을 진행하는 것.

@GetMapping("/servlet")
    public String users(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        boolean isAuthenticated = request.authenticate(response);

        if(isAuthenticated){
            return "authenticated";
        }

        return "not authenticated";
    }

두번째는 request.authenticate 및 response를 활용하여 인증여부를 프로그래밍 방식으로 확인하는 것.

이 두가지 방식 모두 Sprig Security 기반의 인증객체가 Wrapper 클래스 기반으로 확장되어 Servlet 영역에서 활용할 수 있기에 가능한 동작임을 기억하자.

참고. Servlet3

Servlet3, Servlet3.0 API는 Java EE 6(in 2009)에 도입된 서블릿 명세로, 기존의 web.xml 기반의 설정에서 벗어나 어노테이션 기반의 설정이 프로젝트 전반적으로 가능해졌다는 점이 가장 큰 차이점이다.

즉, Servlet 3.0의 가장 큰 변화는 web.xml 파일 기반 설정에서 벗어나 어노테이션(@WebServlet, @WebFilter 등)이나 자바 설정(Java Config)으로 서블릿, 필터 등을 선언할 수 있게 되어 설정이 훨씬 간결해졌다는 점이며, 이로 인해 web.xml이 필수가 아니게 되거나(선택적 사용), 서블릿 매핑을 XML에 수동으로 작성할 필요 없이 코드에서 바로 관리 가능해진 것이 핵심 차이점이다.

참고. Servlet3 기반 인증기능 제공 컴포넌트 비교

필터의 동작 > factory의 wrapper 클래스 생성 > Wrapper를 통한 기능 확장의 큰 흐름으로 이어진다.

컴포넌트역할API 중심언제 사용인증적 보안 기여
SecurityContextHolderAwareRequestFilterHttpServletRequest를 Spring Security 인증 정보로 확장된 wrapper로 교체doFilterInternal()컨트롤러/서블릿에서 Servlet API 인증 메서드를 사용할 때Servlet 인증 API와 Spring Security 인증 정보 일관성 유지
HttpServlet3RequestFactoryServlet3 기반 Request Wrapper 생성 팩토리create(request, auth)filter가 wrapper를 생성할 때Servlet3 기능 기반 인증 호출을 Spring Security와 연동
Servlet3SecurityContextHolderAwareRequestWrapperServlet API 인증 메서드 재정의getUserPrincipal(), isUserInRole(), login(), authenticate()컨트롤러/필터/JSP에서 Servlet API 인증 사용 시Servlet 인증과 Spring Security 인증 완전 통합

3. AuthenticationPrincipal 어노테이션(Spring MVC)

Spring Security는 AuthenticationPrincipal 어노테이션을 제공하는데, 결론적으로 이는 Spring MVC Argument Resolver 기능을 통해 메소드 파라미터에 Authentication.getPrincipal()을 쉽게 주입하도록 도움을 준다.

위에서 살펴보았던 Servlet 3 인증 API와는 무관하다. 다만,
Spring MVC 레벨에서 Authentication을 편하게 사용하는 도우미의 역할을 해주는 어노테이션이다.

SecurityContextHolder → Authentication → principal 객체
                                     |
                            @AuthenticationPrincipal
                                     ↓
              HandlerMethodArgumentResolver 가 파라미터에 principal 주입

위 과정에서

HandlerMethodArgumentResolver 가 동작하면서 현재 스레드의 SecurityContextHolder.getContext().getAuthentication() 조회 후, 그 중 authentication.getPrincipal() 값을 추출하여 최종적으로 컨트롤러 메소드의 파라미터에 바인딩한다.

이는 기존 principal를 추출하는 과정에서

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
User user = (User) auth.getPrincipal();

꽤 길고 복잡하며, 가독성을 해치는 boilerPlate가 매번 생성되는데

@GetMapping("/me")
public String me(@AuthenticationPrincipal CustomUser user) {
    return user.getName();
}

AuthenticationPrincipal을 통해 손쉽게 principal을 추출할 수 있고, 나아가 테스트가 쉬운 코드로 변환해주어 아키텍칭적인 면에서도 여러 편의성을 제공해준다.

다시 한번 상기하자면, MVC와의 통합을 위해 유의해야할 점은 어노테이션에서 추출하기 위한 객체이름(UserDetails/User) 혹은 프로퍼티가 매개변수로 전달하는 객체명 및 프로퍼티가 반드시 일치해야 한다는 점이다.

@GetMapping("/principal1")
    public User principal(@AuthenticationPrincipal User user) {
        return user;
    }

    @GetMapping("/principal2")
    public String principal(@AuthenticationPrincipal(expression = "username") String user) {
        return user;
    }

위와 같이 어노테이션 적용및 expression을 사용하여 간단하게 인증객체를 추출해올 수 있다.

참고. 인증방식에 따른 getPrincipal 추출정보

로그인 인증 방식에 따라 getPrincipal()을 통해 추출할 수 있는 정보는 각기 다르며, 다만 해당 추출정보가 "인증에 필요한 기초정보"를 포함한다는 것은 변함이 없다.

아래와 같이 다양한 인증방식 상황에서의 principal 추출정보를 정리해보았다.

상황principal 값
Form Login + 기본 UserDetailsServiceUserDetails 객체
Basic Auth문자열(username) 또는 UserDetails
Remember-MeUserDetails
OAuth2 LoginOAuth2User / OidcUser
JWT 커스텀 구현개발자가 정의한 객체
AnonymousAuthenticationToken문자열 "anonymousUser"

4. MVC Asynchroized thread로의 SecurityContext 설정 및 공유 지원

Spring Security는 Spring MVC Controller에서 Callable을 통한 비동기 별도 스레드 생성 및 반환 시, 메인 스레드가 보유하고 있는 ThreadLocal의 Security Context 객체를 해당 스레드에 접근 가능하도록 공유해주도록 지원한다.

이를 위해 WebAsyncManagerIntegrationFilter를 동작하여 SecurityContextCallableProcessingInterceptor를 manager에 등록한다. 이후 비동기 스레드 풀 생성 시, WebAsyncManager가 해당 interceptor를 통해 SecurityContext를 공유하도록 ThreadLocal에 저장한다.

다만, Spring에서 제공하는 @Async, 기타 비동기 기술은 Spring Security와 연관성이 없고 반드시 Callable를 사용해야만 비동기 스레드에 Security Context 공유가 가능하다는 점에 유의한다.

@GetMapping("/callable")
public Callable<Authentication> callble(){
	SecurityContext securityContext = SecurityContextHolder.getContextHolderStrategy().getContext();
    
    return new Callable<Authentication>(){
    public Authentication call() throws Exception{
    	SecurityContext securityContext = SecurityContextHolder.getContextHolderStrategy().getContext();
        Authentication authentication = securityContext.getAuthentication();
        
        return authentication;
    }
}

위와 같이, @Async가 아닌 Callable을 통해 비동기적으로 실행한 스레드로부터 값을 반환하도록 해야 SecurityContext 공유가 가능하며, 위에서 명시한 두 SecurityContext는 본질적으로 동일한 인증객체이다.

5. 다중 Security filter chain

Spring Security는 하나의 filter가 아닌, 여러개의 filter를 등록하여, 특정 요청에 대해서만 특정 filter chian을 적용하게 하거나, 혹은 모든 요청에 대해 항상 특정 security filter chain을 적용하게 할 수 있는 설정을 할 수 있다.

요점은 Security filter chain을 여러 개 생성하여(빈객체), 다중으로 보안 설정을 할 수 있다는 것이다.

기존의 다중 설정은

public class HttpSecurityConfig extends WebSecurity

위와 같이 webSecurity 클래스를 상속한 클래스를 여러 개를 만들어서 다중 설정을 해줄 수 있었다면,

@Configuration
@EnableWebSecurity
public class HttpSecurityConfig{
	@Bean
    @Order(1)
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{
    	http.securityMatcher("/user/**")
        .authorizeHttpRequests(authorize -> authorize.anyRequest().hasRole("ADMIN"))
        .httpBasic(withDefaults());
        
    	return http.build();
    }
    
    @Bean
    public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception{
    	http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()0
        .formLogin(withDefaults());
        
        http.build();
    }
}

지금은 위와 같이, SecurityFilterChain 빈객체를 여러개 등록하여, 각각의 다중 filterChain을 동작한다던가, 특정 요청에 대해서만 filterChain을 적용한다던가 등의 동작을 취할 수 있게 된다.

Order 어노테이션을 활용하여, 최우선적으로 적용할 SecurityFilterChain을 지정해줄 수 있겠다.

5-1. 아키텍처

Spring Security의 filterChainProxy는 다중보안설정한 security filter chain들을 여러개의 httpSecurity 빌더를 통해 등록, 이를 filterChainProxy에 최종 등록한다.

SecurityConfig → 여러 HttpSecurity 빌더 → 여러 SecurityFilterChain → FilterChainProxy → 요청 매칭 후 적합한 FilterChain 실행

좀 더 세부적으로 살펴보자.

securityConfig를 통해 securityFilterChain을 등록하는 과정은 특정 요청 혹은 특정 우선순위에 대한 요청사항을 만족하기 위해, 특정 로직을 가진 SecurityFilterChain 빈객체를 등록하는 것에서 시작한다.

@Bean
SecurityFilterChain apiChain(HttpSecurity http) { ... }

@Bean
SecurityFilterChain adminChain(HttpSecurity http) { ... }

@Bean
SecurityFilterChain defaultChain(HttpSecurity http) { ... }

이후, 해당 filterChain은 HttpSecurity 빌더를 통해 생성되어 최종적으로 SecurityConfig 내에 저장된다.

[SecurityConfig]
      │
      ├─ create HttpSecurity #1  ─→ build() → SecurityFilterChain #1
      │
      ├─ create HttpSecurity #2  ─→ build() → SecurityFilterChain #2
      │
      └─ create HttpSecurity #3  ─→ build() → SecurityFilterChain #3

이때 만들어진 다중 securityFilterChain들은 독립적인 필터 집합을 보유한다.

이후, ApplicationContext에서는 이러한 SecurityFilterChain 목록(securityFilterChains)을 가지게 된다.

ApplicationContext
    ├─ SecurityFilterChain #1  (예: /api/** 전용)
    ├─ SecurityFilterChain #2  (예: /admin/** 전용)
    └─ SecurityFilterChain #3  (fallback/기본)

이 securityFilterChains (빈객체 목록)을 FilterChainProxy에 등록하여, 이후 요청에 매칭되는 security filter chain이 동작하게 된다(보통은 빈객체 등록 순서로 chain이 동작한다).

FilterChainProxy
    ├─ securityFilterChains: [Chain#1, Chain#2, Chain#3]
    └─ 통합된 DelegatingFilterProxy 가 서블릿 필터로 등록됨

5-2. 요청에 대해 security filter chain의 동작과정

만약, 요청이 들어왔다면

[HttpServletRequest]
      ↓
DelegatingFilterProxy (springSecurityFilterChain)
      ↓
FilterChainProxy

기존과 동일하게, DelegatingFilterProxy라는 Servlet 컨테이너 측의 필터에 최초 진입하며, 이에 따라 요청은 Spring Container의 Spring Security filter로 진입하게 된다.

FilterChainProxy
    ├─ Chain #1  (requestMatcher=/api/**)
    │       └─ matches? → true → 이 체인 사용
    ├─ Chain #2  (requestMatcher=/admin/**)
    │       └─ matches? → false
    └─ Chain #3 (anyRequest)
            └─ matches? → false (fallback)

filter에 진입하기 위해, 요청에 매핑된 filter chain proxy을 먼저 탐색하고 진행하며, securityMatcher에 등록되어있는 패턴에 대해서만 security chain 빈객체를 동작한다.

선택된 SecurityFilterChain:
    └─ 그 체인에 등록된 Filter 리스트만 실행됨

최종적으로는, 선택된 1개의 securityFilterChain의 filter를 동작하게 된다.

(apiChain)
    ├─ UsernamePasswordAuthenticationFilter
    ├─ BearerTokenAuthenticationFilter
    ├─ CsrfFilter
    ├─ AuthorizationFilter
    └─ 기타 설정된 필터들

매핑되어 실행된 security filter chain은, 내부 filter들을 동작하기 위해 httpSecurity에 구성된 순서를 따른다.

위 아키텍칭 및 동작과정을 정리하면 아래와 같다.

┌───────────────────────────────────────────────────────┐
│                  SecurityConfig (여러 개)             │
└───────────────────────────────────────────────────────┘
         │                    │                    │
         ▼                    ▼                    ▼
┌────────────────┐   ┌────────────────┐   ┌────────────────┐
│ HttpSecurity #1 │   │ HttpSecurity #2 │   │ HttpSecurity #3 │
└────────────────┘   └────────────────┘   └────────────────┘
         │                    │                    │
         ▼                    ▼                    ▼
┌─────────────────────────┐ ┌────────────────────────┐ ┌──────────────────────────────┐
│ SecurityFilterChain #1  │ │ SecurityFilterChain #2 │ │ SecurityFilterChain #3       │
│   - RequestMatcher #1   │ │   - RequestMatcher #2  │ │   - RequestMatcher(any)      │
│   - Filter List #1      │ │   - Filter List #2     │ │   - Filter List #3           │
└─────────────────────────┘ └────────────────────────┘ └──────────────────────────────┘
                   ↓
         ┌───────────────────────────┐
         │       FilterChainProxy     │
         │   [Chain1, Chain2, Chain3] │
         └───────────────────────────┘
                   ↓  (매칭)
         ┌───────────────────────────┐
         │   선택된 SecurityFilterChain │
         └───────────────────────────┘
                   ↓
         ┌───────────────────────────┐
         │   Filter list 실행         │
         └───────────────────────────┘

이때 각각의 security filter 체인은 서로의 필터나 설정에 간섭하지 않으며, 애초에 단 1개의 security filter만 선택되어 내부의 필터들을 동작시킨다는 것을 유의하자.

6. CustomDSLs

dsl이란 domain specified language, 해당 도메인에서만 제공하는 특징적인 언어를 뜻하는데 쉽게 말해서 추상클래스 및 인터페이스, 빌더패턴 등을 통해 프레임워크 측에서 제공하는 메소드 및 api를 말한다.

나아가, dsl의 구성에는 이러한 메소드 및 api뿐만 아니라 프레임워크가 제공하는 명세를 오버라이드(Override)하는 것까지 포함할 수 있으며, 단순히 프레임워크에서 제공하는 api 및 빌더패턴을 활용하는 것이 아니라 Customized한 빈객체를 등록하고 이를 securityFilterChain에 적용하여 사용하는 것 까지 포함할 수 있다.

6-1. customDsl을 구성하기 위한 아키텍칭

customDsls을 구성하면 필터, 핸들러, 관련 메소드 등을 한 클래스에서 정의하여 처리가 가능해진다.

customDsl을 구현하기 위해서는 AbstractHttpConfigurer라는 추상클래스를 상속받아야 하며, AbstractHttpConfigurer와 HttpSecurityBuilder를 매개변수로 받아 init과 configure 두개의 메소드를 오버라이딩 해야한다.

configurer 내부의 init, configure 메소드는 많이 접한 메소드이지만, 이에 대해 상기할 겸 복습해보도록 한다.

Spring Security는 HttpSecurity를 빌더 패턴(builder pattern)으로 동작시키면서, 최종적으로 FilterChainProxy에 빌더패턴으로 생성한 SecurityFilterChain을 등록한다.

이때 HttpSecurity 내부에는 수많은 “Configurer”들이 등록되며,
각 Configurer는 “필요한 빈 주입 / 설정 초기화 / 필터 추가” 등을 수행하게되며, 이 Configurer들의 공통 부모가 바로 AbstractHttpConfigurer 이다.

이때 Configurer는 init을 통해 상호의존성 및 특정 객체의 등록 등 configure를 위한 사전 준비작업을 하며, configure를 통해 filter를 실질적으로 등록하는 과정을 진행한다.

즉 init() = 준비, configure() = 실제 filter 구성
Spring Security의 모든 Configurer(예: FormLoginConfigurer, LogoutConfigurer)도 이 구조를 따르게 된다.

메소드실행 시점역할의 본질예시
init(HttpSecurity)빌더 초기화 단계Configurer 간 상호 의존성 등록, 공유 객체 등록, 사전 준비 작업SecurityContextRepository 교체, AuthenticationManagerBuilder에 Provider 추가
configure(HttpSecurity)실제 구성 단계Filter 등록, 가로채기 규칙 설정 등 “진짜 설정” 수행addFilterBefore, 요청 매칭 규칙 설정

정리하면 다음과 같다.

  • HttpSecurity.build() 호출
  • 내부적으로 각 Configurer의 init() → configure() 순서로 호출
HttpSecurity
    ├── Configurer1.init()
    ├── Configurer2.init()
    ├── ...
    ├── Configurer1.configure()
    ├── Configurer2.configure()
    └── ...

6-2. customDsls 적용

최종적으로 securityFilterChain에서 http 빌더 시점, 즉

http.with(new MyCustomDsls(), dsl -> dsl.flag(true));

위와 같이 abstractHttpConfigurer를 상속한 customDsls을 with api에 등록해주면, 해당 customDsls에서 정의한 여러 customized api(init/configure)를 진행하게 된다(이전 security에서는 apply였으나 6.x 버전부터 with로 변경).

7. 이중화 설정

서비스 아키텍칭 시 부하 분산, SPOF(단일실패지점으로 인한 연쇄장애발생, Single Point of Failure)를 방지하는 목적으로 흔히 분산환경을 구성하는데, Spring Security도 마찬가지로 분산/이중화 환경에서 지속적인 서비스 제공(인증/인가/세션 관리) 등의 보안 기능을 제공한다.

Spring Security는 특히, 사용자 세션을 분산/이중화 환경에서 안전하게 보관(관리) 및 해당 세션 정보를 공유할 수 있는 메커니즘을 제공하며 대표적으로 redis 분산캐시를 활용할 수 있다.

@Configuration
@EnableRedisHttpSession
public class RedisConfig{
	@Value("${spring.data.redis.host}")
    private String host;
    
    @Value("${spring.data.redis.port}")
    private int port;
    
    @Bean
    public RedisConnectFactory redisConnectionFactory(){
    	return new LeetuceConnectionFactory(host, port);
    }
}

이때 유의해야 할 점은, 세션정보를 공유한다는 의미는 세션을 복사/분산저장한다는 의미가 아니라 "Redis"라는 공유저장소에 세션을 일관되게 저장해놓는다는 의미이다.

Spring Session + Redis는 “복제된 세션”을 서버들끼리 주고받는 구조가 아니라, 세션을 아예 서버가 아닌 Redis에 중앙 집중 저장해 놓고,
모든 서버 인스턴스가 같은 Redis 세션 저장소를 바라보는 구조다.

따라서 "세션이 복사된다"는 개념이 아니라,
"세션의 원본 자체가 Redis에 존재한다"가 정확하고, 어떤 서버가 죽어도 Redis에 있는 세션만 다시 읽으면 끝이다.

7-1. Redis 분산캐시를 활용한 세션 활용과정

사용자가 서버에 최초 요청을 보낸다.

기존 Spring MVC라면 HttpSession이 서버 메모리에 생성되겠지만, @EnableRedisHttpSession을 사용하면 HttpSession이 Redis-backed session으로 대체됨.

즉, HttpSession 구현체가 RedisOperationsSessionRepository를 사용하는 형태로 바뀌게 되며 스프링 내부에서 인증 등을 위해 세션이 생성될때 Spring Session은 세션 데이터를 WAS메모리가 아닌, Redis에 즉시 Save 한다.

spring:session:sessions:<sessionId> → 세션 Attribute Map
spring:session:sessions:expires:<sessionId> → TTL 관리 키

세션에 관리되는 방식은 기존 알고있던 redis 관리방식과 동일하다.

Client ----> [Server A]
             [Server B]
             [Server C]
                 │
                 └── Redis(전체 세션 저장)

만약, 서버 1대가 꺼진다하더라도 redis에서 보유중인 sessionId는 동일하기에, 그대로 해당 session id를 key값으로하는 value를 추출하여 세션을 그대로 사용하기만 하면 된다(자료구조에 따라 달라지겠지만, 보통은 일반적인 map구조를 사용하여 getAttribute 등으로 활용).

요청 쿠키(JSESSIONID) → sessionId 확인
    ↓
Redis에서 spring:session:sessions:<sessionId> 로 조회
    ↓
세션 정보 map 그대로 로드
    ↓
사용자 로그인 상태 유지

참고로 세션 값 변경 시점에도 Redis에 저장된다.

8. 결론

지금까지 Spring Security에서 제공하는 다양한 부가적인 기능과 설정 등에 대해 분석해보았다.

Spring Security는 하나의 거대한 프레임워크로, 생각 이상으로 엄청난 보안기능과 Customized 관련 기능들을 제공하며 여기에 관련된 많은 항목에 대해 직접 설정 및 등록이 가능하다는 것을 알 수 있었다.

나아가, 그만큼 고수준의 유지보수성, 편의성과 함께 이와 동시에 유연성과 확장성을 동시에 제공할 수 있다는 특징까지 체감할 수 있었다.

Spring Security에 대해 깊게 분석하고 알게된 만큼, 향후 실무 적용을 위한 강력한 무기, 경쟁력으로 적극 활용할 수 있도록 해보자.

0개의 댓글