[Security] Security Exception

Security Exception

Spring Security는 filter chain이므로 요청이 컨트롤러에 들어오기 전에 수행이 됩니다.
이러한 특징으로 인해 Security Filter에서 예외가 발생하면 우리가 흔히 사용하는 @ControllerAdvice로 예외처리를 할수가 없습니다.
그래서 Security에서는 두가지의 handler를 사용해서 예외처리를 해줍니다.

AccessDeniedHandler 인터페이스

해당 인터페이스를 구현한 CustomAccessDeniedHandler 객체를 생성하고 security filter에 등록하게 되면 권한이 없는 리소스를 요청했을때 예외를 처리해줍니다.

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {


    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //response.sendRedirect("/fail");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"message\": \"Unauthorized111\"}");
    }
}

AuthenticationEntryPoint 인터페이스

인증이 필요한 리소스에 인증이 되지 않은 사용자의 요청에 대한 처리를 해줍니다. 즉, 인증이 되지 않은 사용자가 리소스를 요청하게 되면 AuthenticationEntryPoint를 구현한 CustomAuthenticationEntryPoint의 commence 메서드가 실행이 되서 리다이렉트 해주거나 json 값을 넘겨줄 수 있습니다.

@RequiredArgsConstructor
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //response.sendRedirect("/fail");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"message\": \"Unauthorized\"}");
    }
}

AuthenticationFailureHandler 인터페이스

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        // 여기에서 인증 실패 시 필요한 처리를 수행합니다.
        // 예를 들어, 에러 페이지로 리다이렉트하거나 에러 메시지를 설정할 수 있습니다.
        response.sendRedirect("/login?error=true");
    }
}

Security Filter에 적용

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final CustomAccessDeniedHandler customAccessDeniedHandler;
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/login","/fail","/register","/css/**").permitAll()
                        .requestMatchers("/success").authenticated()
                        .requestMatchers("/test").hasRole("ADMIN"))
                .formLogin(form -> form
                        .loginPage("/login")
                        .permitAll()
                        .defaultSuccessUrl("/success")
                        .failureHandler(customAuthenticationFailureHandler)
                ).exceptionHandling(ex->ex.accessDeniedHandler(customAccessDeniedHandler)
                        .authenticationEntryPoint(customAuthenticationEntryPoint));
       return http.build();
    }
}