DefaultSecurityFilterChain에 기본적으로 등록되는 필터로 각각 5번째, 6번째에 위치하는 CorsFilter, CsrfFilter에 대해 알아보자.
필터가 등록되는 목적은 CorsConfigurationSource에 설정한 값에 따라 필터 단에서 응답 헤더를 설정하기 위함이다.
http
.cors((cors) -> cors.disable());
CorsFilter은 다른 필터와 다르게 GenericFilterBean 추상 클래스가 아닌 GenericFilter 추상 클래스를 사용한다.
두 추상 클래스는 Filter 인터페이스를 구현한다는 형식에서는 동일하다.
그러나 GenericFilter는 스프링의 POJO 특성으로 스프링에서 사용하지만 엄밀하게 정의하면 스프링에는 연관이 없다. 즉 Bean으로 관리되지 않는다.
CorsFilter는 스프링 시큐리티 의존성에 포함된 것이 맞지만, 시큐리티 이전의 자바 서블릿 생태계에서 이미 그 형태를 가지고 있었기 때문에 본질의 특성을 따라서
GenericFilter기반으로 구현되어 있다.

등록 및 초기화
기본적으로 CorsFilter가 등록될 때, @Override 되어 있는 CorsFilter의 init() 메소드에 의해 사용자가 SecurityConfig 클래스의 SecurityFilterChain에 정의한 CorsConfigurationSource 값에 의해 필드 변수들이 초기화된다.
doFilter()
doFilter() 메소드에서 간단하게 HttpServletRequest로 캐스팅을 진행한 뒤 CORS 요청 타입을 확인한다. 그리고 switch문에 의해서 request의 값에 따라 상황에 알맞은 CORS 처리를 진행한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
return configuration;
}
}));
return http.build();
}
※ CSRF 공격
: 사용자의 의지와 무관하게 해커가 강제로 사용자의 브라우저를 통해 서버측으로 특정한 요청을 보내도록 공격하는 방법(일반적으로 서버가 세션 방식일 때, 브라우저의 쿠키로 인해 발생한다)
이 필터가 등록되는 목적은 CSRF 공격 방어를 위해 HTTP 메소드 중 GET, HEAD, TRACE, OPTIONS 메소드를 제외한 요청에 대해서 검증을 진행한다.

스프링 시큐리티의 CSRF 검증 방식은 토큰 방식이며, 요청시 토큰을 서버 저장소에 저장 후 클라이언트에게도 전송하며, 그 후 해당하는 요청에 대해서 서버에 저장된 토큰과 비교 검증을 진행한다.
http
.csrf((csrf) -> csrf.disable());
public final class CsrfFilter extends OncePerRequestFilter {
...
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 토큰을 토큰 저장소로 부터 불러옴
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
// 다음 요청을 위해 토큰을 request header에 추가
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
this.requestHandler.handle(request, response, deferredCsrfToken::get);
// HTTP 메소드 확인 후 CSRF 검증이 필요 없는 메소드면 다음 필터로 넘김
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
// 서버 저장 토큰
CsrfToken csrfToken = deferredCsrfToken.get();
// 클라이언트에서 온 토큰
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
// 클라이언트로 부터 온 토큰과 서버 저장소의 토큰을 비교 검증
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
boolean missingToken = deferredCsrfToken.isGenerated();
this.logger
.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
// 다음 필터로 넘김
filterChain.doFilter(request, response);
}
...
}
CSRF 토큰의 생성 및 관리는 CsrfTokenRepository라는 인터페이스를 정의하고 그것을 구현한 클래스에게 위임 시킨다.
구현체
HttpSessionCsrfTokenRepository : 서버 세션에 토큰을 저장 관리함 (기본값)CookieCsrfTokenRepository : 쿠키에 토큰을 저장 관리함// CSRF 값 설정 - 토큰 저장소 설정
http
.csrf((csrf) -> csrf.csrfTokenRepository(new HttpSessionCsrfTokenRepository()));
기본 동작은 SSR 세션 방식으로 설정되어 있다.
Controller 단에서 View 단 응답 시, HTML Form 영역에 서버에 저장되어 있는 CSRF 토큰 값을 넣어주면 된다.
Stateless한 REST API에서는 JSESSION에 대한 서버 세션이 상태를 가지지 않기 때문에 CSRF 공격 위험 자체가 없어 disable 한다.
하지만 JWT를 쿠키에 저장할 경우 CSRF 공격의 위험이 있을 수 있기 때문에 활성화하는 것이 좋다. 다만 CSRF 토큰을 발급할 VIEW 페이지와 같은 로직이 없기 때문에 토큰 방식이 아닌 Referer 방식을 사용한다.
이 방식은HTTP Referer헤더를 통해 요청의 출발점, 이전 URL등을 검증한다.