Spring Security에서 필터가 어떻게 동작하는지 알아보자. 진행하기 앞서 아래의 설정을 application.properties에 추가하면 콘솔에서 필터에 관한 정보를 확인할 수 있다.
logging.level.web=trace
logging.level.org.springframework.security=trace 1. FilterChain
먼저 클라이언트가 요청을 보내면 Container가 FilterChain을 생성한다. FilterChain에는 Filter(필터)와 요청을 실행하는 Servlet이 있다.
Servlet에 요청이 전달되기 전에 필터를 순서대로 거치게 된다. 이를 다이어그램으로 보면 다음과 같다.

요청이 오면 Filer 1, Filter 2, …, Filter N 을 거친 후 Servlet에서 실행한다.
아래는 Filter의 doFilter 코드이다.
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 해당 필터에서 작업할 내용 실행
chain.doFilter(request, response);
// 실행이 완료되고 나서 처리
} chain.doFilter(request, response);는 다음 필터로 전송하고 모든 필터를 거쳤다면 Servlet에 전송하게 된다.
2. ApplicationFilterChain
FilterChain은 Servlet Container가 실행하고 스프링이 관여하지 않는다. 따라서 컨테이너가 직접 제공하는 FilterChain을 사용해야 한다.
Tomcat에서는 ApplicationFilterChain으로 제공하고, Jetty에서는 FilterChain으로 제공한다. 현재 Tomcat 10.1를 사용하고 있어 ApplicationFilerChain을 살펴보자.
예시로 다음 컨트롤러 코드를 작성하고 생성되는 필터를 확인해보자.
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
} 먼저 ApplicationFilterChain에서 아래를 차례로 호출한다.
CharacterEncodingFilter: 요청과 응답의 인코딩을 설정한다.
FormContentFilter: HTTP PUT, PATCH, DELETE를 파싱하여 ServletRequest.getParameter*()도 접근 가능하도록 설정한다.
RequestContextFilter: RequestContextAttributes를 현재 스레드에 바인딩해 DispatcherServlet, RequestContextListener등이 동작하도록 한다.

Filter는 order의 오름차순으로 실행이 되고 이는 @Order 어노테이션으로 순서를 지정할 수 있다. 만약 Order를 지정하지 않으면 가장 낮은 우선순위로 배정이 된다. Order가 가질수 있는 가장 낮은 우선순위는 Integer.MAX_VALUE이고 가장 높은 우선순위는 Integer.MIN_VALUE인데 Integer.MIN_VALUE는 CharacterEncodingFilter에 사용되야 해서 사용하지 않는 것이 좋다.
3. DelegatingFilterProxy
Spring에서는 DelegatingFilterProxy를 이용하여 Spring Bean을 Filter로 사용할 수 있도록 한다. Spring Security는 DelegatingFilterProxy를 사용해서 보안 관련 필터를 Servlet에 주입시킨다.
예시로 Spring Security를 불러오면 기본적으로 사용하는 FilterChain을 살펴보자.
@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();
} @Order(SecurityProperties.BASIC_AUTH_ORDER)는 가장 마지막에 실행하겠다고 지정하고, http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()) 모든 요청은 인증이 돼있으면 실행 가능하도록 지정했다.
여기에 formLogin을 사용하여 UsernamePasswordAuthenticationFilter에서 인증할 수 있도록 지정했다.
필터의 가장 마지막에는 AuthorizationFilter가 있어 인증이 완료된 필터는 인가하는 필터가 있다.
이를 다이어그램으로 보면 다음과 같다.

4. Spring Security 인증 절차
Spring security에는 SecurityContext가 인증을 위한 정보를 담아 인정 정보를 확인한다.
SecurityContext에는 Authentication 인터페이스가 있고 Principal, Credentials, Authorities 3가지 정보를 담는다.
Principal은 어떤 유저를 인증할지, Credentials은 해당 유저의 인증 정보, Authorities는 해당 유저에게 어떤 권한을 줘야하는지를 나타낸다.
스프링에 순서대로 어떻게 되는지 정리해 보았다.
- SecurityFilterChain
SecurityFilterChain에 지정된 요청이 로그인 요청인지 감지하고, 로그인 요청이 맞다면 인증되지 않은 Authentication 객체를 생성한다.
- AuthenticationManager
AuthenticationManager는 Authentication 객체를 불러와 List<AuthenticationProvider>에 지정된 인증과정을 실행한다. 인증이 성공적으로 되었다면 SecurityContext를 저장한다.
- SecurityContextHolder
앱 전체에 사용자 정보를 불러올 수 있는 SecurityContextHolder에 SecurityContext를 저장한다.
4.1 Api-key 인증 필터 추가 예시
아래 api-key 인증 방식으로 구현하면서 새로운 필터를 어떻게 추가할지 알아보자. 아래 코드는 서버에 저장되어있는 키와 요청의 헤더에 있는 키가 일치하면 다음 필터로 SecurityContext를 전달하는 코드이다.
@Slf4j
public class ApiKeyFilter implements Filter {
private final String key;
public ApiKeyFilter() {
this.key = Base64.getEncoder()
.encodeToString("test".getBytes(StandardCharsets.UTF_8));
log.info("Api Key : "+this.key);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String token = req.getHeader("x-api-key");
if(token == null || !token.equals(this.key)) {
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new TestingAuthenticationToken(
"username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
chain.doFilter(req, res);
}
} 여기서 TestAuthenticationToken은 생성할 때 인증이 완료되어 있다.
아래는 SecurityFilterChain에 ApiKeyFiler를 추가하는 코드이다. /api/**로 들어오는 모든 요청은 인증되어야 하고, 인증은 ApiKeyFilter를 인가 필터 전에 사용하도록 지정했다.
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER - 2)
SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http.securityMatchers((matcher)-> matcher.requestMatchers("/api/**"));
http.authorizeHttpRequests((req) -> req.anyRequest().authenticated());
http.addFilterBefore(new ApiKeyFilter(), AuthorizationFilter.class);
return http.build();
} Reference
- https://docs.spring.io/spring-boot/reference/web/servlet.html#web.servlet.embedded-container
- https://docs.spring.io/spring-security/reference/servlet/architecture.html
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/CharacterEncodingFilter.html?utm_source=chatgpt.com
- https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/RequestContextFilter.html
- https://github.com/nlinhvu/spring-security-servlet-2024
- https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html