JWT 인증 필터의 동작 흐름 (OncePerRequestFilter)

최정윤·2026년 8월 21일

Spring

목록 보기
34/38

JWT 로그인은 만들었지만, "토큰이 실제로 어느 순간에 검증되고
컨트롤러는 어떻게 로그인 사용자를 아는지"가 흐릿했다.

이번 과정의 핵심은 "필터를 등록하는 것"이 아니라,
요청 한 번이 필터를 거쳐 인증되는 흐름 전체를 이해하는 것이었다.


1. 핵심 원리

JWT 인증은 컨트롤러가 아니라 필터 단계에서 끝난다. 흐름은 이렇다.

요청 (Authorization: Bearer xxx)
      │
JwtAuthenticationFilter (OncePerRequestFilter)
   1) 헤더에서 토큰 추출
   2) 토큰 검증 (jwtUtil.isValid)
   3) 유효하면 CustomUserPrincipal 생성
   4) SecurityContextHolder에 인증 객체 심기
      │
filterChain.doFilter (다음 필터 → 컨트롤러)
      │
Controller (@AuthenticationPrincipal 로 사용자 꺼내 씀)

필터가 "이 요청은 누구다"를 SecurityContext에 등록해두면, 그 뒤의 인가 규칙과 컨트롤러가 그 정보를 그대로 활용한다.


2. 코드로 보는 흐름

@Override
protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response, FilterChain filterChain) {

    String token = resolveToken(request);                 // 1) "Bearer " 떼고 토큰 추출

    if (token != null && jwtUtil.isValid(token)) {        // 2) 검증
        Long memberId = jwtUtil.getMemberId(token);
        String email  = jwtUtil.getEmail(token);

        CustomUserPrincipal principal =                   // 3) 사용자 표현 객체
                new CustomUserPrincipal(memberId, email, "USER");

        UsernamePasswordAuthenticationToken authentication =
                UsernamePasswordAuthenticationToken.authenticated(
                        principal, null,
                        List.of(new SimpleGrantedAuthority("ROLE_USER")));

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);        // 4) 인증 객체 심기
        SecurityContextHolder.setContext(context);
    }

    filterChain.doFilter(request, response);              // 다음 단계로
}

토큰이 없거나 틀리면 아무것도 심지 않고 그냥 통과시킨다. 필터가 직접 막지 않는다는 게 포인트다(뒤의 인가 규칙이 판단).


3. 기술적 의사결정

  • OncePerRequestFilter인가 — 이름 그대로 요청 한 번당 필터가 딱 한 번 실행되도록 보장한다. forward/include 같은 내부 디스패치로 필터가 중복 실행돼 인증을 두 번 하는 걸 막아준다.
  • 왜 필터에서 심는가 — 인가 규칙(authorizeHttpRequestsauthenticated())은 SecurityContext를 보고 통과 여부를 정한다. 그래서 인가가 판단하기 전에 인증이 끝나 있어야 한다. 그래서 addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)로 이 필터를 앞쪽에 끼웠다.
  • 왜 토큰 없으면 그냥 통과시키나 — 필터의 책임은 "차단"이 아니라 "신원 확인"이다. 막는 건 인가의 몫이고, 인증이 필요한데 안 된 요청은 JwtAuthenticationEntryPoint가 401을 낸다. 역할을 분리한 것이다.

4. 헤맸던 지점

헷갈렸던 것이해한 내용
컨트롤러가 로그인 사용자를 어떻게 아는가필터가 SecurityContext에 심은 principal을, 컨트롤러가 @AuthenticationPrincipal로 꺼내 쓰는 것
createEmptyContext() + setContext() 패턴기존 컨텍스트를 직접 건드리지 않고 새 컨텍스트를 만들어 교체하는 게 Spring Security 권장 방식
authenticated(...) 정적 팩토리"이미 인증 완료된" 토큰을 만드는 것(자격증명은 null, 권한만 부여)

인증은 컨트롤러에 도달하기 전에 필터에서 이미 끝나 있고, 컨트롤러는 그 결과를 받아 쓰기만 한다는 걸 알게 됐다.


5. 마무리

JWT 인증은 OncePerRequestFilter를 상속한 필터에서, 요청당 한 번 토큰을 꺼내 검증하고
성공하면 SecurityContextHolder에 인증 객체를 심는 방식으로 동작한다.
필터는 신원만 확인하고, 실제 차단(401)은 인가 단계와 EntryPoint가 맡는다.
덕분에 세션 없이(stateless) 매 요청을 토큰만으로 인증할 수 있다.

한 줄 요약

JWT 필터는 요청당 한 번, 토큰을 검증해 SecurityContext에 사용자를 심는다.
막는 건 필터가 아니라 인가 규칙이고, 컨트롤러는 그 결과를 @AuthenticationPrincipal로 받아 쓴다.

profile
콩떡

0개의 댓글