Filter

2023. 5. 19. 15:48Spring Framework/Web on Servlet Stack

Spring Web 모듈 필터 개요

Spring Web 모듈은 서블릿 기반 애플리케이션에서 웹 요청을 처리하는 데 유용한 여러 필터를 제공합니다. 이 필터들은 Form 데이터 처리, 전달된 헤더 관리, 얕은 ETag 캐싱, CORS(교차 출처 리소스 공유) 처리를 위한 기능을 제공합니다. 각 필터의 역할에 대해 자세히 살펴보겠습니다.

Form Data 필터

디폴트로 브라우저에서는 HTTP GET 또는 POST 메서드를 통해서만 폼 데이터를 전송할 수 있습니다. 하지만 Non-브라우저 클라이언트(예: REST 클라이언트)는 다른 HTTP 메서드인 PUT, PATCH, 또는 DELETE를 사용하여 폼 데이터를 전송할 수 있습니다. Servlet API는 POST 요청에 대해서만 ServletRequest.getParameter*() 메서드를 통해 폼 데이터에 접근할 수 있도록 제한하고 있기 때문에, PUT, PATCH, 또는 DELETE 요청의 폼 데이터는 기본적으로 접근할 수 없습니다.

이를 해결하기 위해 Spring Web 모듈은 FormContentFilter를 제공합니다. 이 필터는 application/x-www-form-urlencoded 콘텐츠 유형의 PUT, PATCH, DELETE 요청을 가로채어 요청 본문에서 폼 데이터를 읽고, ServletRequest를 래핑하여 ServletRequest.getParameter*() 메서드를 통해 폼 데이터를 접근할 수 있도록 합니다. 이는 POST 요청에서 폼 데이터를 처리하는 방식과 유사합니다.

사용 예:

  • PUT, PATCH, DELETE 메서드에서 폼 제출을 지원해야 할 때 FormContentFilter는 폼 필드를 POST 요청과 동일하게 접근할 수 있도록 보장합니다.

Forwarded Headers 필터

여러 프록시(예: 로드 밸런서 또는 리버스 프록시)를 거치는 요청의 경우, 원본 요청의 호스트, 포트 및 스킴이 변경될 수 있습니다. 이로 인해 서버가 클라이언트 요청에 대해 링크나 리디렉션을 생성할 때 문제가 발생할 수 있습니다. 서버는 클라이언트가 본래 요청한 내용과 다른 정보로 처리할 수 있기 때문입니다.

Forwarded HTTP 헤더:

  • RFC 7239는 원본 요청에 대한 정보를 전달하기 위해 Forwarded라는 표준 헤더를 정의합니다.
  • X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, X-Forwarded-Prefix와 같은 표준이 아닌 헤더도 많이 사용됩니다.
주요 헤더:
  1. X-Forwarded-Host: 원본 호스트를 전달합니다.
  2. X-Forwarded-Port: 원본 포트를 전달합니다.
  3. X-Forwarded-Proto: 원본 프로토콜(예: http 또는 https)을 전달합니다.
  4. X-Forwarded-Ssl: SSL 사용 여부(on 또는 off)를 전달합니다.
  5. X-Forwarded-Prefix: 원본 URL 경로 접두어를 전달합니다.

ForwardedHeaderFilter:

Spring Web은 ForwardedHeaderFilter를 제공하며, 이 필터는 다음과 같은 역할을 합니다:

  • 전달된 헤더에 따라 요청의 host, port, scheme을 조정합니다.
  • 요청을 수정한 후, 해당 헤더를 제거하여 이후 처리에 영향을 미치지 않도록 합니다.

리버스 프록시를 사용할 때 ForwardedHeaderFilter는 서버가 원본 클라이언트 요청 정보와 일관되게 요청을 처리할 수 있도록 해줍니다.

보안 고려 사항:
  • X-Forwarded-Host와 같은 헤더는 스푸핑될 수 있으므로, 신뢰할 수 있는 프록시만 이러한 헤더를 추가 또는 제거할 수 있도록 해야 합니다. 그렇지 않으면 악의적인 클라이언트가 헤더를 조작하여 애플리케이션을 속일 수 있습니다.
  • removeOnly=trueForwardedHeaderFilter를 구성하여 헤더를 사용하는 대신 제거만 할 수도 있습니다.

Shallow ETag 필터

ETag(엔터티 태그)는 동일한 데이터를 여러 번 전송하지 않도록 돕는 웹 캐싱 메커니즘입니다. Spring Web의 ShallowEtagHeaderFilter는 웹 응답에 대한 얕은 ETag를 생성하는 데 사용됩니다.

작동 방식:

  • 필터는 응답 내용을 캡처하고 MD5 해시를 계산한 후, 이를 ETag 헤더로 설정합니다.
  • 이후 클라이언트가 동일한 ETag 값을 포함한 If-None-Match 헤더를 제공하면, 서버는 304 Not Modified 응답을 반환하여 대역폭을 절약할 수 있습니다.

얕은 ETag와 깊은 ETag:

  • "얕은" ETag는 요청 세부 사항을 고려하지 않고 응답 내용만을 기반으로 ETag를 생성하는 것을 의미합니다. "깊은" ETag는 더 복잡한 체크를 포함할 수 있습니다.
  • 이 필터는 RFC 7232에서 정의된 대로 "약한" ETag(예: W/"etag-value")를 작성하는 기능을 지원합니다.
제한 사항:
  • 이 필터는 CPU 자원을 절약하지는 않습니다. 응답이 완전히 생성된 후에야 ETag를 계산할 수 있기 때문입니다.

CORS (교차 출처 리소스 공유)

CORS는 웹 서버의 리소스를 다른 도메인에서 요청할 수 있도록 하는 메커니즘으로, 현대 웹 애플리케이션에서 필수적입니다. Spring은 다음과 같은 방법으로 CORS에 대한 세밀한 지원을 제공합니다:

  • 어노테이션: Spring MVC 컨트롤러에서 직접 CORS를 구성할 수 있습니다.
  • CorsFilter: CORS 정책을 처리하는 필터로, Spring Security의 필터 체인보다 앞서 배치되어야 합니다.

이를 통해 어떤 출처(origin)가 리소스에 접근할 수 있는지, 어떤 HTTP 메서드와 헤더가 허용되는지, 쿠키와 같은 자격 증명이 요청과 함께 전송될 수 있는지를 정의할 수 있습니다.

Spring Security 통합:

Spring Security를 사용하는 경우, CorsFilter를 사용하여 CORS를 관리하는 것이 좋습니다. Spring Security도 자체적으로 CORS 처리를 구현하고 있기 때문입니다.

Dispatcher Types 및 비동기 요청

ForwardedHeaderFilterShallowEtagHeaderFilter와 같은 필터들이 비동기 요청을 올바르게 처리하려면 DispatcherType.ASYNC와 함께 등록해야 합니다. 이렇게 하면 필터가 비동기 디스패치를 사용하는 요청을 처리할 수 있습니다. 비동기 요청 외에도 에러 처리를 위해 필터들이 DispatcherType.ERROR와 함께 등록되어야 에러 디스패치 시나리오도 처리할 수 있습니다.

AbstractAnnotationConfigDispatcherServletInitializer를 사용하는 경우, 모든 필터는 자동으로 모든 디스패처 타입(REQUEST, ASYNC, ERROR 등)으로 등록됩니다. 하지만 필터를 수동으로 등록하는 경우(Spring Boot에서 FilterRegistrationBean을 사용하는 경우), DispatcherType.ASYNCDispatcherType.ERROR를 명시적으로 포함해야 합니다.

Summary

  • FormContentFilter: PUT, PATCH, DELETE 요청에서 폼 데이터를 POST 요청과 동일하게 접근할 수 있게 합니다.
  • ForwardedHeaderFilter: ForwardedX-Forwarded-*와 같은 헤더를 처리하여 올바른 호스트, 포트, 스킴 정보를 유지합니다.
  • ShallowEtagHeaderFilter: 얕은 ETag 기반 캐싱을 제공하여 네트워크 효율성을 향상시킵니다.
  • CorsFilter: 교차 출처 요청에 대한 세밀한 CORS 구성을 가능하게 합니다.

이 필터들은 폼 데이터 제출, 캐싱, 리버스 프록시 통합, CORS 처리 등의 다양한 시나리오를 다룰 수 있도록 하여 웹 애플리케이션의 유연성과 성능을 유지할 수 있게 해줍니다.

 


다음은 Spring Boot 기반의 데모 웹 애플리케이션을 구현하는 코드입니다. 이 애플리케이션은 Spring Web 모듈에서 제공하는 주요 필터(FormContentFilter, ForwardedHeaderFilter, ShallowEtagHeaderFilter, CorsFilter)를 설정하고 작동하는 예제입니다.

 

1. Spring Boot 프로젝트 설정

pom.xml (Maven 사용 시)

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Security (CORS 필터와 함께 활용 가능) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Spring Boot DevTools (옵션) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>

    <!-- Lombok (선택사항) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

 

2. Spring Boot 애플리케이션 기본 설정

Application.java

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 

3. Controller 구현

FormController.java

package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/form")
public class FormController {

    @PostMapping
    public String handlePost(@RequestParam Map<String, String> formData) {
        return "POST 요청 처리됨: " + formData;
    }

    @PutMapping
    public String handlePut(@RequestParam Map<String, String> formData) {
        return "PUT 요청 처리됨: " + formData;
    }

    @PatchMapping
    public String handlePatch(@RequestParam Map<String, String> formData) {
        return "PATCH 요청 처리됨: " + formData;
    }

    @DeleteMapping
    public String handleDelete(@RequestParam Map<String, String> formData) {
        return "DELETE 요청 처리됨: " + formData;
    }
}

 

PUT, PATCH, DELETE 요청에서도 @RequestParam을 통해 폼 데이터를 받을 수 있도록 FormContentFilter가 필요합니다.

 

4. Filter 설정

WebConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.FormContentFilter;
import org.springframework.web.filter.ForwardedHeaderFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class WebConfig {

    // FormContentFilter 등록 (PUT, PATCH, DELETE에서도 form-data 처리 가능)
    @Bean
    public FormContentFilter formContentFilter() {
        return new FormContentFilter();
    }

    // ForwardedHeaderFilter 등록 (프록시 환경에서 올바른 요청 정보 유지)
    @Bean
    public ForwardedHeaderFilter forwardedHeaderFilter() {
        return new ForwardedHeaderFilter();
    }

    // ShallowEtagHeaderFilter 등록 (ETag를 사용하여 응답 캐싱)
    @Bean
    public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
        return new ShallowEtagHeaderFilter();
    }

    // CORS 설정을 위한 CorsFilter 등록
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("*")); // 모든 도메인 허용
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

 

5. 테스트용 CORS 컨트롤러

CorsTestController.java

package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/cors")
public class CorsTestController {

    @GetMapping
    public String testCors(@RequestHeader Map<String, String> headers) {
        return "CORS 요청 성공: " + headers;
    }
}

 

GET /cors 요청 시 클라이언트가 CORS 관련 헤더를 전달하는지 확인할 수 있습니다.

 

6. 애플리케이션 실행 후 테스트

1️⃣ 폼 데이터 처리 테스트 (FormContentFilter)

PUT, PATCH, DELETE 요청에서 application/x-www-form-urlencoded 데이터를 처리하는지 확인:

curl -X PUT http://localhost:8080/form -d "name=John&age=30" -H "Content-Type: application/x-www-form-urlencoded"

2️⃣ 프록시 헤더 처리 테스트 (ForwardedHeaderFilter)

프록시 환경에서 요청이 올바르게 처리되는지 확인:

curl -H "X-Forwarded-Proto: https" -H "X-Forwarded-Host: example.com" http://localhost:8080/cors

3️⃣ ETag 캐싱 테스트 (ShallowEtagHeaderFilter)

같은 요청을 여러 번 보내면 ETag가 생성되고, If-None-Match 헤더를 포함하면 304 응답을 받을 수 있는지 확인:

curl -v -H "If-None-Match: W/\"123456\"" http://localhost:8080/cors

4️⃣ CORS 테스트

서버가 CORS 요청을 올바르게 허용하는지 확인:

curl -H "Origin: http://example.com" -H "Access-Control-Request-Method: GET" --verbose http://localhost:8080/cors

 

7. Summary

FormContentFilter: PUT, PATCH, DELETE 요청에서도 application/x-www-form-urlencoded 폼 데이터 접근 가능
ForwardedHeaderFilter: 프록시 서버를 거친 요청에서 올바른 host, port, scheme 설정 유지
ShallowEtagHeaderFilter: 응답에 ETag 헤더를 추가하여 캐싱 최적화
CorsFilter: 교차 출처 요청을 허용하고 클라이언트가 서버 리소스를 요청할 수 있도록 구성

 

이렇게 구성하면 Spring Boot 기반 웹 애플리케이션에서 주요 필터들을 활용하여 다양한 환경에서의 요청을 효과적으로 처리할 수 있습니다. 🚀

 

출처 : https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html

'Spring Framework > Web on Servlet Stack' 카테고리의 다른 글

Annotated Controllers[3] - @CookieValue  (0) 2024.10.09
@RequestHeader  (0) 2024.10.09
Annotated Controllers[1]  (1) 2024.10.06
WebDataBinder  (0) 2023.05.01
Servlet  (0) 2023.04.17