Engineering Note

[Spring] OncePerRequestFilter 본문

Server

[Spring] OncePerRequestFilter

Software Engineer Kim 2025. 12. 21. 11:48

OncePerRequestFilter

  • Spring Framework에서 제공하는 클래스로, 하나의 요청당 딱 한 번만 실행하게 하는 필터

 

웹 어플리케이션은 서블릿이 요청을 받아 처리할 때 다른 서블릿으로 요청을 Forwad하거나 리다이렉트 하는 등의 작업이 일어나는데, 이때Filter 인터페이스만 사용하면 서블릿 컨테이너(Tomcat 등)의 설정에 따라 필터가 중복해서 실행될 수 있는데 이를 방지하기 위해 사용합니다.

 

 

OncePerRequestFilter가 필요한 이유(배경)

  • 문제점 : 인증(Authentication)이나 로깅(loggin) 요청처럼 한 번만 수행되어야 하는 작업이 중복 실행되어서 자원을 낭비하거나 로직의 오류가 발생할 수 있습니다.
  • 해결책 : OncePerRequestFilter를 상속받으면, 해당요청이 이 필터를 거쳤는지 확인하는 로직이 내장되어 있어 자동으로 중복 실행을 차단해줍니다.

 

 

주요 특징

  • 동일 요청 내 1회 실행 보장: doFilterInternal 메서드만 구현하면 됩니다.
  • Spring Security와의 궁합: JWT 토큰 인증처럼 요청마다 사용자 정보를 확인해야 하는 필터를 만들 때 표준처럼 사용됩니다.
  • 유연한 제어: 특정 조건(URL 패턴)에 따라 필터를 거치지 않게 설정할 수 있는 shodnotFilter 메서드를 제공합니다.

 

 

JWT 토큰 인증 코드를 구현한 예시 코드

package com.shop.core.security;

import com.shop.common.constant.Role;
import com.shop.domain.member.Member;
import com.shop.domain.member.MemberRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

/**
 * JWT 인증 필터
 * 모든 HTTP 요청에서 JWT 토큰을 검증하고 인증 정보를 SecurityContext에 저장
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
        private final JwtTokenProvider jwtTokenProvider;
        private final MemberRepository memberRepository;

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            try {
                // 1. Request Header에서 JWT 토큰 추출
                log.info("Request Header Authorization: {}", request.getHeader("Authorization"));
                String token = extractTokenFromRequest(request);

                // 2. 토큰이 있고, 유효하면 인증정보 설정
                if(token != null && jwtTokenProvider.validateToken(token)) {
                    // 토큰에서 memberId 추출
                    Long memberId = jwtTokenProvider.getMemberId(token);
                    String email = jwtTokenProvider.getEmail(token);
                    Member member = memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException(email));

                    //회원 권한 설정
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(memberId, null, List.of(new SimpleGrantedAuthority("ROLE_" + member.getRole().USER.name())));

                    //Security 인증정보 저장
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    log.debug("JWT 인증 성공: memberId = {}, email = {} ", memberId, email);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            //다음 필터로 진행
            filterChain.doFilter(request, response);
    }

    /**
     * Request Header에서 Bearer Token 추출
     */
    private String extractTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);//"Bearer " 제거
        }

        return null;
    }
}
Comments