[Spring Security] Filter에서 발생하는 예외들을 모두 처리해보자

Doyeon·2023년 3월 1일
0
post-thumbnail
post-custom-banner

JWT 토큰을 검증해서 인증된 사용자 객체를 저장하는 스프링 시큐리티를 적용했다. 시큐리티 필터 내에서 인증 과정을 진행하던 중에 exception이 발생한 경우, 상황에 따라 exception 처리를 할 수 있도록 AuthenticationEntryPoint 를 구현하여 예외처리를 해보자!

Security Filter 예외처리

  • Security Filter는 보안을 강화하기 위해 애플리케이션의 요청을 검사한다.
  • Security Filter 내 예외 처리 방법
    • try-catch 구문을 사용해서, @ExceptionHandler 로 처리할 수 있다.
    • ExceptionTranslationFilter를 통해 Security Filter 내 예외처리를 한다. 이 필터는 보호된 리소스에 접근할 때 발생하는 예외를 처리하며, AuthenticationEntryPoint 를 사용하여 인증되지 않은 사용자를 처리할 수 있다.

AuthenticationEntryPoint

  • Spring Security에서 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때 호출되는 필터이다.
  • 인증되지 않은 사용자를 처리하기 위한 매커니즘을 제공한다.
  • 사용자를 로그인 페이지로 리다이렉트시키거나, HTTP 401 Unauthorized 에러를 리턴하도록 구성할 수 있다.

설계

  • 예외가 발생하는 경우
    • 1) token이 없는 경우
    • 2) token이 유효하지 않은 경우
    • 3) 사용자 인증 객체를 만들기 위해 사용자를 DB에서 조회했는데, 조회결과가 없는 경우
  • 예외가 발생하는 지점에서 HttpServletRequest의 속성으로 { “exception” : ErrorType } 을 넣어준다.
    request.setAttribute("exception", ErrorType.NOT_TOKEN);
  • AuthenticationEntryPoint 를 구현한 클래스에서 request의 속성 중 “exception” 값을 보고 해당하는 예외에 맞게 처리해준다.
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ErrorType exception = (ErrorType) request.getAttribute("exception");
    
        if (exception.equals(ErrorType.NOT_TOKEN)) {
            exceptionHandler(response, ErrorType.NOT_TOKEN);
            return;
        }
    }
    
    public void exceptionHandler(HttpServletResponse response, ErrorType error) {
        response.setStatus(error.getCode());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        try {
            String json = new ObjectMapper().writeValueAsString(ResponseUtils.error(ErrorResponse.of(error)));
            response.getWriter().write(json);
            log.error(error.getMessage());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

구현

  • JwtAuthFilter
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // request 에 담긴 토큰을 가져온다.
        String token = jwtUtil.resolveToken(request);

        // 토큰이 null 이면 다음 필터로 넘어간다
        if (token == null) {
            request.setAttribute("exception", ErrorType.NOT_TOKEN);
            filterChain.doFilter(request, response);
            return;
        }

        // 토큰이 유효하지 않으면 다음 필터로 넘어간다
        if (!jwtUtil.validateToken(token)) {
            request.setAttribute("exception", ErrorType.NOT_VALID_TOKEN);
            filterChain.doFilter(request, response);
            return;
        }

        // 유효한 토큰이라면, 토큰으로부터 사용자 정보를 가져온다.
        Claims info = jwtUtil.getUserInfoFromToken(token);
        try {
            setAuthentication(info.getSubject());   // 사용자 정보로 인증 객체 만들기
        } catch (UsernameNotFoundException e) {
            request.setAttribute("exception", ErrorType.NOT_FOUND_USER);
        }
        // 다음 필터로 넘어간다.
        filterChain.doFilter(request, response);

    }

    private void setAuthentication(String loginId) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(loginId); // 인증 객체 만들기
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

}
  • CustomAuthenticationEntryPoint
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ErrorType exception = (ErrorType) request.getAttribute("exception");

        if (exception.equals(ErrorType.NOT_TOKEN)) {
            exceptionHandler(response, ErrorType.NOT_TOKEN);
            return;
        }

        if (exception.equals(ErrorType.NOT_VALID_TOKEN)) {
            exceptionHandler(response, ErrorType.NOT_VALID_TOKEN);
            return;
        }

        if (exception.equals(ErrorType.NOT_FOUND_USER)) {
            exceptionHandler(response, ErrorType.NOT_FOUND_USER);
        }
    }

    public void exceptionHandler(HttpServletResponse response, ErrorType error) {
        response.setStatus(error.getCode());
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        try {
            String json = new ObjectMapper().writeValueAsString(ResponseUtils.error(ErrorResponse.of(error)));
            response.getWriter().write(json);
            log.error(error.getMessage());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}
profile
🔥
post-custom-banner

0개의 댓글