Spring Boot Servlet Filter에서 에러 코드 변경하기

jonghyun.log·2022년 9월 9일
1

spring

목록 보기
3/7
post-thumbnail

문제 상황

Spring 으로 프로젝트를 진행하던 도중 Filter 를 이용해서 JWT 검증 로직을 만들었는데, 토큰이 만료되면 에러코드를 다르게 보내주려고 했었다.

허나.. 구글링해서 얻은 답변인 @ResponseStatus() 에너테이션을 이용해도 에러 코드가 바뀌지 않고 ResponseStatusException 객체를 이용해봐도 안되길래
스택오버플로우를 통해 알아낸 방법인 application.properties
server.error.include-message = always 설정 넣어주기를 해봐도 바뀌지 않았다....

기존 코드

//JwtAuthenticationFilter

@Component
@Getter
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	private final JwtTokenProvider jwtTokenProvider;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

		//cors 설정
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept, Authorization");
        response.setHeader("Content-Type", "*");

        if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK); //option 요청일때 필터검증 안함
        }else { 
        // 진짜 요청일때 필터 검증
            String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
            Claims claims = jwtTokenProvider.parseJwtToken(authorizationHeader);
            request.setAttribute("claims", claims); // jwt 정보 컨트롤러에서 사용할 수 있게 request에 담기
            filterChain.doFilter(request, response);
        }
    }
}
//JwtTokenProvider 클래스 내부

public Claims parseJwtToken(String authorizationHeader) {
        validationAuthorizationHeader(authorizationHeader);
        String token = extractToken(authorizationHeader);

        //토큰 검증
        Claims claims = (Claims) validateToken(token);
        return claims;
    }

    /**
     * 토큰 검증 메서드
     * @param token
     * @return claims
     */
    public Object validateToken(String token) throws ExpiredJwtException {
        try {
            return Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException exception) {
            log.info("토큰 만료");
            throw new ResponseStatusException(
                    HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다.", exception
            );
        } catch (JwtException | IllegalArgumentException exception) {
            log.info("jwtException : {}", exception);
            throw exception;
        }
        
    }

JwtTokenProviderJwt.parser() 에서 ExpiredJwtException 에러를 뱉어주면 ResponseStatusException 객체가 에러코드를 401 로 바꿔줄거라고 생각했다...

그러나 결과는...

그놈의 500 ....

문제 원인

디버깅을 통해 혹시...? 라는 마음으로 필터 말고 컨트롤러 에 위 방식들을 적용해보자 놀랍게도

!!!!!!

그렇다, 필터와 컨트롤러는 에러를 처리하는 방식이 달라서 생기는 문제였다.

정확히는 JwtTokenProvider 에서 에러를 throw 해줘도
그 에러가 바로 Response 로 오는것이 아닌 필터를 거쳐 에러가 다르게 변형되는 것이 문제였다.

해결 방법

그래서, 에러 처리를 JwtTokenProvider 에서 해주지 말고 Jwt.parser()throw 해주는 에러를 JwtAuthenticationFilter 에서 처리해주는 것으로 변경했다.


@Component
@Getter
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	private final JwtTokenProvider jwtTokenProvider;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    	
		//cors 설정
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept, Authorization");
        response.setHeader("Content-Type", "*");

        if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK); //option 요청일때 필터검증 안함
        }else { // 진짜 요청일때 필터 검증

            String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
            try {
                Claims claims = jwtTokenProvider.parseJwtToken(authorizationHeader);
                request.setAttribute("claims", claims); // jwt 정보 컨트롤러에서 사용할 수 있게 request에 담기

            } catch (ExpiredJwtException jwtException) {
                log.info("토큰 만료");

                ObjectMapper mapper = new ObjectMapper();

                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.setCharacterEncoding("UTF-8");

                ResponseStatusException responseStatusException = new ResponseStatusException(
                        HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다.");

                mapper.writeValue(response.getWriter(), responseStatusException);

            }catch (JwtException | IllegalArgumentException exception) {
                log.info("jwtException : {}", exception);
                throw exception;
            }

            filterChain.doFilter(request, response);
        }
    }
    
    

}

Claims 를 받아오는 과정에서 에러가 발생하므로 이 부분을 try-catch 문으로 감싸주고
doFilter 메서드로 현재 필터 다음 과정으로 넘어가므로
response 객체에 에러를 담아서 보내주었다.

//JwtTokenProvider 클래스 내부

public Claims parseJwtToken(String authorizationHeader) {
        validationAuthorizationHeader(authorizationHeader);
        String token = extractToken(authorizationHeader);

        //토큰 검증
        Claims claims = (Claims) validateToken(token);
        return claims;
    }

    /**
     * 토큰 검증 메서드
     * @param token
     * @return claims
     */
    public Object validateToken(String token) throws ExpiredJwtException {

        return Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();
    }

위 코드로 변경하여 에러를 보냈더니

아주 이쁘게 에러가 변경된 모습 😆

여담

이번 에러를 겪고 디버깅 하는 과정중 스프링 필터가 내가 생각했던 것 보다 훨씬 복잡한 구조로 이루어졌다는 것을 알았다.
앞으로는 로직짜고 된다고 넘어가지말고 코드도 다 까보면서 공부해봐야겠다...

참고한 링크

0개의 댓글