2024. 2. 25. 23:04ㆍSpring Security
이번 장에서는 다음을 다룹니다
- Working with the filter chain
- Defining custom filters
- Using Spring Security classes that implement the Filter interface
스프링 시큐리티에서 HTTP 필터는 HTTP 요청에 적용되어야 하는 다양한 책임을 처리하는 중요한 역할을 합니다. 이러한 필터들은 각각 특정한 보안 관련 작업을 담당하며, 요청이 서버로 들어오면 이 필터들을 통과하게 됩니다. 이 과정에서 각 필터는 자신이 담당하는 특정 작업을 수행합니다. 예를 들어, 인증, 권한 부여, CSRF 보호 등이 이에 해당할 수 있습니다.
필터들은 필터 체인(filter chain)을 형성하여 순차적으로 작동합니다. 이 체인 구조는 요청이 들어올 때마다 순차적으로 각 필터를 거치며 필요한 보안 절차를 수행하게 합니다. 하나의 필터가 자신의 로직을 실행한 후, 그 결과에 따라 요청을 다음 필터로 넘기거나, 필요한 경우 요청 처리를 중단하고 응답을 반환할 수도 있습니다.
예를 들어, 인증 필터는 들어오는 요청에서 사용자의 신원을 확인하고 인증합니다. 만약 사용자가 성공적으로 인증된다면, 요청은 체인을 따라 다음 필터로 이동합니다. 그 다음 필터는 권한 부여 필터일 수 있으며, 이 필터는 사용자가 요청한 작업을 수행할 권한이 있는지 확인합니다. 모든 필터가 해당 요청에 대해 자신의 책임을 다한 후, 요청은 최종적으로 애플리케이션의 나머지 부분으로 진행될 수 있습니다.
이러한 방식으로, 스프링 시큐리티는 애플리케이션에 대한 보안 요구 사항을 모듈화하고, 각각의 보안 관련 작업을 잘 정의된 필터로 관리할 수 있게 합니다. 이는 보안 로직을 애플리케이션의 나머지 부분에서 분리함으로써, 보안 관련 코드를 더욱 체계적으로 관리할 수 있게 해줍니다.

그림 5.1 필터 체인이 요청을 받습니다. 각 필터는 관리자를 사용하여 요청에 특정 로직을 적용하고, 결국 체인을 따라 다음 필터에게 요청을 더 위임합니다.

그림 5.2 공항에서는 비행기에 탑승하기까지 여러 필터 체인을 거칩니다. 마찬가지로, 스프링 시큐리티에는 애플리케이션이 받는 HTTP 요청에 작용하는 필터 체인이 있습니다.
공항에 가는 것을 예로 들어 봅시다. 터미널에 들어서서 비행기에 탑승하기까지, 여러분은 여러 단계의 필터를 거칩니다(그림 5.2 참조). 먼저 여러분은 티켓을 제시하고, 그 다음 여권이 검증되며, 그 후에 보안 검사를 받습니다. 공항 게이트에서는 더 많은 "필터"가 적용될 수 있습니다. 예를 들어, 경우에 따라 탑승 직전에 여권과 비자가 다시 한 번 검증됩니다. 이는 스프링 시큐리티의 필터 체인에 대한 훌륭한 비유입니다. 마찬가지로, 스프링 시큐리티에서는 HTTP 요청에 작용하는 필터 체인을 커스터마이즈합니다. 스프링 시큐리티는 커스터마이즈를 통해 필터 체인에 추가할 수 있는 필터 구현을 제공하지만, 사용자 정의 필터를 정의할 수도 있습니다.
이 장에서는 웹 애플리케이션의 스프링 시큐리티 인증 및 권한 부여 아키텍처의 일부인 필터를 어떻게 커스터마이즈할 수 있는지에 대해 논의할 것입니다. 예를 들어, 사용자에게 이메일 주소 검증이나 일회용 비밀번호 사용과 같은 추가 단계를 통해 인증을 강화하고 싶을 수 있습니다. 또한, 인증 이벤트에 대한 감사 기능을 추가하는 것도 가능합니다. 애플리케이션에서 인증 감사를 사용하는 다양한 시나리오를 찾을 수 있습니다: 디버깅 목적에서부터 사용자 행동 식별에 이르기까지. 현재 기술과 머신러닝 알고리즘을 사용하면, 예를 들어 사용자 행동을 학습하여 누군가가 그들의 계정을 해킹했거나 사용자를 사칭했는지 알아내는 등 애플리케이션을 개선할 수 있습니다.
HTTP 필터 체인의 역할을 커스터마이즈하는 방법을 아는 것은 소중한 기술입니다. 실제로, 애플리케이션은 디폴트 설정만으로는 더 이상 작동하지 않는 다양한 요구 사항을 가지고 있습니다. 체인의 기존 구성 요소를 추가하거나 교체해야 할 수도 있습니다. 디폴트 구현에서는 HTTP Basic 인증 방식을 사용하여 사용자 이름과 비밀번호에 의존합니다. 하지만 실제 시나리오에서는 이보다 더 많은 것이 필요한 경우가 많습니다. 다른 인증 전략을 구현해야 할 수도 있고, 권한 부여 이벤트에 대해 외부 시스템에 알려야 할 수도 있으며, 단순히 추적 및 감사[auditing]에 사용되는 성공적인 또는 실패한 인증을 로깅해야 할 수도 있습니다(그림 5.3 참조). 어떤 시나리오든, 스프링 시큐리티는 필요에 딱 맞게 필터 체인을 모델링할 수 있는 유연성을 제공합니다.

그림 5.3 기존 필터 앞, 뒤 또는 기존 위치에 새 필터를 추가하여 필터 체인을 사용자 정의할 수 있습니다. 이러한 방식으로 인증은 물론 요청 및 응답에 적용되는 전체 프로세스를 커스터마이징을 할 수 있습니다.
(스프링 보안에서 제공하는 필터 외에 체인에 사용자 정의 필터를 추가할 수 있습니다.)
5.1 Implementing filters in the Spring Security architecture
이 섹션에서는 Spring Security 아키텍처에서 필터와 필터 체인이 작동하는 방식에 대해 논의합니다. 이번 장의 다음 섹션에서 작업하는 구현 예제를 이해하기 위해서는 먼저 이 일반적인 개요가 필요합니다. 2장과 3장에서 배운 바와 같이, 인증 필터는 요청을 가로채고 인증 역할을 인가[Authorizaiton] 관리자에게 떠 넘깁니다. 인증 전에 특정 로직을 실행하고 싶다면 인증 필터 앞에 필터를 삽입함으로써 이를 수행할 수 있습니다. Spring Security 아키텍처의 필터들은 일반적인 HTTP 필터입니다. jakarta.servlet 패키지의 Filter 인터페이스를 구현함으로써 필터를 생성할 수 있습니다. 다른 HTTP 필터와 마찬가지로, doFilter() 메소드를 오버라이드하여 그 로직을 구현해야 합니다. 이 메소드는 ServletRequest, ServletResponse 및 FilterChain을 파라미터로 받습니다.
- ServletRequest—HTTP 요청을 나타냅니다. ServletRequest 객체를 사용하여 요청에 대한 세부 정보를 검색합니다.
- ServletResponse—HTTP 응답을 나타냅니다. ServletResponse 객체를 사용하여 응답을 클라이언트에게 돌려보내기 전에 응답을 변경합니다.
- FilterChain—필터의 연쇄를 나타냅니다. FilterChain 객체를 사용하여 요청을 체인의 다음 필터로 전달합니다.
참고로, Spring Boot 3부터 Jakarta EE가 기존의 Java EE 사양을 대체하게 됩니다. 이 변경으로 인해 일부 패키지의 접두사가 javax에서 jakarta로 변경된 것을 볼 수 있습니다. 예를 들어, Filter, ServletRequest, ServletResponse와 같은 타입은 이전에는 javax.servlet 패키지에 있었지만, 이제는 jakarta.servlet 패키지에서 찾을 수 있습니다.
charitalics 필터 체인은 정의된 순서대로 작동하는 필터들의 모음을 나타냅니다. Spring Security는 우리를 위해 몇 가지 필터 구현과 그 순서를 제공합니다. 제공된 필터들 중에서:
- BasicAuthenticationFilter는 HTTP 기본 인증을 담당합니다(해당되는 경우).
- CsrfFilter는 사이트 간 요청 위조(CSRF) 보호를 담당합니다. 이 내용은 9장에서 논의할 예정입니다.
- CorsFilter는 교차 출처 리소스 공유(CORS) 권한 규칙을 담당합니다. 이 내용도 10장에서 논의할 예정입니다.
Charitalics는 소프트웨어 개발에서 설정보다 관례(Convention over Configuration, CoC) 개념에 대한 단어 장난으로 보입니다. Spring Boot와 같은 환경에서 복잡한 구성 없이 디폴트 값을 제공하여 개발자가 반복적인 구성 작업을 줄이고, 애플리케이션의 고유한 로직에 집중하도록 돕는 접근법을 의미합니다.
여러분이 직접 코드에서 이 필터들을 직접 다룰 일은 거의 없기 때문에 모든 필터를 알 필요는 없지만, 필터 체인이 어떻게 작동하는지 이해하고 몇 가지 구현에 대해서는 알고 있어야 합니다. 이 책에서는 우리가 논의하는 여러 주제에 필수적인 필터들만 설명합니다.
응용 프로그램이 반드시 이 모든 필터의 인스턴스를 체인에 가지고 있지는 않다는 것을 이해하는 것이 중요합니다. 체인의 길이는 어떻게 응용 프로그램을 구성하느냐에 따라 길어지거나 짧아집니다. 예를 들어, 2장과 3장에서 HTTP Basic 인증 방식을 사용하려면 HttpSecurity 클래스의 httpBasic() 메소드를 호출해야 한다는 것을 배웠습니다. httpBasic() 메소드를 호출하면 BasicAuthenticationFilter의 인스턴스가 체인에 추가됩니다. 마찬가지로, 작성하는 구성에 따라 필터 체인의 정의가 영향을 받습니다. 새로운 필터를 체인에 다른 필터에 상대적으로 추가할 수 있습니다(그림 5.4 참조). 또는 알려진 필터 전에, 후에, 또는 그 위치에 필터를 추가할 수 있습니다. 각 위치는 사실상 인덱스(숫자)이며, 순서로도 언급될 수 있습니다.

그림 5.4 각 필터는 순서 번호를 가지고 있습니다. 이것은 필터가 요청에 적용되는 순서를 결정합니다. Spring Security가 제공하는 필터와 함께 사용자 정의 필터를 추가할 수 있습니다.
Spring Security가 제공하는 필터와 이러한 필터가 구성된 순서에 대한 자세한 내용을 알고 싶다면, org.springframework.security.config.annotation.web.builders 패키지의 FilterOrderRegistration을 살펴볼 수 있습니다. 같은 위치에 두 개 이상의 필터를 추가할 수 있습니다(그림 5.5 참조). 5.4절에서는 개발자들 사이에서 흔히 혼란을 일으키는, 이러한 상황이 발생할 수 있는 일반적인 경우를 만나게 될 것입니다.

참고: 여러 필터가 동일한 위치에 있을 경우, 그들이 호출되는 순서는 정의되어 있지 않습니다.
위 이미지에 확인할 수 있는 각 필터들의 정의는 다음과 같습니다.
- DisableEncoderUrlFilter : DisableEncodeUrlFilter는 Spring Security에서 제공하는 필터로, URL을 HttpServletResponse로 인코딩하는 기능을 비활성화하여 URL에 세션 ID가 포함되는 것을 방지하는 역할을 합니다. URL에 세션 ID가 포함되면 보안 위험이 발생할 수 있으며, 예를 들어 HTTP 접근 로그에 노출되어 세션 ID가 유출될 가능성이 있습니다. 이 필터는 Spring Security 5.7부터 도입되었습니다
- ForceEagerSessionCreationFilter :ForceEagerSessionCreationFilter는 Spring Security의 필터로, 요청이 들어왔을 때 세션이 아직 생성되지 않은 경우 즉시 HttpSession을 생성하도록 강제하는 기능을 수행합니다. 이는 일반적으로 인증 과정이나 보안 컨텍스트에서 세션이 필요할 때 사용됩니다. 기본적으로 세션은 필요할 때까지 생성되지 않지만, 이 필터는 세션이 미리 준비되도록 함으로써, 이후 요청에서 세션에 의존하는 보안 처리를 보장합니다. 이 필터는 Spring Security 5.7에서 도입되었으며, 보안 컨텍스트가 세션에 저장되도록 요구되는 경우 특히 유용합니다. 예를 들어, 인증 정보를 지속해서 참조해야 하는 상황에서 세션을 일찍 생성해둠으로써, 응답을 통해 전달된 세션 정보의 누락을 방지할 수 있습니다.
- ChannelProcessingFilter :ChannelProcessingFilter는 Spring Security에서 요청이 특정 전송 채널(예: HTTP 또는 HTTPS)을 통해 전달되도록 보장하는 필터입니다. 이 필터는 요청을 가로채고, 정의된 규칙에 따라 요청이 HTTPS와 같은 안전한 채널에서만 이루어지도록 설정합니다. 일반적인 사용 사례는 로그인 페이지나 민감한 데이터가 오가는 페이지에서 HTTPS를 강제하여 보안을 강화하는 것입니다.이 필터는 안전하지 않은 요청을 안전한 채널로 리다이렉트하거나, 요구된 채널이 아닌 경우 요청 처리를 거부하도록 할 수 있습니다. 이를 통해 혼합된 HTTP/HTTPS 요청을 사용하는 애플리케이션에서도 특정 경로만 HTTPS로 강제할 수 있는 유연성을 제공합니다. ChannelProcessingFilter는 내부적으로 ChannelDecisionManager와 FilterInvocationSecurityMetadataSource를 통해 요청이 올바른 채널에서 처리되는지 확인합니다. 예를 들어, /secure 경로나 /login 페이지에 대해 HTTPS를 요구하고, 다른 경로는 HTTP로 유지할 수 있습니다. 설정 방식으로는 Java 설정에서 requiresChannel() 메서드를 사용하거나 XML 설정에서 requires-channel="https" 속성을 사용하여 구현할 수 있습니다.
- WebAsyncManagerIntegrationFilter :WebAsyncManagerIntegrationFilter는 Spring Security에서 비동기 요청 처리와 보안 컨텍스트의 통합을 위한 필터입니다. 이 필터는 비동기 요청에서 SecurityContext를 유지하기 위해 WebAsyncManager와 SecurityContext 간의 연계를 제공합니다.WebAsyncManagerIntegrationFilter는 SecurityContextCallableProcessingInterceptor를 통해 비동기 작업에 Callable 인터셉터를 추가하여 SecurityContext를 적절히 설정합니다. 이 설정을 통해 비동기 요청에서 사용자 인증 및 권한 설정이 유지되도록 보장합니다. 이를 통해 비동기 작업이 실행되는 동안 인증된 사용자의 보안 컨텍스트를 그대로 유지하여, 비동기 요청의 보안 설정이 일관되게 적용됩니다. 예를 들어, 비동기 작업을 처리할 때도 현재 사용자의 인증 정보가 유지되도록 하여, 보안이 필요한 비동기 요청에서도 동일한 사용자 권한이 적용됩니다.
- SecurityContextHolderFilter :SecurityContextHolderFilter는 Spring Security의 필터로, 요청이 처리되는 동안 SecurityContext를 SecurityContextRepository에서 가져와 SecurityContextHolder에 설정하는 역할을 합니다. 이 필터는 Spring Security 6에서 도입되었으며, 이전 버전에서 사용하던 SecurityContextPersistenceFilter와 유사한 기능을 수행하지만, 좀 더 명시적인 설정을 요구합니다.이 필터는 주로 세션 기반이 아닌 OAuth2와 같은 인증 방법에서도 보안 컨텍스트를 일관되게 관리하는 데 유용하며, 필요에 따라 보안 상태의 저장을 조정할 수 있습니다. 특히, 이 필터는 요청이 완료된 후에 SecurityContext를 자동으로 저장하지 않으며, 저장이 필요한 경우 개발자가 명시적으로 SecurityContextRepository.saveContext() 메서드를 호출해야 합니다. 이 방식은 SecurityContext를 필요할 때만 저장함으로써 성능을 향상시키고, 여러 인증 메커니즘이 선택적으로 SecurityContext를 저장할 수 있는 유연성을 제공합니다. 또한, shouldNotFilterErrorDispatch 메서드를 통해 오류 발생 시 필터를 비활성화할 수도 있어, 특정 상황에서 SecurityContext가 필요하지 않을 때도 유연하게 사용할 수 있습니다.
- LogoutFilter :LogoutFilter는 Spring Security의 필터 중 하나로, 로그아웃 요청을 처리하고 필요한 후속 작업을 수행합니다. 주로 /logout URL에 대해 작동하며, 이 필터는 요청을 받으면 다양한 LogoutHandler를 호출하여 세션 무효화, 보안 컨텍스트 제거, 쿠키 삭제 등의 로그아웃 관련 작업을 수행합니다.또한 LogoutFilter는 로그아웃 URL을 커스터마이징할 수 있으며, 예를 들어 logoutUrl("/custom-logout")과 같이 설정하여 기본 /logout 대신 특정 URL에서 로그아웃을 처리할 수 있습니다. Spring Security는 로그아웃이 발생할 때 LogoutSuccessEvent도 발행할 수 있어, 필요에 따라 로그아웃 성공 이벤트를 처리하는 핸들러를 추가할 수 있습니다.
- 주요 LogoutHandler 중 하나인 SecurityContextLogoutHandler는 기본적으로 HTTP 세션을 무효화하고, SecurityContext를 지우며, CSRF 토큰을 삭제하는 등의 작업을 합니다. 로그아웃이 완료되면 LogoutSuccessHandler를 통해 지정된 URL로 리다이렉션하거나, 필요한 경우 사용자 정의 성공 처리기를 통해 추가 작업을 수행할 수 있습니다.

그림 5.5 체인에서 동일한 순서 값을 가진 여러 필터를 가질 수 있습니다. 이 경우, Spring Security는 그들이 호출되는 순서를 보장하지 않습니다.
5.2 Adding a filter before an existing one in the chain
이 섹션에서는 필터 체인에서 기존 필터보다 앞서 사용자 정의 HTTP 필터를 적용하는 방법에 대해 논의합니다. 이것이 유용한 시나리오를 발견할 수도 있습니다. 실제적인 방법으로 접근하기 위해, 우리는 예제를 위한 프로젝트에서 작업할 것입니다. 이 예제를 통해, 사용자 정의 필터를 구현하고 필터 체인에서 기존 필터보다 앞서 적용하는 방법을 쉽게 배울 수 있습니다. 그런 다음 이 예제를 생산 애플리케이션에서 발견할 수 있는 유사한 요구 사항에 적응할 수 있습니다.
첫 번째 사용자 정의 필터 구현을 위해, 사소한 시나리오를 고려해 보겠습니다. 모든 요청이 Request-Id라는 헤더를 포함하고 있는지 확인하고 싶습니다(프로젝트 ssia-ch5-ex1 참조). 우리의 애플리케이션은 이 헤더를 요청 추적용으로 사용하며, 이 헤더는 필수적이라고 가정합니다. 동시에, 애플리케이션이 인증을 수행하기 전에 이러한 가정을 검증하고 싶습니다. 인증 과정은 데이터베이스 조회나 기타 리소스를 소모하는 행동을 포함할 수 있는데, 요청 형식이 유효하지 않은 경우 애플리케이션이 이러한 작업을 수행하지 않도록 하고 싶습니다. 이것을 어떻게 할까요? 현재 요구 사항을 해결하는 것은 단 두 단계만 거치면 되며, 마지막에 필터 체인은 그림 5.6과 같습니다:
1. RequestValidationFilter 클래스를 구현하여 요청에 필요한 헤더가 있는지 확인하는 필터를 만드세요.
2. 필터 체인에 필터를 추가하세요. 이를 구성 클래스에서 configure() 메서드를 재정의하여 수행하세요.

그림 5.6 우리 예제를 위해, 인증 필터[BasicAuthenticationFilter]보다 먼저 작동하는 RequestValidationFilter를 추가합니다. RequestValidationFilter는 요청의 유효성 검증이 실패하면 인증이 발생하지 않도록 보장합니다. 우리의 경우, 요청은 Request-Id라는 필수 헤더를 포함해야 합니다.
1단계를 수행하기 위해, 필터를 구현하기 위해 사용자 정의 필터를 정의합니다. 다음 리스팅은 구현을 보여줍니다.
Listing 5.1 Implementing a custom filter
public class RequestValidationFilter
implements Filter { #A
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain)
throws IOException, ServletException {
// ...
}
}
#A 필터를 정의하기 위해, 이 클래스는 Filter 인터페이스를 구현하고 doFilter() 메소드를 오버라이드합니다.
doFilter() 메소드 내부에서, 우리는 필터의 로직을 작성합니다. 우리의 예제에서, 우리는 Request-Id 헤더가 존재하는지 확인합니다. 만약 존재한다면, 우리는 doFilter() 메소드를 호출하여 Request를 체인의 다음 필터로 전달합니다. 헤더가 존재하지 않는 경우, 우리는 응답에 HTTP 상태 400 Bad Request를 설정하고 체인의 다음 필터로 전달하지 않습니다(그림 5.7). 리스팅 5.2는 로직을 제시합니다.

그림 5.7 우리가 인증 전에 추가하는 사용자 정의 필터는 Request-Id 헤더의 존재 여부를 확인합니다. 요청에 이 헤더가 존재하면, 애플리케이션은 요청을 인증하도록 전달합니다. 이 헤더가 존재하지 않는 경우, 애플리케이션은 HTTP 상태 400 Bad Request를 설정하고 클라이언트에게 반환합니다.
Listing 5.2 Implementing the logic in the doFilter() method
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
var httpRequest = (HttpServletRequest) request;
var httpResponse = (HttpServletResponse) response;
String requestId = httpRequest.getHeader("Request-Id");
if (requestId == null || requestId.isBlank()) {
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return; #A
}
filterChain.doFilter(request, response); #B
}
#A Request-Id 헤더가 누락된 경우, HTTP 상태가 400 Bad Request로 변경되고 요청은 체인의 다음 필터로 전달되지 않습니다.
#B Request-Id 헤더가 존재하는 경우, 요청은 체인의 다음 필터로 전달됩니다.
2단계를 구현하기 위해, 자바 기반 Configuration 클래스 내에서 필터를 적용하기 위해 HttpSecurity 객체의 addFilterBefore() 메소드를 사용합니다. 우리는 이 사용자 정의 필터를 인증 전에 실행하도록 하고 싶기 때문입니다. 이 메소드는 두 가지 파라미터를 받습니다:
- 체인에 추가하고자하는 사용자 정의 필터의 인스턴스입니다. 우리 예제에서는 리스팅 5.1에 제시된 RequestValidationFilter 클래스의 인스턴스입니다.
- 새 인스턴스를 추가할 기존 필터의 유형입니다. 이 예제의 요구 사항이 필터 로직을 인증 전에 실행하는 것이므로, 우리는 사용자 정의 필터 인스턴스를 인증 필터 전에 추가해야 합니다. BasicAuthenticationFilter 클래스는 인증 필터의 기본 유형을 정의합니다.
지금까지 우리는 인증과 관련된 필터를 일반적으로 인증 필터라고 언급했습니다. 다음 장에서 Spring Security가 다른 필터들도 구성한다는 것을 알게 될 것입니다. 9장에서는 사이트 간 요청 위조(CSRF) 보호에 대해, 10장에서는 교차 출처 리소스 공유(CORS)에 대해 논의할 것입니다. 두 기능 모두 필터에 의존합니다.
리스팅 5.3은 구성 클래스에서 인증 필터 전에 사용자 정의 필터를 추가하는 방법을 보여줍니다. 예제를 단순하게 하기 위해, 모든 인증되지 않은 요청을 허용하기 위해 permitAll() 메소드를 사용합니다.
Listing 5.3 Configuring the custom filter before authentication
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.addFilterBefore( #A
new RequestValidationFilter(), BasicAuthenticationFilter.class)
.authorizeRequests(c -> c.anyRequest().permitAll());
return http.build();
}
}
#A Adds an instance of the custom filter before the authentication filter in the filter chain
이제 애플리케이션을 실행하고 테스트할 수 있습니다. 헤더 없이 엔드포인트를 호출하면 HTTP 상태 400 Bad Request가 있는 응답이 생성됩니다. 요청에 헤더를 추가하면, 응답 상태는 HTTP 200 OK가 되며, 응답 본문인 Hello!도 볼 수 있습니다. Request-Id 헤더 없이 엔드포인트를 호출하기 위해 이 Postman을 사용합니다:

5.3 Adding a filter after an existing one in the chain
이 섹션에서는 필터 체인에서 기존 필터 이후에 필터를 추가하는 방법에 대해 논의합니다. 이 접근 방식은 필터 체인에 이미 존재하는 특정 필터 이후에 어떤 로직을 실행하고 싶을 때 사용합니다. 인증 과정 이후에 어떤 로직을 실행해야 한다고 가정해 봅시다. 이에 대한 예시로는 특정 인증 이벤트 이후에 다른 시스템에 알림을 보내거나 단순히 로깅 및 추적 목적일 수 있습니다(그림 5.8). 5.1절과 마찬가지로, 이를 수행하는 방법을 보여주기 위해 예제를 구현합니다. 실제 상황에 맞게 이를 조정할 수 있습니다.
우리의 예제에서는 인증 필터 이후에 필터를 추가함으로써 모든 성공적인 인증 이벤트를 로깅합니다(그림 5.8). 인증 필터를 통과하는 것이 성공적으로 인증된 이벤트를 나타낸다고 간주하며, 이를 로깅하고자 합니다. 5.1절에서의 예제를 계속하여, HTTP 헤더를 통해 받은 요청 ID도 로깅합니다.

그림 5.8 우리는 애플리케이션이 인증하는 요청을 로깅하기 위해 BasicAuthenticationFilter 이후에 AuthenticationLoggingFilter를 추가합니다.
다음 리스팅 5.5는 인증 필터를 통과하는 요청을 로깅하는 필터의 정의를 제시합니다.
Listing 5.5 Defining a filter to log requests
public class AuthenticationLoggingFilter implements Filter {
private final Logger logger =
Logger.getLogger(
AuthenticationLoggingFilter.class.getName());
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
var httpRequest = (HttpServletRequest) request;
var requestId =
httpRequest.getHeader("Request-Id"); #A
logger.info("Successfully authenticated #B
request with id " + requestId); #B
filterChain.doFilter(request, response); #C
}
}
#A Request Header에서 Request ID를 가져옵니다.
#B Request ID 값으로 이벤트를 로깅합니다.
#C Request을 체인의 다음 필터로 전달합니다.
인증 필터 이후에 체인에 사용자 정의 필터를 추가하려면, HttpSecurity의 addFilterAfter() 메소드를 호출합니다. 다음 리스팅은 구현을 보여줍니다.
Listing 5.6 Adding a custom filter after an existing one in the filter chain
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.addFilterBefore(
new RequestValidationFilter(),
BasicAuthenticationFilter.class)
.addFilterAfter( #A
new AuthenticationLoggingFilter(),
BasicAuthenticationFilter.class)
.authorizeRequests(c -> c.anyRequest().permitAll());
return http.build();
}
}
#A AuthenticationLoggingFilter의 인스턴스를 BasicAuthenticationFiler 뒤, 필터 체인에 추가하세요.
애플리케이션을 실행하고 엔드포인트를 호출할 때, 엔드포인트에 대한 모든 성공적인 호출에 대해 애플리케이션이 콘솔에 로그 라인을 출력하는 것을 관찰할 수 있습니다. 호출에 대해서는

콘솔에 다음과 로그를 확인할 수 있습니다.

5.4 Adding a filter at the location of another in the chain
이 섹션에서는 필터 체인의 다른 필터 위치에 필터를 추가하는 방법에 대해 논의합니다. 이 방법은 주로 Spring Security에서 알려진 필터 중 하나가 이미 가정한 역할에 대해, 다른 구현을 제공해야 할 때 사용합니다. 전형적인 시나리오는 인증입니다.
예를 들어, HTTP Basic 인증 흐름 대신 다른 것을 구현하고 싶다고 가정해 봅시다. 사용자 이름과 비밀번호를 입력 자격 증명으로 사용하여 애플리케이션이 사용자를 인증하는 대신, 다른 접근 방식을 적용합니다. 발생할 수 있는 몇 가지 시나리오 예는 다음과 같습니다.
- 인증을 위한 정적 헤더 값 기반 식별
- 인증을 위해 요청에 대칭 키로 서명 사용
- 인증 과정에서 일회용 비밀번호 (OTP) 사용
첫 번째 시나리오에서는, 인증을 위한 정적 키 기반 식별에서, 클라이언트는 항상 동일한 문자열을 HTTP 요청의 헤더에 앱으로 보냅니다. 애플리케이션은 이러한 값을 어딘가에 저장하는데, 대부분 데이터베이스나 시크릿 볼트에 저장할 것입니다. 이 정적 값에 기반하여 애플리케이션은 클라이언트를 식별합니다.
이 접근법(그림 5.9)은 인증과 관련하여 약한 보안을 제공하지만, 간단함 때문에 아키텍트와 개발자들은 백엔드 애플리케이션 간의 호출에서 이를 종종 선택합니다. 구현은 또한 복잡한 계산을 할 필요가 없기 때문에 빠르게 실행됩니다. 이는 암호화 서명을 적용하는 경우와 같습니다. 이 방식으로, 인증을 위해 사용되는 정적 키는 개발자들이 보안 측면에서 인프라 수준에 더 의존하며, 동시에 엔드포인트를 완전히 보호되지 않은 상태로 남겨두지 않는 타협을 나타냅니다.
시크릿 볼트(Secrets Vault)는 중요한 정보를 안전하게 저장하고 관리하기 위한 도구나 시스템을 의미합니다. 여기에는 비밀번호, API 키, 인증서, 암호화 키 등과 같이 보호해야 할 민감한 데이터가 포함됩니다. 시크릿 볼트는 이러한 정보를 평문으로 저장하는 대신 암호화하여 보관함으로써 보안을 강화합니다. 시크릿 볼트는 접근 제어, 감사 로깅, 자동 키 교체와 같은 기능을 제공하여 보안성을 더욱 강화합니다. 개발자와 시스템 관리자는 이러한 도구를 사용하여 애플리케이션과 시스템에서 사용하는 중요한 정보를 안전하게 관리할 수 있습니다. HashiCorp Vault, AWS Secrets Manager, Azure Key Vault 등이 시크릿 볼트를 제공하는 인기 있는 솔루션 중 일부입니다.
※마이크로 서비스 아키텍쳐의 마이크로서비스 서버들의 configuration 정보를 HashiCorp Vault 서버에 저장합니다

그림 5.9 요청은 정적 키의 값을 포함하는 헤더를 담고 있습니다. 이 값이 애플리케이션이 알고 있는 값과 일치하면, 애플리케이션은 요청을 수락합니다.
두 번째 시나리오에서는 대칭 키를 사용하여 요청에 서명하고 검증합니다. 클라이언트와 서버 모두 키의 값을 알고 있습니다(클라이언트와 서버가 키를 공유합니다). 클라이언트는 이 키를 사용하여 요청의 일부분(예를 들어, 특정 헤더의 값을 서명하기 위해)에 서명하고, 서버는 같은 키를 사용하여 서명이 유효한지 확인합니다(그림 5.10). 서버는 데이터베이스나 시크릿 볼트에 각 클라이언트별 개별 키를 저장할 수 있습니다. 마찬가지로, 비대칭 키 쌍을 사용할 수도 있습니다.

그림 5.10 Authorization 헤더에는 클라이언트와 서버 모두가 알고 있는 키(또는 서버가 공개 키를 가진 개인 키)로 서명된 값이 포함되어 있습니다. 애플리케이션은 서명을 확인하고, 이 서명이 올바른 경우 요청을 허용합니다.
마지막으로, 세 번째 시나리오에서 인증 과정에 일회용 비밀번호(OTP)를 사용하는 경우, 사용자는 메시지를 통하거나 Google Authenticator와 같은 인증 제공자 앱을 사용하여 OTP를 받습니다(그림 5.11).

그림 5.11 자원에 접근하기 위해, 클라이언트는 일회용 비밀번호(OTP)를 사용해야 합니다. 클라이언트는 제3의 인증 서버로부터 OTP를 획득합니다. 일반적으로, 애플리케이션은 다중 인증이 요구될 때 로그인하는 동안 이 접근 방식을 사용합니다.
사용자 정의 필터를 적용하는 방법을 보여주는 예제를 구현해 보겠습니다. 관련성을 유지하면서 간단하게 사례를 유지하기 위해 구성에 중점을 두고 인증을 위한 간단한 논리를 고려합니다. 우리 시나리오에서는 모든 요청에 대해 동일한 정적 키 값이 있습니다. 인증을 받으려면 사용자는 그림 5.12에 표시된 것처럼 Authorization 헤더에 정적 키의 올바른 값을 추가해야 합니다. 이 예제의 코드는 ssia-ch5-ex2 프로젝트에서 찾을 수 있습니다.

그림 5.12 클라이언트는 HTTP 요청의 Authorization 헤더에 정적 키를 추가합니다. 서버는 요청을 승인하기 전에 키를 알고 있는지 확인합니다.
우리는 StaticKeyAuthenticationFilter라고 명명된 필터 클래스를 구현하는 것으로 시작합니다. 이 클래스는 속성 파일에서 정적 키의 값을 읽고 Authorization 헤더의 값이 이와 동일한지 확인합니다. 값이 같으면 필터는 요청을 필터 체인의 다음 구성 요소로 전달합니다. 그렇지 않으면 필터는 요청을 필터 체인에서 전달하지 않고 응답의 HTTP 상태를 401 Unauthorized로 설정합니다. 리스팅 5.7에서는 StaticKeyAuthenticationFilter 클래스를 정의합니다.
Listing 5.7 The definition of the StaticKeyAuthenticationFilter class
@Component #A
public class StaticKeyAuthenticationFilter
implements Filter { #B
@Value("${authorization.key}") #C
private String authorizationKey;
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
var httpRequest = (HttpServletRequest) request;
var httpResponse = (HttpServletResponse) response;
String authentication = #D
httpRequest.getHeader("Authorization");
if (authorizationKey.equals(authentication)) {
filterChain.doFilter(request, response);
} else {
httpResponse.setStatus(
HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
#A 속성 파일에서 값을 주입할 수 있도록 클래스의 인스턴스를 Spring 컨텍스트에 추가합니다.
#B Filter 인터페이스를 구현하고 doFilter() 메서드를 재정의하여 인증 로직을 정의합니다.
#C @Value 주석을 사용하여 속성 파일에서 정적 키의 값을 가져옵니다.
#D 요청에서 Authorization 헤더의 값을 가져와 이를 정적 키와 비교합니다.
필터를 정의한 후에는 addFilterAt() 메서드를 사용하여 BasicAuthenticationFilter 클래스의 위치에 해당 필터를 필터 체인에 추가합니다(그림 5.13).

그림 5.13에서는 사용자 정의 인증 필터를 추가합니다. 이 필터는 HTTP Basic 인증 방법을 사용할 때 BasicAuthenticationFilter 클래스가 위치했을 곳에 추가됩니다. 이는 우리의 사용자 정의 필터가 동일한 순서 값을 가지고 있다는 것을 의미합니다.
하지만 섹션 5.1에서 논의한 내용을 기억하세요. 특정 위치에 필터를 추가할 때, Spring Security는 해당 위치에 단 하나만 있는 것으로 가정하지 않습니다. 동일한 위치에 여러 필터를 추가할 수 있습니다. 이 경우 Spring Security는 이러한 필터가 어떤 순서로 작동할지를 보장하지 않습니다. 이것을 다시 말하는 이유는 많은 사람들이 이 작동 방식에 혼동을 겪기 때문입니다. 어떤 개발자들은 이미 알려진 위치에 필터를 적용하면 해당 필터가 대체될 것으로 생각합니다. 하지만 그렇지 않습니다! 우리는 필요하지 않은 필터를 체인에 추가하지 않도록 주의해야 합니다.
참고: 체인 내 동일한 위치에 여러 필터를 추가하지 않는 것을 권장합니다. 동일한 위치에 더 많은 필터를 추가하면 그들이 사용되는 순서가 정의되지 않습니다. 필터가 호출되는 명확한 순서를 갖는 것이 합리적입니다. 알려진 순서를 갖는 것은 애플리케이션을 이해하고 유지하는 데 도움이 됩니다.
리스팅 5.8에서는 필터를 추가하는 구성 클래스의 정의를 찾을 수 있습니다. 여기서 HttpSecurity 클래스의 httpBasic() 메서드를 호출하지 않는 것을 관찰할 수 있습니다. 왜냐하면 BasicAuthenticationFilter 인스턴스를 필터 체인에 추가하고 싶지 않기 때문입니다.
Listing 5.8 Adding the filter in the configuration class
@Configuration
public class ProjectConfig {
private final StaticKeyAuthenticationFilter filter; #A
// omitted constructor
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.addFilterAt(filter, #B
BasicAuthenticationFilter.class)
.authorizeRequests(c -> c.anyRequest().permitAll());
return http.build();
}
}
#A Spring 컨텍스트에서 필터의 인스턴스를 주입합니다.
#B 필터를 필터 체인에서 기본 인증 필터의 위치에 추가합니다.
애플리케이션을 테스트하기 위해서는 엔드포인트가 필요합니다. 이를 위해 리스팅 5.4에서 제공된 대로 컨트롤러를 정의합니다. 이 코드 스니펫에 표시된대로 application.properties 파일에 서버에 대한 정적 키의 값을 추가해야 합니다.
authorization.key=SD9cICjl1e
참고: 모든 사람이 볼 수 있는 것이 아닌 비밀번호, 키 또는 기타 데이터를 속성 파일에 저장하는 것은 생산적인 애플리케이션에는 좋은 방법이 아닙니다. 우리의 예제에서는 이러한 접근 방식을 단순화하고 Spring Security 구성에 집중할 수 있도록 하기 위해 사용합니다. 그러나 실제 시나리오에서는 이러한 유형의 세부 정보를 저장하기 위해 시크릿 볼트를 사용해야 합니다.
이제 애플리케이션을 테스트할 수 있습니다. 우리는 앱이 Authorization 헤더에 올바른 값이 포함된 요청을 허용하고, 다른 요청은 거부하여 응답으로 HTTP 401 Unauthorized 상태를 반환하는 것을 기대합니다. 다음 코드 조각은 애플리케이션을 테스트하기 위해 사용된 curl 호출을 나타냅니다. 서버 측에 설정한 Authorization 헤더에 동일한 값을 사용하면 호출이 성공하고, 응답 본문인 "Hello!"를 볼 수 있습니다. 다음 curl 호출은 다음과 같은 응답 본문을 반환합니다.
curl -H "Authorization:SD9cICjl1e" http://localhost:8080/hello
응답 본문:
Hello!
다음 호출에서는 Authorization 헤더가 누락되거나 올바르지 않은 경우 응답 상태가 HTTP 401 Unauthorized로 설정됩니다.
curl -v http://localhost:8080/hello
응답 상태:
...
< HTTP/1.1 401
...
이 경우, UserDetailsService를 구성하지 않았기 때문에 Spring Boot가 자동으로 하나를 구성합니다. 그러나 우리의 시나리오에서는 사용자 개념이 존재하지 않기 때문에 UserDetailsService가 전혀 필요하지 않습니다. 우리는 단지 서버에 엔드포인트를 호출할 수 있는 사용자가 특정 값을 알고 있는지를 확인합니다. 애플리케이션 시나리오는 보통 이렇게 간단하지 않으며, 대개 UserDetailsService가 필요합니다. 그러나 이 구성 요소가 필요하지 않은 경우를 예상하거나 가지고 있다면 자동 구성을 비활성화할 수 있습니다. 기본 UserDetailsService의 구성을 비활성화하려면 주요 클래스에 @SpringBootApplication 주석의 exclude 속성을 사용할 수 있습니다.
@SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class})
Postman에서는 다음과 같이 테스트할 수 있습니다.

5.5 Filter implementations provided by Spring Security
이 섹션에서는 Filter 인터페이스를 구현하는 Spring Security에서 제공하는 클래스들에 대해 논의합니다. 이 장의 예제에서는 이 인터페이스를 직접 구현하여 필터를 정의합니다.
Spring Security는 Filter 인터페이스를 구현하는 몇 가지 추상 클래스를 제공합니다. 이러한 클래스들을 확장하여 필터를 정의할 수 있습니다. 이러한 클래스들은 확장할 때 추가 기능을 제공하여 구현에 이점을 줍니다. 예를 들어, 적용 가능한 경우 web.xml 디스크립터 파일에 정의할 초기화 파라미터를 사용할 수 있는 GenericFilterBean 클래스를 확장할 수 있습니다. GenericFilterBean을 확장하는 더 유용한 클래스는 OncePerRequestFilter입니다. 필터를 체인에 추가할 때 프레임워크는 요청당 한 번만 호출될 것을 보장하지 않습니다. OncePerRequestFilter는 이름에서 알 수 있듯이 필터의 doFilter() 메서드가 요청당 한 번만 실행되도록 로직을 구현합니다.
만약 애플리케이션에서 이러한 기능이 필요하다면 Spring이 제공하는 클래스를 사용하세요. 그러나 그러한 기능이 필요하지 않은 경우, 구현을 가능한 한 간단하게 유지하는 것을 권장합니다. 너무 자주, GenericFilterBean 클래스를 확장하여 필요하지 않은 기능에 대한 사용자 정의 로직을 추가하는 대신 Filter 인터페이스를 구현하는 것을 본 적이 있습니다. 왜냐하면 구현을 복사하여 웹에서 찾은 예제에 그대로 사용한 것 같습니다.
이러한 클래스를 사용하는 방법을 명확하게 하기 위해 예제를 작성해 보겠습니다. 섹션 5.3에서 구현한 로깅 기능은 OncePerRequestFilter를 사용하는 좋은 후보입니다. 동일한 요청을 여러 번 로깅하는 것을 피하고자 합니다. Spring Security는 필터가 한 번 이상 호출되지 않을 것을 보장하지 않으므로 우리가 직접 관리해야 합니다. 가장 간단한 방법은 OncePerRequestFilter 클래스를 사용하여 필터를 구현하는 것입니다. 이것을 별도의 프로젝트인 ssia-ch5-ex3에 작성했습니다.
리스팅 5.9에서는 AuthenticationLoggingFilter 클래스에 대한 변경 사항을 찾을 수 있습니다. 섹션 5.3의 예제와 같이 Filter 인터페이스를 직접 구현하는 대신 이제 OncePerRequestFilter 클래스를 확장합니다. 여기서 오버라이드하는 메서드는 doFilterInternal()입니다. 이 코드는 프로젝트 ssia-ch5-ex3에서 찾을 수 있습니다.
Listing 5.9 Extending the OncePerRequestFilter class
public class AuthenticationLoggingFilter
extends OncePerRequestFilter { #A
private final Logger logger =
Logger.getLogger(
AuthenticationLoggingFilter.class.getName());
@Override
protected void doFilterInternal( #B
HttpServletRequest request, #C
HttpServletResponse response, #C
FilterChain filterChain) throws
ServletException, IOException {
String requestId = request.getHeader("Request-Id");
logger.info("Successfully authenticated request with id " +
requestId);
filterChain.doFilter(request, response);
}
}
#A Filter 인터페이스를 구현하는 대신 OncePerRequestFilter 클래스를 확장합니다.
#B Filter 인터페이스의 doFilter() 메서드의 역할을 대체하는 doFilterInternal()을 오버라이드합니다.
#C OncePerRequestFilter는 HTTP 필터만 지원합니다. 이것이 파라미터를 HttpServletRequest와 HttpServletResponse로 직접 제공하는 이유입니다.
OncePerRequestFilter 클래스에 대한 몇 가지 짧은 관찰 사항을 소개합니다. 여러분이 유용하게 활용할 수 있을 것입니다:
- 이 클래스는 HTTP 요청만 지원하지만, 실제로 우리가 항상 사용하는 것입니다. 이점은 OncePerRequestFilter가 타입을 캐스팅하고, 요청을 HttpServletRequest와 HttpServletResponse로 직접 수신한다는 것입니다. Filter 인터페이스를 사용할 때 요청과 응답을 캐스팅해야 했던 것을 기억하세요.
- 필터가 적용되는지 여부를 결정하는 로직을 구현할 수 있습니다. 필터를 체인에 추가했더라도 특정 요청에는 적용되지 않도록 결정할 수 있습니다. 이를 위해 shouldNotFilter(HttpServletRequest) 메서드를 오버라이드하여 설정합니다. 기본적으로 필터는 모든 요청에 적용됩니다.
- 기본적으로 OncePerRequestFilter는 비동기 요청이나 오류 디스패치 요청에는 적용되지 않습니다. shouldNotFilterAsyncDispatch() 및 shouldNotFilterErrorDispatch() 메서드를 오버라이드하여 이 동작을 변경할 수 있습니다.
여러분의 구현에서 OncePerRequestFilter의 이러한 특성 중 하나라도 유용하다고 생각된다면, 이 클래스를 사용하여 필터를 정의하는 것이 좋습니다.
5.6 Summary
- 웹 애플리케이션 아키텍처의 첫 번째 레이어는 HTTP 요청을 가로채는 필터 체인입니다. Spring Security 아키텍처의 다른 구성 요소와 마찬가지로 이를 사용자 정의하여 요구 사항에 맞게 조정할 수 있습니다.
- 기존 필터 전, 후 또는 위치에 새 필터를 추가하여 필터 체인을 사용자 정의할 수 있습니다.
- 기존 필터와 동일한 위치에 여러 필터를 가질 수 있습니다. 이 경우, 필터가 실행되는 순서는 정의되지 않습니다.
- 필터 체인을 변경하면 인증 및 권한 부여를 정확히 애플리케이션 요구 사항에 맞게 사용자 정의할 수 있습니다.
'Spring Security' 카테고리의 다른 글
ch07 Configuring endpoint-level authorization: Restricting access (0) | 2024.03.01 |
---|---|
ch06 Implementing authenticaiton (0) | 2024.02.26 |
ch04 Managing passwords (0) | 2024.02.25 |
ch03 Managing users (0) | 2024.02.25 |
Ch02 Hello Spring Security (0) | 2024.02.25 |