AuthenticationManager를 bean등록해야하는 이유
문제 상황
jwt관련하여 발생하는 모든 에러를 403 에러로 반환함
토큰을 누락하는 경우에도 403, 토큰이 유효하지 않은 경우에도 403
개발할때 원인을 찾기 어려울뿐더러 클라이언트에서 구분하기 어려움
고려할점 - 어디까지 에러 코드를 구분할 것인가 (보안차원에서)
authenticationEntryPoint
정의
authenticationEntryPoint: 인증되지 않은 사용자가 보호된 리소스에 접근했을 때 무엇을 할지 정하는 시작 지점
인증되지 않은 사용자란
요청
↓
Security Filter Chain // 필터 체인으로 가장 먼저 호출된다
↓
Authentication 확인 // 인증 정보를 확인한다 토큰 등..
↓
❌ 인증 정보 없음
↓
AuthenticationException 발생 // 인증 정보가 없으므로 예외를 발생시킨다
↓
ExceptionTranslationFilter
↓
AuthenticationEntryPoint 호출 ✅
AuthenticationEntryPoint를 만들지 않으면 모든 에러가 403으로 내려온다
왜 이런 현상이 발생할까 ?
-> Spring Security는 원래 “웹 사이트용” 프레임워크다
그래서 기본 동작이 인증 안되면 로그인 페이지로 다시 보내도록 기본 설정이 되어있다
그럼 여기서 AuthenticationEntryPoint를 만들지 않았다면 기본 설정대로 LoginUrlAuthenticationEntryPoint를 등록한다
이 EntryPoint의 역할은 로그인 페이지로 리다이렉트 시키는 일을 한다
전혀문제될게 없어보이지만 api를 구현한다면 보통 json으로 데이터를 주고 받을 것이라고 예상하지만
스프링 시큐리티는 로그인 페이지로 응답할 것이다 (기본 설정 때문에)
더보기
더보기
//json을 예상했지만 실제 스프링이 응답으로 보낸것
<html>
<form>login</form>
</html>
그러면 파싱에러가 날것이다 기대한 타입과 다르므로
따라서 내가 커스텀하게 EntryPoint을 바꾸는것이 authenticationEntryPoint을 사용하는 것과 같다
주의할점
AuthenticationEntryPoint는 Security Filter Chain 안에서 AuthenticationException이 발생했을 때만 호출된다
즉 다른 에러를 사용해서 다음과 같이 코드가 작성되어 있었기 때문에 인식을 못했다.
RuntimeException / IllegalStateException는 EntryPoint를 호출하지 않는다
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.warn("잘못된 JWT 토큰: {}", e.getMessage());
return false;
}
의문점 1 - EntryPoint 설정을 추가하지 않고 jwt를 예외처리하면 안되나?
AuthenticationEntryPoint가 필요한 이유는 무엇일까
예외처리는 기본적으로 호출한 곳의 상위로 throws 예외를 던지기 때문에 globalException에서 처리할 수 없다
jwt필터는 인증 보다 ,컨트롤러 호출 보다도 앞에서 호출된다.
JwtAuthenticationFilter (OncePerRequestFilter)
↓
UsernamePasswordAuthenticationFilter
↓
AuthorizationFilter
↓
ExceptionTranslationFilter
여기서 살펴볼 것은 예외처리란 무엇인가이다.
숫자를 0으로 나눌때와 같은 예외를 들어보자
이는 “이 입력은 내 책임 범위 밖이다 호출자가 처리해라”를 명시하면서 ArithmeticException를 던진다
그런데 jwt를 보자 null,만료,위조 모두 시스템 오류가 아니다.
다시말해 실패 != 예외임을 헷갈려선 안된다
jwt를 검증할 수 없다 == 보안 판단을 할 수 없다
jwt를 검증할 수 없다 != 심각한 오류가 발생했다
parserBuilder도중 실패 = 라이브러리 오류 = 심각한 오류 발생 = 예외처리 필요
즉 스프링 시큐리티 입장에서 jwt검증 실패는 처리할 수 있는 정상 범위니까 예외가 아니라 판단해야 하는 상태라고 본다
의문점 2 - EntryPoint 설정을 추가하지 않고 jwt에서 AuthenticationException을 던지면 되지 않을까?
필터에서 인증 실패가 발생하면, 바로 다음 단계로 넘어가지 않고 ExceptionTranslationFilter가 잡음
클라이언트 요청 →
SecurityContextPersistenceFilter →
JwtFilter / UsernamePasswordAuthenticationFilter →
ExceptionTranslationFilter →
기타 인증/인가 필터 →
DispatcherServlet →
Controller
그리고 AuthenticationManager 내부에서 발생한 에러만 ExceptionTranslationFilter가 catch
AuthenticationManager authManager = ...;
try {
authManager.authenticate(token); // <- 이 라인에서 실패하면 AuthenticationException 발생
} catch (AuthenticationException ex) {
// Security는 이걸 인증 실패로 간주
}
ExceptionTranslationFilter의 핵심 로직
try {
filterChain.doFilter(request, response); // 다음 필터로 진행
} catch (AuthenticationException ex) {
authenticationEntryPoint.commence(request, response, ex); // 인증 실패 처리
} catch (AccessDeniedException ex) {
accessDeniedHandler.handle(request, response, ex); // 권한 없음 처리
}
필터에서 직접 던지면:
- filterChain.doFilter() 바깥에서 RuntimeException 발생
- ExceptionTranslationFilter는 try 안에서 발생한 인증 시도 실패만 보고 catch
- 그래서 Security 입장에서는 “내가 만든 인증 실패가 아니다” → 그냥 RuntimeException
- 결과: EntryPoint 호출 안 됨
어째서 바깥에서 발생했는가?
말 그대로 doFilter를 호출하기 전에 jwt검증을 끝냈다
그래서 이게 왜 바깥이냐? doFilter 호출로 try-catch를 하기 때문임
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
Authentication authentication = jwtTokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
결론
Spring Security에서 인증 실패를 자동으로 EntryPoint로 연결시키려면 인증 시도가 반드시 AuthenticationManager를 거쳐야 함.
AccessDeniedHandler: 로그인 했지만(=인증 통과) 권한 부족한 경우