SecurityFilterChain과 SecurityWebFilterChain에 대해서
- 서블릿 기반(Spring MVC)과 리액티브 기반(WebFlux)에서 Spring Security가 어떻게 동작하는가
- 두 플랫폼 각각의 요청 처리 모델, 보안 필터 체인, 세션/토큰, CSRF 처리 차이
- 간단한 디렉토리 구조와 설정 코드를 통해 두 플랫폼의 차이 비교
- 프로젝트 성격에 맞춰 플랫폼 선택 기준을 세우고, 체크리스트로 적용 여부 결정하기
서블릿 기반(Spring MVC)에서의 Spring Security
1. 개념
서블릿 기반 애플리케이션은 동기, 블로킹 I/O 모델로 동작한다
요청이 들어오면 요청당 스레드(Per-Request Thread)가 할당되어 컨트롤러까지 진행한다
Spring Security는 이 경로 앞단에 FilterChainProxy를 두고, 다수의 서블릿 필터를 순서대로 통과시키며 인증, 인가, 예외처리, CSRF 등을 수행한다
* 필터 체인 - > 컨트롤러 순이며, 모든 요청이 필터를 반드시 거친다
2. 인증 흐름(폼 로그인 예)
세션을 사용하는 경우, 인증 성공 후 서버 세션에 Authentication 저장
- > 이후 요청은 세션 아이디로 사용자를 식별함
3. 인가 처리(요청 경로 및 메서드 보안)
요청 경로 기반 : authorizeHttpRequest()에서 경로별 권한 지정
메서드 기반 : @PreAuthorize("hasRole('ADMIN')") 등으로 서비스 계층에서 2차 검증을 함
4. 예제(최소 설정)
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-h2console'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}
SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(login -> login
/**
* loginPage 메서드를 집어넣음으로 login 컨트롤러와 정적페이지를 만들어줘야함
*/
.loginPage("/login").permitAll()
.defaultSuccessUrl("/", true))
.logout(logout -> logout
.logoutUrl("/logout"));
return http.build();
}
@Bean
UserDetailsService users() {
UserDetails user = User.withUsername("user")
.password("{noop}pass")
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}pass")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
HomeController.java
@Controller
public class HomeController {
@GetMapping("/")
@ResponseBody
public String home() {
return "home";
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/panel")
@ResponseBody
public String admin() {
return "admin";
}
}
LoginController.java
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
5. 세션, CSRF, 예외 처리
- 세션 : 기본은 세션 기반임. 토큰 기반과 병행시 세션 생성 정책 조정 필요
- CSRF : 상태 유지형 POST 등에서 보호가 필요함. REST API만 제공하면 비활성할 수 있으나, 폼 제출이 있다면 반드시 활성 후 토큰을 포함하기
- 예외처리 : 인증 실패 - > AuthenticationEntryPoint / 인가 실패 -> AccessDeniedHandler
6. 성능, 확장성 특징
- 요청당 스레드 고정으로 코드 단순성이 높다
- 높은 동시성에서는 스레드 풀이 병목될 수 있어 캐싱, 커넥션, 풀, 쿼리 최적화가 중요하다
- 이커머스 대규모 할인 이벤트
- 예 : 네이버 쇼핑, 쿠팡, 무신사에서 블랙프라이데이 이벤트 오픈 시간(00:00)에 수십만 명이 동시에 특정 상품 페이지를 새로고침 하거나 주문 API를 호출한다
- 결과 : 상품 재고 조회 DB 쿼리문이 동시에 폭주함 -> DB 커넥션 풀이 부족해짐 -> 스레드가 대기 상태로 묶인다
- 공공기관 원서 접수 / 선착순 예약 서비스
- 예 : 대학 수강신청, 코로나 백신 예약, 콘서트 예매 등(인터파크, Yes24)
- 수만 건의 요청이 "한 시점"에 집중됨 -> 요청당 스레드 모델에서는 수천 개 스레드가 동시에 생성됨
-> CPU Context Switching 폭증
- 이커머스 대규모 할인 이벤트
7. 디버깅 체크리스트
- 필터 순서 오해로 인한 우회 허용 여부 확인하기
- 경로 매칭 우선순위 점검하기(/admin/** 앞에 과도한 permitAll() 배치 금지)
- 세션 정책과 CSRF 정책 상충 여부 확인하기
| 항목 | 내용 |
| 처리 모델 | 동기 / 블로킹, 요청당 스레드 |
| 보안 핵심 | FilterChainProxy + SecurityFilterChain |
| 주 사용처 | 전통적 웹, 서버렌더링, 일반 REST API |
| 강점 | 단순한 흐름, 풍부한 레거시 호환 |
| 주의 | 동시성 한계, 필터 순서 / 경로 매칭 실수 |
리액티브 기반(WebFlux)에서의 Spring Security
1. 개념
리액티브(WebFlux)는 비동기, 논블로킹 I/O와 리액티브 스트림 모델로 동작한다
요청은 이벤트 루프에 등록되어 스레드를 점유하지 않고 파이프라인을 타고 흐른다.
보안은 SecurityFilterChain(리액티브 웹 필터 체인)으로 처리된다
2. 인증 흐름(리액티브 라우터 예)
3. 예제(최소 설정)
build.gradle
// 서블릿 기반(Spring MVC)
// implementation 'org.springframework.boot:spring-boot-starter-webmvc'
// 리액티브 기반(WebFlux)
implementation 'org.springframework.boot:spring-boot-starter-webflux'
SecurityConfig.java
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
//@EnableMethodSecurity
public class SecurityConfig {
/**
* 리액티브(WebFlux) 기반 SecurityFilterChain
*/
@Bean
SecurityWebFilterChain webfluxChain(ServerHttpSecurity http) {
return http
.csrf(csrf -> csrf.disable())
.authorizeExchange(ex -> ex
.pathMatchers("/","/login","/public/**").permitAll()
.pathMatchers("/admin/**").hasRole("ADMIN")
.anyExchange().authenticated()
)
.formLogin(form -> form // 세션 기반 로그인 유지라서 브라우저에 인증 정보가 남아있음
.loginPage("/login")
)
.httpBasic(Customizer.withDefaults()) // 매 요청마다 Authorization 헤더로 인증을 보내는 형태임
.build();
}
@Bean
MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}pass")
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}pass")
.roles("ADMIN")
.build();
return new MapReactiveUserDetailsService(user, admin);
}
HomeController.java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Mono;
@Controller
public class HomeController {
@GetMapping("/")
@ResponseBody
public Mono<String> home() {
return Mono.just("home");
}
/**
* 리액티브(WebFlux) 기반 FilterChain을 사용할 때
* FilterChain에 formLogin()과 httpBasic()을 둘 다 사용할때
* 403이 뜰 수 있다
*/
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/panel")
@ResponseBody
public Mono<String> admin() {
return Mono.just("admin");
}
}
LoginController.java
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Mono;
@Controller
public class LoginController {
@GetMapping("/login")
public Mono<String> login() {
return Mono.just("login");
}
@GetMapping("/me")
@ResponseBody
public Mono<String> me(Authentication authentication) {
if (authentication == null) {
return Mono.just("anonymous");
}
return Mono.just(authentication.getName() + " / " + authentication.getAuthorities());
}
}
4. 세션 / CSRF / 백프레셔
- 세션 : 기본적으로 WebSession 기반임. 토큰 기반과 혼용시 ServerSecurityContextRepository 전략 검토하기
- CSRF : 폼 제출 등 상태 유지형 요청에 필요함. API만 제공하면 비활성이 가능하나, 운영에서는 경로별 분리 권장
5. 성능, 확장성 특징
- 이벤트 루프 기반으로, 적은 스레드로 높은 동시성 처리 가능
- 블로킹 I/O 호출(전통 JDBC 등)이 섞이면 이점이 상쇄되므로 리액티브 스택 전반(DB, 외부 API)을 고려해야한다
6. 디버깅 체크리스트
- 블로킹 호출 존재 여부 점검하기(리액터 스레드에서 블로킹 호출 금지)
- pathMatchers 순서와 권한 맵핑 확인하기
- 메서드 보안에서 Mono<Boolean> 조건을 사용할 때 비동기 평가 시점 주의하기
| 항목 | 내용 |
| 처리 모델 | 비동기 / 논블로킹, 이벤트 루프 |
| 보안 핵심 | SecurityWebFilterChain + 리액티브 필터 |
| 주 사용처 | 스트리밍, IoT, 대규모 동시 연결 등 |
| 강점 | 높은 동시성, 효율적 리소스 사용 |
| 주의 | 전 스택 리액티브 요구(MVC와 섞어쓰면 X) 디버깅 난이도 |
플랫폼 선택과 심화 비교
1. 선택 가이드
- 팀 역량과 기존 자산 : MVC 경험이 많고 JDBC 위주라면 서블릿이 유리함
- 문제 성격 : 다수 장기 연결, 스트리밍, WebSocket 중심이라면 리액티브가 유리함
- 외부 의존성 : 호출 대상 API/DB가 블로킹이라면 서블릿이 단순할 수 있다
2. 비교 표
| 항목 | 서블릿(Spring MVC) | 리액티브(WebFlux) |
| 실행 모델 | 요청당 스레드 | 이벤트 루프, 논블로킹 |
| 필터 체인 | Servlet Filter 기반 | WebFilter 기반 |
| 보안 체인 빈 | SecurityFilterChain | SecurityWebFilterChain |
| 인증 저장 | HttpSession에 SecurityContext | WebSession 또는 토큰 컨텍스트 |
| CSRF | CsrfFilter | CsrfWebFilter |
| 메서드 보안 | @PreAuthorize 즉시 평가 | 리액티브 컨텍스트에서 비동기 평가 |
| 적합한 업무 | 전통적 웹, 관리 백오피스 | 스트리밍, 실시간 알림, IoT |
| 학습 난이도 | 낮음 | 높음 |
| 디버깅 | 익숙함 | 체계가 필요함(리액터 툴) |
* CSRF에 대해서
SecurityFilterChain은 그냥 CsrfFilter임
그런데 CsrfWebFilter의 경우 implements로 WebFilter를 포함한다
public class CsrfWebFilter implements WebFilte
SecurityFilterChain, SecurityWebFilterChain 모두 똑같은 이름의 csrf 메서드를 사용하지만
.csrf(csrf -> csrf.disable())
CsrfSpec은 '아예' 다르다. 참고
적용 체크리스트
- 경로별 보안 규칙 분리(/public/**, /api/**, /admin/**)
- 인증 저장 전략 결정하기(세션 vs 토큰)
- CSRF 전략 수립(폼 여부, API 분리)
- 예외 처리 표준화하기(인증 실패 / 인가 실패에 대한 응답)
- 로그, 감사 추적하기(SecurityContext, 사용자 ID)
- 부하 테스트로 스레드 / 이벤트 루프 병목 점검하기
- 외부 통신의 블로킹 / 논블로킹 여부 정리하기
'Spring Boot > Security' 카테고리의 다른 글
| 필터 아키텍처 : 필터 순서와 우선순위 (1) | 2026.04.21 |
|---|---|
| 필터 아키텍처 : 주요 보안 필터 (0) | 2026.03.31 |
| 필터 아키텍처 : Spring Security 필터 구조 (0) | 2026.03.30 |
| 필터 아키텍처 : 서블릿 필터 기본 개념 (0) | 2026.03.30 |
| Security : Spring Security (0) | 2026.03.26 |