Spring Security JWT 예외처리

안세웅·2023년 4월 26일
1

Spring Boot

목록 보기
3/3

이전에 구현한 JWT 토큰 인증을 확인하던 도중 API요청 시 들어오는 토큰이 문제가 되어 에러 메세지를 보여주는 부분에서 맞지 않는 에러 메세지를 보여주고 있는 부분을 확인 하였습니다.

토큰 검증 메서드를 확인 해보니

public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
        log.info("Invalid JWT Token", e);
    } catch (ExpiredJwtException e) {
        log.info("Expired JWT Token", e);
    } catch (UnsupportedJwtException e) {
        log.info("Unsupported JWT Token", e);
    } catch (IllegalArgumentException e) {
        log.info("JWT claims string is empty.", e);
    }
    return false;
}

검증 시 로그로 각 오류 메세지를 보여주고 있긴 하나 따로 클라이언트로 보여주는 과정이 빠져 있었습니다.

그럼 예외 처리를 어떻게 해야하는지 고민하던 중 Filter단에서 예외처리 하는 방법으로 선택하였습니다.

구현

우선 enum을 사용하여 에러 메시지를 코드화시켜 관리하기 위해 ErrorCode 클래스를 생성하였습니다.

ErrorCode

@Getter
public enum ErrorCode {
    UNKNOWN_ERROR(1001, "토큰이 존재하지 않습니다."),
    WRONG_TYPE_TOKEN(1002, "변조된 토큰입니다."),
    EXPIRED_TOKEN(1003, "만료된 토큰입니다."),
    UNSUPPORTED_TOKEN(1004, "변조된 토큰입니다."),
    ACCESS_DENIED(1005, "권한이 없습니다.");


    ErrorCode (int status, String message) {
        this.code = status;
        this.message = message;
    }

    private int code;
    private String message;

}

JwtTokenProvider - validateToken

토큰 검증하는 부분은 Execption이 발생 할 수 있도록 try-catch 문을 제거 해줍니다.

public boolean validateToken(String token) {
    Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
    return true;
}

JwtAuthenticationFilter - doFilter

토큰 검증 부분에서 제외하였던 try-catch 부분을 추가 해주고 각 Exception마다 ErrorCode에서 만들었던 에러코드를 request Attribute에 저장 해줍니다.

에러코드를 request Attribute에 저장하는 이유는 추후 추가할 CustomAuthenticationEntryPoint에서 response에 결과를 담아 클라이언트로 전달히기 위함 입니다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

    // 1. Request Header 에서 JWT 토큰 추출
    String token = resolveToken((HttpServletRequest) request);

    try {
        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
    } catch (SecurityException | MalformedJwtException e) {
        request.setAttribute("exception", ErrorCode.WRONG_TYPE_TOKEN.getCode());
    } catch (ExpiredJwtException e) {
        request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN.getCode());
    } catch (UnsupportedJwtException e) {
        request.setAttribute("exception", ErrorCode.UNSUPPORTED_TOKEN.getCode());
    } catch (IllegalArgumentException e) {
        request.setAttribute("exception", ErrorCode.WRONG_TYPE_TOKEN.getCode());
    } catch (Exception e) {
        request.setAttribute("exception", ErrorCode.UNKNOWN_ERROR.getCode());
    }

    chain.doFilter(request, response);
}

CustomAuthenticationEntryPoint

Spring Security에서 인증되지 않은 사용자의 리소스에 대한 접근 처리는 AuthenticationEntryPoint가 담당합니다.

AuthenticationEntryPoint는 인터페이스이기 때문에 CustomAuthenticationEntryPoint라는 이름으로 상속받아서 나만의 메서드를 만들었습니다.

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        Integer exception = (Integer)request.getAttribute("exception");

        if(exception == null) {
            setResponse(response, ErrorCode.UNKNOWN_ERROR);
        }
        //잘못된 타입의 토큰인 경우
        else if(exception == 1004) {
            setResponse(response, ErrorCode.WRONG_TYPE_TOKEN);
        }
        //토큰 만료된 경우
        else if(exception == 1005) {
            setResponse(response, ErrorCode.EXPIRED_TOKEN);
        }
        //지원되지 않는 토큰인 경우
        else if(exception == 1006) {
            setResponse(response, ErrorCode.UNSUPPORTED_TOKEN);
        }
        else {
            setResponse(response, ErrorCode.ACCESS_DENIED);
        }
    }
    //한글 출력을 위해 getWriter() 사용
    private void setResponse(HttpServletResponse response, ErrorCode 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);
    }
}

Filter 부분에서 저장한 에러코드를 받아서 response에 에러코드와 에러메세지를 추가합니다.

SecurityConfig

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
    	...
        
        /*
        exceptionHandling(): 예외처리
        .authenticationEntryPoint(메소드): 인증 실패 시 처리할 메소드
        */
        .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
        
    	...

    }

security 설정에서는 인증 실패시 CustomAuthenticationEntryPoint를 실행할 수 있도록 설정 해줍니다.


테스트

postman으로 테스트 하여 확인 해보면 정상적으로 response를 확인 할 수 있습니다.

  • 토큰만료

  • 변조된 토큰



Reference

https://velog.io/@dltkdgns3435/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC

0개의 댓글