오류 노트

Filter 앞 TomcatFilter

Chemi___6_oj 2024. 12. 12. 00:17

오늘 되게 신기한 경험을 했다!

프로젝트 코드에서는 400번대, 500번대 요청이 오면 프론트에서는 적절한 alert처리를 해주겠지만, 사실 백엔드에서는 단순히 그 에러를 뱉어내기만 했다.

그래서 필요하지 않은 HttpMethod를 제한하기 위한 조치들을 단계적으로 했다.

그래서 이런 보안적인 처치를 어떻게 단계적으로 검토하고 해결했는지 해결 과정을 정리해보려고 한다.

SpringSecurityFIlterChain(2.7 버전 기준)

Spring컨텍스트 내에서 예외를 처리하는 방법이다. 여기서 허용해줄 수 있는 Http메소드를 제한한다.

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests()
                .requestMatchers(HttpMethod.GET, "/api/**").permitAll() // GET 요청 허용
                .requestMatchers(HttpMethod.DELETE, "/api/**").denyAll() // DELETE 요청 차단
                .anyRequest().authenticated() // 그 외 모든 요청 인증 필요
            .and()
            .csrf().disable(); // 선택: CSRF 비활성화

        return http.build();
    }

CORS에서 Http메소드 제한

가장 일반적으로 알고 있는 방법일 거다. 특정 메소드에 대한 요청을 허용하거나, 허용하지 않거나 제한할 수 있다.

@Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));

 

Custom Filter에서 Http메소드 제한

더 앞단에서 처리할 수 있는 방법이다. Interceptor보다 앞인 필터레벨에서 처리하니 여기서 메서드 제한을 해주더라도 맞는 방법이다.

@Component
public class HttpMethodRestrictFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // HTTP 메서드 제한
        String method = httpRequest.getMethod();
        if ("TRACE".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
            httpResponse.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
            httpResponse.getWriter().write("HTTP Method " + method + " is not allowed.");
            return; // 요청 중단
        }

 

TomcatFilter 메소드 제한

위에 있는 방법은 빈으로 등록해서 Http메소드를 처리할 수 있을 거다. 앞서 제안한 1,2,3번 같은 경우에는 필터 레벨에서 해결하고 있어서  가장 알려진 방법일 거다. 하지만, Postman으로 테스트를 해보면, 응답값으로 에러코드 405와 함께 어떤 Html페이지가 나오는 것을 볼 수 있다.

이부분을 구체적으로 분석해 보면 톰캣에서 기본 제공되는 Http페이지가 반환되는 것을 알 수 있다. 대부분 프론트측에서 alert창을 보여주는 등과 같은 방식으로문제를 해결하겠지만, 백엔드에서도 이런 부분을 제거해야 하는 요구사항이 있었다.

그래서 저 방법들을 사용할 수 없었고 Tomcat에 대해서 조금 더 구체적인 처리가 필요하다고 생각해 새로운 방법을 찾았다.

 

공부해보며 알게된 사실이 Tomcat의 가장 큰 특징은 TomcatFIlter는 빈들의 순서와 상관없이 가장 먼저 실행되는 필터라는 점이다. 

구체적으로 톰캣은 먼저 Valve, Connector, Filter가 있다. 요청 순서대로 설명해보자면

1. Connector

Connector는 클라이언트로부터 HTTP요청을 수신하는 첫번째 컴포넌트다.

요청을 파싱하고 내부적인 Tomcat객체로 변환한다. 이때의 객체 이름은 CoyoteRequest다.

여기서 특이한 실험을 하나 했었는데, CoyoteRequest의 메소드 이름은 GET, POST처럼 Method가 아니어도 123, abc처럼 아무 문자조차 메서드명으로 받아들인다.

 

2. Engine -> Host -> Context -> Valve

Enigne

요청을 받아 적절한 가상 호스트로 전달한다. (라우팅 역할, 가상 호스트간의 요청 분리) 

Host

특정 도메인으로 들어오는 요청을 적절한 컨텍스트 (웹 어플리케이션)으로 전달한다.

하나의 톰캣 인스턴스에서 여러 도메인을 처리할 수 있다.

Context

톰캣에서 실행되는 웹 어플리케이션을 의미한다. ex : 서블릿, jsp 필터

Valve

요청 처리 과정에서 특정 작업을 수행한다.

로깅, 인증, 요청 제한 등을 할 수 있음 (AccessLogValve, ErrorReportValve)

이후 설명할 부분에서 가장 핵심이 될 내용이다.

 

3. Filter Chain

Valve를 통과한 요청을 Servlet 필터 체인으로 전달한다.

SpringSecurity를 비롯한 모든 어플리케이션 필터들이 이 단계에서 실행된다.

아까 설명했던 커스텀 필터 등등도 여기서 순서를 정해서 실행되는 것이다.

 

4. Servlet

FIlter chain을 통과한 요청이 servlet로 전달되고 여기서 실제 비즈니스 로직이 처리된다.

 

정리하자면 Tomcat이 요청을 가장 먼저 받아들여서 Http요청의 유효성을 검사, 데이터를 파싱하거나 조작이 가능하고, 인증, 로깅, 접근 제한등의 로우 레벨의 작업을 수행한다. 완전히 내가 찾던 해결 방안이었다.

 

이런 방식대로 문제를 해결하면 아래와 같이 커스텀 필터를 만들어서 처리할 수 있다.

@Component
public class TomcatValveCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addContextCustomizers(context -> {
            context.getPipeline().addValve(new HttpMethodRestrictValve());
        });
    }
}

 

WebSecurityFactoryCustomizer

SpringBoot에서 제공하는 인터페이스로 내장 웹서버의 설정을 커스터마이징 하는데 사용한다.

내장 웹서버의 동작 방식을 변경하거나 세부 설정을 추가

TomcatSevletWebServletFactory

Tomcat을 내장 웹 서버로 사용할 때의 설정을 제공하는 클래스

여기서 내가 커스텀으로 생성해준 Valve(HttpMethodRestrictValve)를 넣어 Http메소드를 조작하고 처리할 예정

public class HttpMethodRestrictValve extends ValveBase {

    @Override
    public void invoke(Request request, Response response) throws IOException {
        String method = request.getMethod();

        // 제한된 HTTP 메서드
        if ("PUT".equalsIgnoreCase(method) || 
            "DELETE".equalsIgnoreCase(method) || 
            "PATCH".equalsIgnoreCase(method)) {
            response.setStatus(Response.SC_METHOD_NOT_ALLOWED);
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("Custom Error: HTTP Method " + method + " is not allowed."); // 메시지 작성
            response.getWriter().flush();
            return;
        }

        // 다음 Valve로 요청 전달
        getNext().invoke(request, response);
    }
}

 

 

하지만 여전히 문제점이 남아있었다.

SpringSecurity가 이해하는 일반적인 HTTPMethod는 위와 같은 방식으로 해결이 가능하지만, TRACE요청 같은 경우에는 스프링 시큐리티가 인식하지 못하기 때문에 RequestRejectedException이 발생한다. (스프링 시큐리티 발생 에러)

그런데 위와 같은 방법으로도 Tomcat Filter에서 이걸 잡아서 처리해주지 못했다는 건데, 여기에서 아무리 여러 방법들을 시도했지만 실패했었다.

 

실패한 내용으로는

- new ValveBase를 사용해서 TRACE로 오는 요청을 잡아서 처리 시도 -> 실패

- 스프링 시큐리티에서 TRACE요청을 잡아 처리 시도 -> 해결은 될 수 있겠으나 톰캣의 실패 페이지가 뜰 거임

- 위에처럼 HttpREstrictValue에 TRACE 관련 조건절 추가 - > 실패

별 처리르 다해줘도 다 막혔다.

 

구현체를 찾아보니 톰캣 9버전 기준 Http11Process클래스의 service 메소드에 이유가 있었다.

if (method.equalsIgnoreCase("TRACE")) {
    if (connector.getAllowTrace()) {
        // TRACE 요청을 허용하는 경우 처리
        handleTraceRequest();
    } else {
        // TRACE 요청을 허용하지 않는 경우 405 반환
        response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
}

이런식으로 SpringSecurity처럼 기본적으로 요청을 먼저 거부하는 처리가 들어있다.

이래서 뒤에서 아무리 TRACE요청을 처리해줘도 여전히 html페이지가 나오고 있었다.

 

그래서 아예 해결 방법을 TRACE요청을 허용으로 바꿔준 다음 내가 뒤에서 직접 처리할 수 있도록 문제를 찾아 해결했다.

@Component
public class TomcatDisableTraceCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addConnectorCustomizers(connector -> {
            connector.setAllowTrace(true); 
        });
    }
}

 

그리고 HttpRestrictValve에서 TRACE에 관한 조건절도 추가해주면 해결 완료!

 

배운점

역시 기본기가 있어야 문제 해결의 실마리를 쉽게 찾을 수 있다는게 놀라웠다.

인터셉터보다 필터가 앞.

필터의 경우는 스프링 컨텍스트를 타지 않는다는 점,

그리고 이 필터들에는 순서를 마련해줘야하는 점,

그리고 그보다 앞에서 요청하는 TomcatFilter가 최우선이라는 점,

구현체를 뜯어보면 답이 나온다는 점

 

등등 지난 경험들이 역시 쌓이면서 문제 해결 속도가 이전보다 훨씬 빨라짐을 체감할 수 있었다!!