본문 바로가기
📕 Spring Framework/Spring 개념 정리

Spring Security [2] - 예외 처리 AuthenticationEntryPoint & AccessDeniedHandler

by GroovyArea 2022. 8. 21.
서큐리티를 도입하며 인증, 인가의 과정을 마쳤다. 

이제 인증 및 인가 작업에서 발생하는 예외에 대해서 처리를 해주어야 하는데, 한 가지 생각해봐야 하는 문제가 있다.

스프링 서큐리티는 필터에 기반한 체이닝 구조이므로, 스프링 컨테이너까지 요청이 도달하지 않는다.
따라서, @ExceptionHandler를 통한 편한 예외 처리가 불가하다. 직접 Try - catch로 잡아서 응답을 내려주는 방법 밖엔 없다. 

하지만, 스프링 서큐리티가 그렇게 허술하진 않다. 서큐리티 필터 체인의 구조를 보면 마지막 즈음에 예외를 처리하는 필터가 있는 것을 확인할 수 있다. 

여기서 주로 사용하는 AuthenticationEntryPoint와 AccessDeniedHandler를 구현해 인증 및 인가 과정에서 일어난 예외에 대한 응답을 내려줄 수 있다.

필터에서만 동작하는게 서큐리티의 구조

 

인증 관련 예외 처리 AuthenticationEntryPoint

인증 관련 작업을 하며 수많은 예외를 처리하거나 던졌다.

예를 들어,

  • UserDetails 객체를 생성하기 위해 DB에서 find를 할 때의 예외 - RuntimeException 던짐
  • JWT 인증 과정 중 토큰 유효성 검사의 수많은 예외 - IllegalArgumentException 던짐

 

public boolean validateAccessToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(secretKey.getBytes()).build().parseClaimsJws(token);
        return true;
    } catch (MalformedJwtException e) {
        throw new IllegalArgumentException(JwtErrorMessage.MALFORMED.getMessage());
    } catch (ExpiredJwtException e) {
        throw new IllegalArgumentException(JwtErrorMessage.EXPIRED.getMessage());
    } catch (UnsupportedJwtException e) {
        throw new IllegalArgumentException(JwtErrorMessage.UNSUPPORTED.getMessage());
    } catch (ClassCastException e) {
        throw new IllegalArgumentException(JwtErrorMessage.CLASS_CAST_FAIL.getMessage());
    } catch (SignatureException e) {
        throw new IllegalArgumentException(JwtErrorMessage.INVALID_SIGNATURE.getMessage());
    } catch (Exception e) {
        log.error("================================================ \n" +
                "JwtValidator - validateAccessToken() 오류발생 \n" +
                "token : " + token +
                "\n Exception Message : " + e.getMessage() + "\n " +
                "================================================");
        throw new IllegalArgumentException(e.getMessage());
    }
}

=> JWT 유효성 검사에서 예외 던지기

 

User dbUser = userRepository.findByLoginId(loginId).orElseThrow(() -> new UsernameNotFoundException(ResponseMessages.USER_NOT_EXISTS_MESSAGE.getMessage()));

=> UserDetails 객체 생성 과정 중 코드 -> 동일하게 예외를 던진다.

 

이러한 예외를 어디선가 받아서 응답을 내려주어야 한다.

이때, AuthenticationEntryPoint 클래스를 이용해 예외 처리가 가능하다.

 

@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

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

        List<String> list = Arrays.stream(JwtErrorMessage.values())
                .map(JwtErrorMessage::getMessage)
                .collect(Collectors.toList());

        if (!list.contains(exceptionMessage)) {
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().print(exceptionMessage);
        }

        if (exceptionMessage.equals(JwtErrorMessage.MALFORMED.getMessage())) {
            setResponse(response, JwtErrorMessage.MALFORMED);
        } else if (exceptionMessage.equals(JwtErrorMessage.CLASS_CAST_FAIL.getMessage())) {
            setResponse(response, JwtErrorMessage.CLASS_CAST_FAIL);
        } else if (exceptionMessage.equals(JwtErrorMessage.EXPIRED.getMessage())) {
            setResponse(response, JwtErrorMessage.EXPIRED);
        } else if (exceptionMessage.equals(JwtErrorMessage.INVALID_SIGNATURE.getMessage())) {
            setResponse(response, JwtErrorMessage.INVALID_SIGNATURE);
        } else {
            setResponse(response, JwtErrorMessage.UNSUPPORTED);
        }

    }

    private void setResponse(HttpServletResponse response, JwtErrorMessage jwtErrorMessage) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().print(jwtErrorMessage.getMessage());
    }
}

=> commence() 를 오버라이드 해 구현하면 된다. 나 같은 경우는 JWT에 관한 예외 케이스가 많기 때문에 if문으로 분기 처리했고, 나머지 예외에 관해서는 JWT 예외를 모아둔 enum 클래스에 없는 객체인지를 검증하고 따로 response를 메시지와 함께 날려주는 형식으로 구현했다.

 

토큰 유효시간 만료 시 예외 메시지 응답
Repository 메서드의 예외 메시지 응답

 

인가 관련 예외 처리 AccessDeniedHandler

인증 관련 작업에서 성공적으로 예외에 대한 처리 응답을 마쳤다.하지만, 인증이 완료되어도 인가에서 예외가 발생하며 그 즉시, 권한이 없는 응답과 함께 접근을 제한해야 한다. 이 경우는 AccessDeniedHandler 클래스를 구현하여 해결할 수 있다.

 

https://sweeeetgoguma.tistory.com/entry/Spring-Security-1-JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-REST-API-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80

 

Spring Security [1] - JWT를 이용한 REST API 인증과 인가

기존 인증은 JWT를 이용한 필터로, 인가는 인터셉터로 애노테이션을 정의해 손수 구현했었다. 이번에 리팩터링을 하면서, 스프링이 제공하는 보안 관련 프레임워크인 서큐리티를 사용해보자는

sweeeetgoguma.tistory.com

SecurityConfig에서 설정 했듯이, 각 url path마다 role을 설정할 수 있다. 여기서 반하는 권한의 요청이 들어왔을 때, 해결 가능하다.

 

ublic class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        setResponse(response);
    }

    private void setResponse(HttpServletResponse response) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().print(SecurityMessage.PERMISSION_DENIED.getMessage());
    }
}

=> 비교적 간단하게 권한이 없다는 응답 메시지를 내려주는 방식으로 구현했다.

 

이렇게 서큐리티를 이용해 인증, 인가를 구현했다. 좀 오래 걸렸고, 제대로 알고 하려다 보니 시간도 오래 걸렸다. 하지만 좋은 자양분이 되었음이 분명하다고 생각이 된다.이제 본격적으로 서비스 로직을 다루면서 리팩토링을 진행한다. 파이팅~

 

https://github.com/GroovyArea/My-ChickenBreast-Shop

 

GitHub - GroovyArea/My-ChickenBreast-Shop: shop api with spring boot

shop api with spring boot . Contribute to GroovyArea/My-ChickenBreast-Shop development by creating an account on GitHub.

github.com

 

반응형