스프링시큐리티 JWT 예외처리

이상훈·2021년 9월 7일
7

spring

목록 보기
3/4
post-custom-banner

📕 ControllerAdvice란?

우리는 SpringSecurity를 이용해 Rest Api를 구현할 때 비즈니스 로직에서 발생하는 예외를 처리해 주기 위해 ControllerAdvice를 사용합니다.

데이터베이스에서 값을 조회하는 데에 실패했다던가, 유저의 아이디, 패스워드가 다르다던가 하는 정상적인 요청의 처리에 벗어나는 상황(예외)에 대해 유연하게 처리하기 위해 사용합니다.

예)


@ControllerAdvice
public class CustomControllerAdvice {

    //javax.persistence.EntityNotFoundException
    @ExceptionHandler(value = {EntityNotFoundException.class})
    public ResponseEntity<ExceptionPayload> handleEntityNotFoundException(EntityNotFoundException e) {
        ResultEmptyException resultEmptyException = new ResultEmptyException();
        final ExceptionPayload payload = ExceptionPayload
                .create()
                .status(HttpStatus.NOT_FOUND.value())
                .code(resultEmptyException.getExceptionCode().getCode())
                .message(e.getMessage());
        return new ResponseEntity<>(payload, HttpStatus.NOT_FOUND);
    }

    // UserNotExistException(Custom Exception)
        @ExceptionHandler(value = {UserNotExistException.class})
    public ResponseEntity<ExceptionPayload> handleUserNotExistException(UserNotExistException e) {
        final ExceptionPayload payload = ExceptionPayload
                .create()
                .status(HttpStatus.NOT_FOUND.value())
                .code(e.getExceptionCode().getCode())
                .message(e.getMessage());
        return new ResponseEntity<>(payload, HttpStatus.NOT_FOUND);
    }
    
    //JWT Filter에서 발생시키는 경우 ControllerAdvice에서 처리를 하지 못한다.
    //PermissionDeniedException(Custom Exception)
    @ExceptionHandler(value = {PermissionDeniedException.class})
    public ResponseEntity<ExceptionPayload> handlePermissionDeniedException(PermissionDeniedException e) {
        final ExceptionPayload payload = ExceptionPayload
                .create()
                .status(HttpStatus.FORBIDDEN.value())
                .code(e.getExceptionCode().getCode())
                .message(e.getMessage());
        return new ResponseEntity<>(payload, HttpStatus.FORBIDDEN);
    }

}

📕 ControllerAdvice 문제점? 단점?

우리가 JWT를 사용하는 가장 큰 장점은, DB에 접근하지 않고 인메모상의 키값을 이용해 사용자의 권한을 체크하기 위함입니다. 때문에 tokenProvider를 만들어 토큰 검증을 하고, 이를 "Filter"에 등록합니다.

하지만 ControllerAdvice는 Filter, Interceptor 단에서 발생하는 Exception은 처리를 해주지 못합니다.


📕 Filter 단에서 예외처리 하는 방법 첫번째 AuthenticationEntryPoint

//CustomAuthenticationEntryPoint.java
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

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

        if(exception == null) {
            setResponse(response, ExceptionCode.UNKNOWN_ERROR);
        }
        //잘못된 타입의 토큰인 경우
        else if(exception.equals(ExceptionCode.WRONG_TYPE_TOKEN.getCode())) {
            setResponse(response, ExceptionCode.WRONG_TYPE_TOKEN);
        }
        //토큰 만료된 경우
        else if(exception.equals(ExceptionCode.EXPIRED_TOKEN.getCode())) {
            setResponse(response, ExceptionCode.EXPIRED_TOKEN);
        }
        //지원되지 않는 토큰인 경우
        else if(exception.equals(ExceptionCode.UNSUPPORTED_TOKEN.getCode())) {
            setResponse(response, ExceptionCode.UNSUPPORTED_TOKEN);
        }
        else {
            setResponse(response, ExceptionCode.ACCESS_DENIED);
        }
    }
    //한글 출력을 위해 getWriter() 사용
    private void setResponse(HttpServletResponse response, ExceptionCode exceptionCode) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        JSONObject responseJson = new JSONObject();
        responseJson.put("message", exceptionCode.getMessage());
        responseJson.put("code", exceptionCode.getCode());

        response.getWriter().print(responseJson);
    }
}
//JwtFilter
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
    public static final String AUTHORIZATION_HEADER = "token";
    public static final String BEARER_PREFIX = "Bearer ";

    private final TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = getToken(request);
        try {
            if (StringUtils.hasText(token) && tokenProvider.checkToken(token)) {
                Authentication authentication = tokenProvider.getAuthentication(token);
                System.out.println(authentication);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (SecurityException | MalformedJwtException e) {
            request.setAttribute("exception", ExceptionCode.WRONG_TYPE_TOKEN.getCode());
        } catch (ExpiredJwtException e) {
            request.setAttribute("exception", ExceptionCode.EXPIRED_TOKEN.getCode());
        } catch (UnsupportedJwtException e) {
            request.setAttribute("exception", ExceptionCode.UNSUPPORTED_TOKEN.getCode());
        } catch (IllegalArgumentException e) {
            request.setAttribute("exception", ExceptionCode.WRONG_TOKEN.getCode());
        } catch (Exception e) {
            log.error("================================================");
            log.error("JwtFilter - doFilterInternal() 오류발생");
            log.error("token : {}", token);
            log.error("Exception Message : {}", e.getMessage());
            log.error("Exception StackTrace : {");
            e.printStackTrace();
            log.error("}");
            log.error("================================================");
            request.setAttribute("exception", ExceptionCode.UNKNOWN_ERROR.getCode());
        }

        filterChain.doFilter(request, response);


    }

    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) {
            return token.substring(7);
        }
        return null;
    }
}

위의 코드를 보면 tokenProvider.checkToken에서 발생하는 Excepction에 대하여 request 의 속성에 "exception" 값으로 넣어준다.
잘못된 토큰, 토큰 만료, 지원되지않는 토큰 등에 대한 처리를 해줍니다.

request에 "exception" 속성의 값을 넣어준 뒤 실제 AuthenticationEntryPoint에서 response에 결과를 담아 요청자에게 돌려줍니다.

//SecurityConfiguration.java
@Configuration
@EnableWebSecurity
@EnableJpaAuditing
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{

        httpSecurity
        		...
                .antMatchers("/user").hasAnyRole("USER", "ADMIN")
                .antMatchers("/admin").hasAnyRole("ADMIN")
                
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                	...
    }

}

위와같이 SpringSecurity 설정에 등록해주면 적용이 됩니다.


📕 Filter 단에서 예외처리 하는 방법 두번째 AccessDeniedHandler

위 상황에서 USER 권한을 가진 사람이 ADMIN권한을 가진 url에 대하여 요청을 보내면 어떻게 될까요?
우리가 원하는건 "권한없음" 이라는 메시지를 전달해 주고 해당 요청을 차단하는 것 입니다.

위에 적은 첫번째 방법은 토큰에 대한 유효성 검사 이지만, 접근 권한에 대한 처리는 해주지 않았습니다.
어떻게 처리를 해줘야 할까요?

바로 AccessDeniedHandler를 상속해 custom 해주면 됩니다.

//CustomAccessDeniedHandler.java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        ExceptionCode exceptionCode;
        exceptionCode = ExceptionCode.PERMISSION_DENIED;
        setResponse(response, exceptionCode);

    }

    private void setResponse(HttpServletResponse response, ExceptionCode exceptionCode) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        JSONObject responseJson = new JSONObject();
        responseJson.put("message", exceptionCode.getMessage());
        responseJson.put("code", exceptionCode.getCode());

        response.getWriter().print(responseJson);
    }
}
//SecurityConfiguration.java
@Configuration
@EnableWebSecurity
@EnableJpaAuditing
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{

        httpSecurity
        		...
                .antMatchers("/user").hasAnyRole("USER", "ADMIN")
                .antMatchers("/admin").hasAnyRole("ADMIN")
                
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeniedHandler())//추가된 코드
                	...
    }

}

accessDeniedHandler를 통해 CustomAccessDeniedHandler를 등록해 주면,
접근 권한에 따른 Exception 발생시 CustomAccessDeniedHandler를 통해 처리해 줄 수 있습니다.

profile
Hello!
post-custom-banner

0개의 댓글