Spring Security에서 JWT를 연동할 때 가장 핵심이 되는 컴포넌트는 JwtAuthenticationFilter다.
매 요청마다 토큰을 검사하고 사용자를 식별하는 이 '문지기'가 어떻게 동작하고, 왜 즉시 에러를 내뱉지 않는지 그 설계 철학을 정리한다.
1. 핵심 개념 1: OncePerRequestFilter (중복 방어)
1) 개념 정의
Spring Security 필터 체인 내에서 "하나의 요청당 단 한 번"만 실행되도록 보장하는 필터다.
2) 왜 필요한가?
인증 로직은 토큰 파싱, 서명 검증, DB 조회 등 비용이 발생하는 작업이다. 서블릿 내부 포워딩이나 에러 페이지 이동 시 인증 필터가 여러 번 실행되면 불필요한 리소스가 낭비되고, 인증 상태의 일관성이 깨질 위험이 있다.
3) 실무 포인트
- 헤더가 없거나 형식이 틀린 요청은 인증을 시도하지 않고 즉시 다음 필터로 넘긴다(Fast Pass).
- 보호된 자원인지 아닌지는 이 필터가 판단하지 않는다. 그건 나중의 '인가(Authorization)' 단계에서 처리한다.
2. 핵심 개념 2: SecurityContext (인증 상태의 보관소)
1) 개념 정의
현재 실행 중인 요청 스레드의 인증 정보(Authentication)를 담는 저장소다.
2) 왜 필요한가?
- 컨트롤러에서 @AuthenticationPrincipal이나 커스텀 @CurrentUser 어노테이션을 통해 현재 사용자 정보를 꺼내 쓰려면 반드시 이곳에 데이터가 있어야 한다.
- SecurityConfig에서 설정한 권한 체크(hasRole(), authenticated()) 로직이 이 컨텍스트를 기준으로 동작한다.
3) 주의점
- 오염 방지: 유효하지 않은 토큰이나 존재하지 않는 유저인데도 Authentication 객체를 채워 넣으면 보안 사고다.
- 모든 검증(토큰+유저상태)이 완벽히 끝난 시점에만 데이터를 주입해야 한다.
3. 핵심 개념 3: Authorization 헤더 파싱

1) 개념 정의
Authorization: Bearer <token> 형식의 HTTP 헤더에서 순수 JWT 문자열만 추출하는 단계다.
2) 실패 시 정책
- 헤더가 없거나 Bearer 로 시작하지 않는 경우: "인증 대상이 아님"으로 간주한다.
- 이때 예외를 던지지 않고 chain.doFilter()를 호출해 요청을 다음 단계로 넘긴다.
4. 통합 코드 가이드: JwtAuthenticationFilter

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 헤더 추출 및 최소 형식 확인
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = header.substring(7); // "Bearer " 제거
// 2. 토큰 유효성 검증 (서명, 만료, 구조)
if (!jwtTokenProvider.validateToken(token)) {
filterChain.doFilter(request, response);
return;
}
// 3. Subject 파싱 (provider:oauthId)
String subject = jwtTokenProvider.getSubject(token);
String[] parts = subject.split(":");
if (parts.length != 2) {
filterChain.doFilter(request, response);
return;
}
OauthProvider provider = OauthProvider.valueOf(parts[0]);
String oauthId = parts[1];
// 4. 사용자 상태 재조회
User user = userRepository.findByOauthProviderAndOauthId(provider, oauthId).orElse(null);
if (user == null) {
filterChain.doFilter(request, response);
return;
}
// 5. Authentication 생성 및 SecurityContext 주입
CustomUserDetails userDetails = new CustomUserDetails(user);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// 6. 다음 체인 진행
filterChain.doFilter(request, response);
}
}
5. 왜 필터에서 바로 401 에러를 주지 않는가?
필터 내부에서 인증 실패 시 즉시 에러 응답을 보내지 않고 다음 필터로 넘기는 이유는 '역할 분리' 때문이다.
- JwtAuthenticationFilter: "인증을 시도"하는 역할. 성공하면 컨텍스트를 채우고, 실패하면 비운 채로 넘긴다.
- SecurityConfig & ExceptionTranslationFilter: "인가 정책"을 결정하는 역할. 특정 API가 authenticated()를 요구하는데 컨텍스트가 비어있다면, 여기서 최종적으로 401(Unauthorized)을 판단한다.
이 구조 덕분에 로그인 없이도 접근 가능한 공개 API(permitAll)는 토큰 없이도 필터를 무사히 통과해 정상 작동할 수 있다.
6. 시나리오별 요청 흐름 정리
| 시나리오 | 필터 동작 | 인가(Authorization) 결과 | 최종 응답 |
| 공개 API + 토큰 없음 | 인증 미설정 후 통과 | permitAll이므로 통과 | 200 OK |
| 보호 API + 토큰 없음 | 인증 미설정 후 통과 | authenticated() 위반 | 401 Unauthorized |
| 보호 API + 만료된 토큰 | 검증 실패 후 통과(미설정) | authenticated() 위반 | 401 Unauthorized |
| 보호 API + 유효한 토큰 | 인증 설정(Context 주입) | 인가 조건 충족 | 200 OK |
7. 운영 보안 고도화 포인트
- 실패 원인 Enum 코드화: 필터 내부 로그나 메트릭을 위해 INVALID_SIGNATURE, TOKEN_EXPIRED 등 실패 원인을 상세히 관리한다.
- shouldNotFilter 정책: /health, /v3/api-docs 등 인증이 전혀 필요 없는 경로는 필터의 shouldNotFilter를 오버라이드하여 성능을 최적화한다.
- 메트릭 수집: 필터 통과/실패 카운트를 수집하면 운영 환경에서 비정상적인 인증 시도(브루트 포스 등)를 모니터링할 수 있다.
최종 요약
JwtAuthenticationFilter는 인증을 강제하는 곳이 아니라, 인증을 '시도하고 증명'하는 곳이다.
provider:oauthId 규약을 통해 신뢰할 수 있는 사용자임을 확인하고 이를 SecurityContext에 담아주는 것만으로도 컨트롤러와 서비스 레이어는 보안 걱정 없이 비즈니스 로직에 집중할 수 있다.
'Spring > Security' 카테고리의 다른 글
| [Spring Security] OAuth2 로그인 최종장: 토큰 정책과 앱 이동 흐름 (0) | 2026.05.11 |
|---|---|
| [Spring Security] OAuth2 로그인 성공 후: 신규와 기존 사용자를 가르는 설계의 모든 것 (0) | 2026.05.11 |
| [Spring Security] 운영 보안 설계 : subject 규약과 Access/Refresh 토큰 분리 (0) | 2026.05.09 |
| [Spring Security] 보안 정책의 설계도, SecurityConfig 핵심 코드 이해 (0) | 2026.05.06 |
| [Spring Security] OAuth2 로그인부터 JWT 인증까지: 전체 흐름 정리 (0) | 2026.05.06 |