
@Bean
public Filter jwtFilter(RequestMatcher publicEndPoints,
JwtTokenExtractor jwtTokenExtractor,
JwtTokenProvider jwtTokenProvider) {
return new JwtFilter(publicEndPoints, jwtTokenExtractor, jwtTokenProvider);
}
@Bean 메서드 파라미터 타입을 보고 빈을 찾아 주입해당 빈이 어떤 의존성이 필요한지 시그니처에 드러나서 명확
설정 클래스가 불필요한 필드를 들고 있지 않아 결합도 감소
@Qualifier 등도 파라미터에서 처리하기 쉬움
@Bean
public Filter jwtFilter() {
return new JwtFilter(publicEndPoints, jwtTokenExtractor, jwtTokenProvider);
}
동작은 동일하게 가능
다만 설정 클래스가 커질수록 “보관하는 의존성”이 늘어 설정 클래스 비대화가 생길 수 있음
jwtFilter()만 보면 무엇이 필요한지 덜 명확해질 수 있음
.addFilterBefore(jwtFilter(publicEndpoints()), UsernamePasswordAuthenticationFilter.class);
하지만 “스프링 주입”을 제대로 활용하면 더 깔끔해짐.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
RequestMatcher publicEndpoints,
Filter jwtFilter) throws Exception {
~~
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
publicEndpoints() 메서드를 여러 번 호출해서 조립하는 느낌이 아니라
스프링이 이미 만든 “하나의 빈”을 주입받아 일관되게 사용
@Bean
public SecretKey jwtSigningKey(JwtProperties props) {
byte[] bytes = Base64.getDecoder().decode(props.secret().key());
return Keys.hmacShaKeyFor(bytes);
}
Base64가 깨지면 IllegalArgumentException
키가 짧으면 WeakKeyException (JJWT 내부에서)
원인 파악이 애매해지고, 설정 오류가 런타임/라이브러리 예외로 흩어짐
@Bean
public SecretKey jwtSigningKey(JwtProperties props) {
byte[] bytes;
try {
bytes = Base64.getDecoder().decode(props.secret().key());
} catch (IllegalArgumentException e) {
throw new IllegalStateException("JWT secret must be Base64-encoded", e);
}
if (bytes.length < 32) { // HS256 최소 256-bit
throw new IllegalStateException("JWT secret is too short: minimum 32 bytes required");
}
return Keys.hmacShaKeyFor(bytes);
}
가능. 그리고 이 위치(@Bean 생성)에서 터지면 WeakKeyException도 애플리케이션 시작이 실패하니까 fail-fast 자체는 동일
이건 “요청 처리 중 인증 실패”가 아니라 애플리케이션 설정이 잘못된 상태
IllegalStateException이 의미적으로 더 맞음(상태/환경이 잘못됨)
메시지를 우리 기준으로 고정해서 “원인 파악”이 쉬움
라이브러리 예외 타입에 덜 의존하는 구조가 됨
결론: fail-fast 여부가 아니라, “설정 오류를 설정 단계에서 명확히 표준화한다”는 의미가 크다.
“JwtFilter에서 AuthenticationException 던지면 EntryPoint로 가겠지?”
ExceptionTranslationFilter는 필터 체인에서 발생한 AuthenticationException을 잡아 EntryPoint로 위임함
그런데 ETF가 잡을 수 있는 건 ‘자기 뒤에서 발생한 예외’인 경우가 일반적임
즉,
JwtFilter를 UsernamePasswordAuthenticationFilter 앞에 두면,
보통 ETF보다도 앞쪽에 위치하게 되는 경우가 많아서
JwtFilter에서 던진 예외가 ETF까지 “자동으로” 안 들어가서 EntryPoint가 안 탈 수 있음
entryPoint.commence(...) 호출하고 return예: addFilterAfter(jwtFilter, ExceptionTranslationFilter.class)
결론: “EntryPoint를 쓰겠다”면 필터 순서가 설계의 일부다.
Authentication 예외를 발생시키면 entrypoint에서 처리되지 않을까에서 찾아보다가 BadCredentialsException을 던지는 코드가 있길래 궁금해졌음
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = tokenExtractor.extract(request);
if (token != null) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request,response);
} catch (JwtException | IllegalArgumentException e) {
throw new BadCredentialsException("JWT_INVALID", e);
}
}
“변환”이 아니라 상속 관계
BadCredentialsException은 AuthenticationException의 하위 클래스
따라서 throw new BadCredentialsException(...) 하면 이미 “AuthenticationException 계열”이 던져지는 것
안 됨.
AuthenticationException은 abstract(추상 클래스)라 직접 new 할 수 없음
그래서 구체 하위 클래스(대표적으로 BadCredentialsException)를 사용하거나,
아예 커스텀 AuthenticationException 하위 클래스를 만들기도 함
가장 직관적
대신 Filter가 “응답 작성 책임”까지 가져서 관심사 분리가 약해질 수 있음
Spring Security 표준 패턴에 가까움
인증 실패 응답을 한 곳에서 통일하기 좋음
다만 “JwtFilter에서 발생한 예외가 EntryPoint로 잘 전달되도록(필터 순서/위임 방식)” 설계해야 함
SRP(단일 책임) 관점에서 좋음
재사용 가능(다른 필터/컨트롤러/테스트 등)
단위 테스트가 쉬움
예: JwtTokenExtractor.extract(request)
정말 재사용이 없고 단순함을 최우선으로 하면 OK
대신 테스트/재사용성/책임 분리 측면은 약해짐
프로젝트가 이미 Parser/Provider를 분리한 구조라면 Extractor 분리가 일관성이 좋다
AuthenticationManager.authenticate(...)내부적으로 Provider가 loadUserByUsername(email) 호출
PasswordEncoder로 비밀번호 검증
성공 시 Authentication 반환
JwtFilter가 토큰 검증
토큰에서 userId 추출
loadUserById(id)로 UserDetails 만들고
SecurityContextHolder에 Authentication 세팅
즉,
로그인(비번 검증) 중심
JWT 인증(요청 처리 중 principal 세팅) 중심