💡 개발 환경
Java 11, Spring 2.7.X, Gradle 7.5, MySQL
[구현하려는 기능]
1. 만료된 JWT token 으로 API 요청 시 401에러와 관련 에러 메세지 return
2. 비회원이 토큰이 필요한 API 요청 시 400에러와 관련 에러 메세지 return
3. @Vaild 예외 처리 시 response 형태 수정
[문제 상황]
1. 만료된 토큰으로 API 요청 시 postman에서는 500에러가 발생하는 반면, IntelliJ Run 창에는 던진 에러메세지가 출력됨.
[시도한 방법]
- 우선 현재 토큰을 검증하는 로직은 JwtAuthenticationFilter, TokenProvider 클래스와 같은 `filter` 단계에서 이루어지므로 `@RestControllerAdvice`가 있는 RestApiExceptionHandler 클래스에서는 처리가 불가능했습니다.
- filter단에서 예외 처리를 하기 위해 아래 처럼 수정하였습니다.
기존 코드
JwtAuthenticationFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = tokenProvider.resolveToken(request);
if (token != null) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
TokenProvider.java
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(decodeUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String decodeUsername(String token) {
DecodedJWT decodedJWT = isValidToken(token)
.orElseThrow(() -> new CustomException(ErrorCode.ILLEGAL\_INVALID\_TOKEN));
Date now = new Date();
if (decodedJWT.getExpiresAt().before(now)) {
throw new CustomException(ErrorCode.ILLEGAL\_INVALID\_TOKEN));
}
return decodedJWT
.getClaim(JwtTokenUtils.CLAIM\_USERID)
.asString();
}
// 토큰 유효성 검사
public Optional<DecodedJWT> isValidToken(String token) {
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT
.require(generateAlgorithm(JWT\_SECRET))
.build();
jwt = verifier.verify(token);
} catch (TokenExpiredException e) {
logger.error(e.getMessage());
}
return Optional.ofNullable(jwt);
}
수정한 코드
JwtAuthenticationFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = tokenProvider.resolveToken(request);
String method = request.getMethod();
log.info("requestMethod : {}", method);
String servletPath = request.getServletPath();
if (token != null) {
Authentication authentication = tokenProvider.getAuthentication(token, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 토큰이 없는 경우에 회원만 가능한 API 요청 시 에러 처리, 추후 거래게시판
else if (servletPath.equals("/api/v1/rehoming") && method.equals("POST") || servletPath.startsWith("/api/v1/likes") ||
servletPath.startsWith("/api/v1/bookmarks") || servletPath.startsWith("/api/v1/rehoming/status")) {
tokenProvider.tokenNullChk(response);
}
filterChain.doFilter(request, response);
}
TokenProvider.java
public Authentication getAuthentication(String token, HttpServletResponse response) throws IOException {
UserDetails userDetails = userDetailsService.loadUserByUsername(decodeUsername(token, response));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 회원 정보 추출
public String decodeUsername(String token, HttpServletResponse response) throws IOException {
DecodedJWT decodedJWT = isValidToken(token, response)
.orElseThrow(() -> new IllegalArgumentException("유효한 토큰이 아닙니다."));
Date now = new Date();
if (decodedJWT.getExpiresAt().before(now)) {
throw new IllegalArgumentException("만료된 토큰");
}
return decodedJWT
.getClaim(JwtTokenUtils.CLAIM\_USERID)
.asString();
}
// 토큰 유효성 검사
public Optional<DecodedJWT> isValidToken(String token, HttpServletResponse response) throws IOException {
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT
.require(generateAlgorithm(JWT\_SECRET))
.build();
jwt = verifier.verify(token);
} catch (TokenExpiredException e) {
logger.error(e.getMessage());
tokenExpired(response);
}
// 토큰이 null일 경우 error 처리
public void tokenNullChk(HttpServletResponse response) throws IOException {
log.info("TokenProvider : JWT Token이 존재하지 않습니다.");
response.setStatus(SC\_BAD\_REQUEST);
response.setContentType(APPLICATION\_JSON\_VALUE);
response.setCharacterEncoding("utf-8");
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD\_REQUEST, "JWT Token이 존재하지 않습니다.");
new ObjectMapper().writeValue(response.getWriter(), errorResponse);
}
// 만료된 토큰인 경우 error 처리
public void tokenExpired(HttpServletResponse response) throws IOException {
log.info("TokenProvider : JWT Token이 만료되었습니다.");
response.setStatus(SC\_UNAUTHORIZED);
response.setContentType(APPLICATION\_JSON\_VALUE);
response.setCharacterEncoding("utf-8");
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다.");
new ObjectMapper().writeValue(response.getWriter(), errorResponse);
}
[참고자료]
'Spring & SpringBoot' 카테고리의 다른 글
[Java/Springboot] OpenAPI를 활용해 데이터 수집하기_(1) (0) | 2024.08.31 |
---|---|
Java/SpringBoot 게시판 기능 구현_Querydsl을 활용한 카테고리별 조회, 필터링 (0) | 2023.10.19 |
SpringBoot 게시판_이미지 대표이미지 출력 (0) | 2023.05.12 |
[Spring] SpringBoot_게시글 수정하기 (JPA, MySQL) (0) | 2023.01.18 |
SpringBoot postman 404 error (0) | 2022.11.16 |