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;
}
}
JwtTokenProvider 의 Jwt.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();
}
위 코드로 변경하여 에러를 보냈더니
아주 이쁘게 에러가 변경된 모습 😆
이번 에러를 겪고 디버깅 하는 과정중 스프링 필터가 내가 생각했던 것 보다 훨씬 복잡한 구조로 이루어졌다는 것을 알았다.
앞으로는 로직짜고 된다고 넘어가지말고 코드도 다 까보면서 공부해봐야겠다...