Spring Security 예외 처리
기존 예외 처리 (Filter)
CustomException exception = new CustomException(ExceptionCode.FORBIDDEN);
CommonResponse<String> result = new CommonResponse<>(
exception.getExceptionCode().getStatus(), exception.getMessage());
response.setStatus(result.getCode());
response.setContentType("application/json; charset=UTF-8");
String json = objectMapper.writeValueAsString(result);
response.getWriter().write(json);
- Filter에서 응답을 만들어서 보냄
- 다른 Filter가 있다면 중복 코드 발생
- SpringSecurity의 예외 흐름과 분리
- 인증/인가 예외의 책임 분리 되지 않음
공식 사이트
- 인증/인가 예외를 처리할 수 있는 확장 포인트 존재
- 인증 예외 →
AuthenticationEntryPoint
- 인가 예외 →
AccessDeniedHandler
AuthenticationEntryPoint
- 인증되지 않은 사용자가 보호된 리소스에 접근할 때 호출
@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String message = "인증이 필요합니다.";
CommonResponse<Void> commonResponse = new CommonResponse<>(false, message, null);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json; charset=UTF-8");
String json = objectMapper.writeValueAsString(commonResponse);
response.getWriter().write(json);
}
}
AccessDeniedHandler
- 인증은 통과했지만, 권한이 없는 경우 호출
- ex) USER권한을 가진 사용자가 ADMIN API에 접근할 때
@Component
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
String message = "접근 권한이 없습니다.";
CommonResponse<Void> commonResponse = new CommonResponse<>(false, message, null);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json; charset=UTF-8");
String json = objectMapper.writeValueAsString(commonResponse);
response.getWriter().write(json);
}
}
Spring Security Config
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtFilter jwtFilter;
private final CustomUserDetailService customUserDetailService;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.userDetailsService(customUserDetailService)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e ->
e.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users", "/api/auth/login").permitAll()
.anyRequest().authenticated()
)
.build();
}
}
- 위에서 만든 두 Handler를 config에 설정
- Spring Security가 자동으로 분기 처리
전체 흐름
요청
⬇️
JwtFilter (토큰 검증)
⬇️
SecurityContextHolder 설정 여부
⬇️
[인증 실패] ➡️ AuthenticationEntryPoint
[인가 실패] ➡️ AccessDeniedHandler
[정상] ➡️ Controller
- 예외 책임이 명확해짐
- Filter는 인증 로직에만 집중
- Security는 예외 흐름과 자연스럽게 통합
- 응답 포맷을 중앙에서 통제 가능
- JwtFilter에서 예외를 직접 응답하지 말기
a. AuthenticationEntryPoint로 위임하는 구조가 이상적
b. Filter는 throw 또는 SecurityContext 미설정만 담당
- Method Security (@PreAuthorize) 연계
a. @EnableMethodSecurity 사용시 메서드 레벨 권한 예외도 AccessDeniedHandler가 처리