Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20240802 Filter 본문

개발Article

[TIL]20240802 Filter

최밤빵 2024. 8. 2. 23:55

팀 프로젝트를 통해 협업을 하면서 다른 팀원분들과의 차이를 하루하루 체감하면서 다른 팀원이 작성한 필터클래스의 코드를 보고 내가 잘 다루지 못하는 부분이라 정리를 해야겠다고 마음먹었다. 사실 너무 잘하시는 분이 작성한 코드기도 하고 JWT관련부분이라 코드를 보면서 답답함과 꺼버리고싶은 마음이 더 컸지만..! 그래도 나도 필터클래스는 작성할 줄 알아야하니까.. 오늘은 필터에 대한 내용을 소재로 개발일지를 작성했다. 

🥺필터(Filter) 클래스란?

필터(Filter)는 클라이언트의 요청을 처리하기 전이나 응답을 클라이언트에게 전달하기 전에 실행되는 컴포넌트이다. 필터는 보통 서블릿(Servlet) 또는 스프링의 DispatcherServlet과 같은 프론트 컨트롤러 전에 실행되고, 웹 애플리케이션에서 요청 및 응답의 전처리와 후처리를 가능하게 한다. 필터는 다양한 목적을 가지고 활용될 수 있고, 사용자가 접근할 수 있는 리소스에 대한 전처리 작업을 수행하는 데 매우 유용하다.

 

▶필터의 주요 역할

필터의 주요 역할은 다음과 같다:

 

→ 보안 및 인증/인가 처리:

필터는 사용자가 요청한 리소스에 대해 접근 권한이 있는지를 확인하는 인증(Authentication) 및 인가(Authorization) 작업을 수행할 수 있다. 예를 들어, JWT(Json Web Token) 또는 OAuth 토큰을 검증하는 필터를 구현하여 보안을 강화할 수 있다.

→ 로깅 및 감사(Audit) 작업:

필터를 사용하여 요청 및 응답의 로그를 기록할 수 있다. 이를 통해 애플리케이션의 트래픽과 사용자 활동을 모니터링하고 분석할 수 있다.

→ 데이터 압축 및 인코딩 처리:

필터를 사용하여 요청 또는 응답 데이터를 압축하거나 특정 인코딩 방식을 적용할 수 있다. 예를 들어, GZIP 압축 필터를 사용하여 응답 데이터의 크기를 줄일 수 있다.

→ 입력 검증 및 변환:

필터는 사용자의 입력을 검증하고 변환하는 데 사용될 수 있다. 예를 들어, XSS(Cross-Site Scripting)나 SQL Injection과 같은 공격을 방지하기 위해 사용자 입력을 필터링할 수 있다.

→ 캐싱(Cache) 처리:

필터를 사용하여 요청 또는 응답을 캐싱하여, 서버 부하를 줄이고 애플리케이션의 성능을 향상시킬 수 있다.

 

▶ 필터의 동작 원리

필터는 서블릿 컨테이너(예: Tomcat, Jetty)나 Spring Framework의 FilterChain을 통해 작동한다. 필터는 클라이언트의 요청이 서버에 도달하기 전, 또는 응답이 클라이언트에게 도달하기 전에 중간에 개입하여 특정 로직을 처리한다.

필터는 요청(Request)을 가로채서, 전처리 작업을 수행한 후 서블릿으로 요청을 전달하거나, 응답(Response)에서 후처리 작업을 수행한다. doFilter() 메서드는 필터의 핵심 메서드로, 모든 요청과 응답을 처리할 수 있는 로직을 구현한다.

여러 개의 필터가 존재할 경우, 필터 체인(Filter Chain)에 따라 순차적으로 실행되며, 필터의 실행 순서는 web.xml 파일 또는 Spring Boot의 경우 @Order 애너테이션을 사용하여 설정할 수 있다.

 

▶ 간단한 필터 클래스 구현 예시

Java Servlet 기반의 간단한 필터 클래스 구현 예시로. 이 필터는 요청을 가로채서 인증 토큰을 확인하는 역할을 한다.

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(urlPatterns = "/*") // 모든 요청에 대해 필터 적용
public class AuthenticationFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 작업 (필요할 경우)
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Authorization 헤더에서 인증 토큰을 가져옴
        String authToken = httpRequest.getHeader("Authorization");

        // 인증 토큰이 없는 경우 요청 차단
        if (authToken == null || !validateToken(authToken)) {
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized Access");
            return;
        }

        // 다음 필터 또는 서블릿으로 요청 전달
        chain.doFilter(request, response);
    }

    private boolean validateToken(String token) {
        // 간단한 토큰 검증 로직 (예시)
        return token.equals("valid-token");
    }

    @Override
    public void destroy() {
        // 필터 종료 시 수행할 작업 (필요할 경우)
    }
}

@WebFilter 애너테이션을 사용하여 모든 URL 패턴에 대해 필터를 적용하도록 설정한다. doFilter() 메서드는 FilterChain을 통해 요청이 필터를 통과할 수 있도록 처리하고, 인증되지 않은 요청은 HttpServletResponse.sendError()를 사용해 차단한다. validateToken() 메서드는 간단한 토큰 검증 로직을 포함하고 있으며, 실제 환경에서는 더 복잡한 인증 로직을 구현할 수 있다.

 

▶ Spring Boot에서 필터 클래스 구현 예시

Spring Boot 애플리케이션에서는 OncePerRequestFilter를 상속하여 필터를 구현할 수 있다. Spring Boot에서 JWT 토큰을 검증하는 JwtFilter 클래스의 예시로 이번 프로젝트에서 사용하고 있는 필터클래스를 예시로 가져왔다. 

package com.example.dischord.global.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.text.ParseException;

public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil; // JWT 유틸리티 클래스
    private final UserDetailsService userDetailsService; // 사용자 정보 로드 서비스

    // JwtFilter 생성자
    public JwtFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // Authorization 헤더에서 JWT 토큰 추출
        String authorizationHeader = request.getHeader("Authorization");

        String token = null;
        String email = null;

        // JWT 토큰이 존재하고 "Bearer "로 시작하는 경우 추출
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7);
            try {
                // 토큰에서 이메일 정보 추출
                email = jwtUtil.getEmailFromToken(token);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }

        // SecurityContext에 인증 정보가 없고, 이메일이 존재하는 경우
        if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            try {
                // 토큰이 만료되지 않은 경우
                if (!jwtUtil.isTokenExpired(token)) {
                    // 사용자 정보 로드
                    UserDetails userDetails = userDetailsService.loadUserByUsername(email);
                    // 토큰이 유효한 경우 SecurityContext에 인증 정보 설정
                    if (jwtUtil.validateToken(token)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }

        // 다음 필터나 서블릿으로 요청 전달
        filterChain.doFilter(request, response);
    }
}

JwtFilter 클래스는 OncePerRequestFilter를 상속받아 한 요청당 한 번씩 실행되는 필터이다. doFilterInternal() 메서드에서 Authorization 헤더를 통해 JWT 토큰을 추출하고, 이를 검증하여 유효한 경우 SecurityContext에 사용자 인증 정보를 설정한다. JWT 토큰 검증 로직은 JwtUtil 클래스를 통해 처리하며, 실제 검증 로직은 상황에 따라 달라질 수 있다.

 

▶ 필터 클래스 사용 시 주의사항

 

→ 필터 순서 관리:

여러 개의 필터가 등록된 경우, 필터의 순서를 적절히 관리해야 한다. 필터의 순서는 요청의 처리 순서에 영향을 미치며, @Order 애너테이션이나 FilterRegistrationBean을 사용하여 순서를 정의할 수 있다.

→ 예외 처리:

필터에서 예외가 발생하면 필터 체인의 나머지 부분이 실행되지 않으므로, 예외를 적절히 처리해야 한다. 일반적으로 응답 코드와 함께 적절한 메시지를 반환하는 것이 좋다.

→ 성능 최적화:

필터는 모든 요청에 대해 실행되기 때문에, 성능에 영향을 줄 수 있다. 필터 로직을 최대한 간단하고 효율적으로 작성하는 것이 중요하다. 특히 데이터베이스 조회나 외부 API 호출 등 시간이 많이 소요되는 작업은 지양해야 한다.

→ 보안 고려:

보안 관련 필터를 작성할 때는, 가능한 모든 경로와 입력을 철저히 검증해야 한다. 필터는 첫 번째 방어선이기 때문에, 제대로 구성되지 않으면 보안 취약점이 될 수 있다.

→ 필터와 인터셉터 구분:

Spring Framework에서는 필터(Filter)와 인터셉터(Interceptor)를 혼동하지 않도록 주의해야 한다. 필터는 서블릿 레벨에서 작동하고, 인터셉터는 Spring MVC 레벨에서 작동한다. 각 기술의 목적과 사용 시점을 이해하고 적절히 선택해야 한다.

 

▶ 마무리

필터 클래스의 개념과 역할, 구현 방법, 그리고 사용 시 주의사항에 대해 다뤘다. 필터는 웹 애플리케이션에서 보안, 로깅, 데이터 변환 등 다양한 작업을 수행하는 강력한 도구이다. JwtFilter와 같은 필터를 통해 요청을 가로채고 보안 검증 로직을 수행할 수 있다. 필터를 효과적으로 활용하면 애플리케이션의 보안성과 유지보수성을 크게 향상시킬 수 있다. GPT가 작성해준 필터클래스와 진행하고있는 팀 프로젝트에서 예시로 가져온 필터클래스를 뜯어보면서 나중에 개인프로젝트 때 구현할 수 있지않을까란 기대를 조금 하고있다..!