Spring/Spring Security

[9장] 필터 구현

꾸준함. 2025. 5. 31. 01:16

주의

이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.

 

서론

  • 스프링 시큐리티의 HTTP 필터는 일반적으로 요청에 적용해야 하는 각 책임을 관리하며 책임의 체인을 형성함
  • 필터는 요청을 수신하고 그 논리를 실행하며 최종적으로 체임의 다음 필터에 요청을 위임함

 

https://livebook.manning.com/book/spring-security-in-action/chapter-9/v-7

 

  • 스프링 시큐리티는 맞춤 구성을 통해 필터 체인에 추가할 수 있는 필터 구현을 제공하지만 맞춤형 필터도 정의할 수 있음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-9/

 

1. 스프링 시큐리티 아키텍처의 필터 구현

  • 인증 필터는 요청을 가로채고 인증 책임을 AuthenticationManager에 위임함
  • 인증 이전에 특정 논리를 실행하려면 인증 필터 앞에 필터를 추가하면 됨
  • 스프링 시큐리티 아키텍처의 필터는 일반적인 HTTP 필터이며 필터를 생성하기 위해 javax.servlet 패키지의 Filter 인터페이스를 구현하면 됨
    • 다른 HTTP 필터와 마찬가지로 doFilter() 메서드를 재정의해 논리를 구현해야 함
    • 해당 메서드는 ServletRequest, ServletResponse, 그리고 FilterChain 매개변수를 받음

 

  • 필터 체인은 필터가 작동하는 순서가 정의된 필터의 모음
  • 스프링 시큐리티에는 몇 가지 필터 구현과 순서가 있으며 이들 중 몇 가지를 소개하면 다음과 같음
    • BasicAuthenticationFilter는 HTTP Basic 인증을 처리
    • CsrfFilter는 CSRF (사이트 간 요청 위조)를 처리
    • CorsFilter는 CORS (교차 출처 리소스 공유) 권한 부여 규칙을 처리

 

  • 애플리케이션이 필터 체인에 이러한 모든 필터의 인스턴스를 반드시 가질 필요는 없음
    • 필터 체인은 애플리케이션을 구성하는 방법에 따라 더 길어지거나 짧아질 수도 있음
    • 개발자가 작성하는 구성에 따라 필터 체인의 정의가 영향을 받음
    • i.g. HTTP Basic 인증 방식을 이용하려면 HttpSecurity 클래스의 httpBasic() 메서드를 호출해야 하는데 해당 메서드를 호출하면 필터 체인에 BasicAuthenticationFilter가 추가됨

 

2. 체인에서 기존 필터 앞에 필터 추가

  • 모든 요청에 Request-Id 헤더가 있고 해당 헤더로 요청을 추적하여 인증을 수행하기 전에 헤더가 있는지 검증한다고 가정
  • 인증 프로세스에는 DB 쿼리나 다른 리소스를 소비하는 작업이 포함될 수 있으므로 요청의 형식이 유효하지 않으면 이런 작업을 실행할 필요가 없음
  • 위 요구 사항을 두 개의 단계로 해결할 수 있으며 최종 필터 체인 형상은 다음과 같음
    • 필터를 구현: 요청에 필요한 헤더가 있는지 확인하는 RequestValidationFilter 클래스 생성
    • 필터 체인에 필터 추가: 구성 클래스에서 configure() 메서드를 재정의해 필터 체인에 필터 추가


public class RequestValidationFilter implements Filter {
@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;
}
filterChain.doFilter(request, response);
}
}
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 필터 체인에서 인증 필터 앞에 맞춤형 필터의 인스턴스 추가
http.addFilterBefore(
new RequestValidationFilter(),
BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
view raw .java hosted with ❤ by GitHub

https://assu10.github.io/dev/2023/12/16/springsecurity-filter/

 

3. 체인에서 기존 필터 뒤에 필터 추가

  • 간단한 로깅과 추적 목적을 위해 특정 인증 이벤트 이후 다른 시스템에 알림을 전달하는 사례가 있다고 가정
  • 다음은 인증 필터 뒤에 성공한 인증 이벤트를 모두 추가하는 필터 예제


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;
String requestId = httpRequest.getHeader("Request-Id");
logger.info("Successfully authenticated request with id " + requestId);
filterChain.doFilter(request, response);
}
}
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(
new RequestValidationFilter(),
BasicAuthenticationFilter.class)
// 필터 체인에서 인증 필터 뒤에 맞춤형 필터의 인스턴스 추가
.addFilterAfter(
new AuthenticationLoggingFilter(),
BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
view raw .java hosted with ❤ by GitHub

https://livebook.manning.com/book/spring-security-in-action/chapter-7/v-4/

 

4. 필터 체인의 다른 필터 위치에 필터 추가

  • 스프링 시큐리티의 기존 필터가 수행하는 책임에 대해 다른 구현을 제공할 때 필터 체인의 다른 필터 위치에 필터를 추가하는 과정이 적합함
  • HTTP Basic 인증 흐름 대신 조금 다른 인증을 구현해서 애플리케이션이 사용자를 인증하기 위한 자격 증명으로 사용자 이름과 암호 대신 다른 접근법을 적용하기를 원한다고 가정했을 때 가능성이 있는 시나리오의 예는 다음과 같음
    • 인증을 위한 정적 헤더 값에 기반을 둔 식별
    • 대칭 키를 이용해 인증 요청 서명
    • 인증 프로세스에 OTP 이용

 

4.1 인증을 위한 정적 헤더 값에 기반을 둔 식별

  • 클라이언트는 HTTP 요청의 헤더에 항상 동일한 문자열 하나를 앱으로 전달
    • 애플리케이션은 이러한 값을 예를 들어 DB나 비밀 볼트에 저장
    • 애플리케이션은 해당 정적 값을 바탕으로 클라이언트를 식별

 

  • 인증의 보안 수준은 낮지만 단순하다는 장점이 있어 설계자와 개발자는 백엔드 애플리케이션 간의 호출에 이를 자주 선택
    • 암호와 서명을 적용하는 것처럼 복잡한 계산을 수행할 필요가 없기 때문에 빠르게 실행됨
    • 정적 키를 인증에 이용하는 방식은 개발자가 보안 측면에서 인프라 수준에 더 의존하면서도 엔드포인트를 완전한 비보호 상태로 두지 않는 절충안

 

https://livebook.manning.com/book/spring-security-in-action/chapter-7/v-4/68

 

4.2 대칭 키를 이용해 인증 요청 서명

  • 클라이언트와 서버가 모두 키의 값을 공유하므로 서로가 키 값을 아는 상태
  • 클라이언트는 해당 키로 요청의 일부에 서명하고 서버는 같은 키로 서명이 유효한지 확인
  • 서버는 각 클라이언트의 개별 키를 DB나 비밀 볼트에 저장할 수 있으며 비슷하게 비대칭 키 쌍을 이용할 수도 있음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-7/v-4/72

 

4.3 인증 프로세스에 OTP 이용

  • 문자 메시지를 통해 또는 Google Authenticator와 같은 인증 공급자 앱으로 OTP를 받음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-7/v-4/72

 

4.4 맞춤형 필터 적용 예제

  • 모든 요청에 대해 같은 정적 키 값을 이용한다고 가정
  • 필터 클래스인 StaticKeyAuthenticationFilter는 속성 파일에서 정저 키 값을 읽고 Authorization 헤더 값과 같은지 확인
    • 값이 같으면 필터는 요청을 필터 체인의 다음 구성 요소에 전달
    • 그렇지 않으면 요청을 필터 체인에 전달하지 않고 응답의 HTTP 상태를 401 권한 없음으로 설정

 

  • 중요한 점은 특정 위치에 필터를 추가해도 스프링 시큐리티는 해당 위치에 필터가 하나라고 가정하지 않음
    • 필터 체인의 같은 위치에 필터를 더 추가할 수 있으며 이 경우 스프링 시큐리티는 필터가 실행되는 순서를 보장하지 않음
    • 기존 필터의 위치에 다른 필터를 적용하면 필터가 대체된다고 생각하는 경우가 많은데 그렇지 않으며 필터 체인에 필요 없는 필터는 아예 추가하지 않아야 함
      • 아래 예제에서는 필터 체인에 BasicAuthenticationFilter 인스턴스가 추가되는 것은 원하지 않으므로 HttpSecurity 클래스의 httpBasic() 메서드를 호출하지 않음


@Component
public class StaticKeyAuthenticationFilter implements Filter {
@Value("${authorization.key}")
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 = httpRequest.getHeader("Authorization");
if (authorizationKey.equals(authentication)) {
filterChain.doFilter(request, response);
} else {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Autowired
private StaticKeyAuthenticationFilter filter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 필터 체인에서 기본 인증 필터의 위치에 필터 추가
http.addFilterAt(filter,
BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
view raw .java hosted with ❤ by GitHub

https://livebook.manning.com/book/spring-security-in-action/chapter-7/v-4/79

 

5. 스프링 시큐리티가 제공하는 필터 구현

  • 스프링 시큐리티에는 Filter 인터페이스를 구현하는 여러 추상 클래스가 있으며, 이를 위해 필터 정의를 확장할 수 있음
    • 또한 이러한 클래스는 구현을 확장할 때 이점을 얻을 수 있는 기능을 추가함

 

  • OncePerRequestFilter는 GenericFilterBean을 확장하는 유용한 클래스
    • 프레임워크는 필터 체인에 추가한 필터를 요청당 한 번만 실행하도록 보장하지 않음
    • OncePerRequestFilter는 이름이 의미하듯이 필터의 doFilter() 메서드가 요청당 한 번만 실행되도록 논리를 구현

 

  • OncePerRequestFilter 클래스에 관해 몇 가지 알아둘 사항은 다음과 같음
    • 해당 클래스의 장점은 형식을 형 변환하여 HttpServletRequest 및 HttpServletResponse로 직접 요청을 수신한다는 것, Filter 인터페이스의 경우에는 요청과 응답을 형 변환해야 함
    • 필터 체인에 추가한 필터가 특정 요청에는 적용되지 않는다고 결정할 수 있음, 이 경우 shouldNotFilter(HttpServletRequest) 메서드를 재정의하면 됨, 기본적으로 필터는 모든 요청에 적용됨
    • OncePerRequestFilter는 기본적으로 비동기 요청이나 오류 발송 요청에는 적용되지 않음: 해당 동작을 변경하려면 shouldNotFilterAsyncDispatch() 및 shouldNotFilterErrorDispatch() 메서드를 재정의하면 됨

 

public class AuthenticationLoggingFilter extends OncePerRequestFilter {
private final Logger logger =
Logger.getLogger(AuthenticationLoggingFilter.class.getName());
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String requestId = request.getHeader("Request-Id");
logger.info("Successfully authenticated request with id " + requestId);
filterChain.doFilter(request, response);
}
}
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(
new AuthenticationLoggingFilter(),
BasicAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
view raw .java hosted with ❤ by GitHub

 

참고

스프링 시큐리티 인 액션

반응형