From f2263b378e6c57ac2e830e89a6bcbdeebacdac62 Mon Sep 17 00:00:00 2001 From: Dongseok Kang Date: Sun, 7 Jan 2024 23:13:25 +0900 Subject: [PATCH] =?UTF-8?q?Refactor:=20response=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refacor: jwt error response 세분화 토큰 만료, 유효하지 않은 토큰, 지원하지 않는 토큰 추가 * Refactor: global exceptioin 추가 exception 추가 및 주석 추가 --- .../domain/api/auth/AuthController.java | 4 -- .../domain/api/post/PostController.java | 14 ------ .../domain/service/OAuthLoginServiceImp.java | 2 +- .../common/exception/ErrorResponse.java | 3 -- .../exception/GlobalExceptionHandler.java | 48 +++++++++++++++++++ .../global/common/exception/ResponseCode.java | 6 ++- .../config/security/SecurityConfig.java | 26 +++++++--- .../jwt/JwtAuthenticationCheckFilter.java | 3 +- .../jwt/JwtAuthenticationEntryPoint.java | 9 +++- .../config/security/jwt/JwtTokenProvider.java | 15 ++++-- 10 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/fubao/project/domain/api/auth/AuthController.java b/src/main/java/com/fubao/project/domain/api/auth/AuthController.java index 8f0e167..d80a06a 100644 --- a/src/main/java/com/fubao/project/domain/api/auth/AuthController.java +++ b/src/main/java/com/fubao/project/domain/api/auth/AuthController.java @@ -52,15 +52,11 @@ public ResponseEntity> logout(@Validated @Reque @DeleteMapping("/deactivation") public ResponseEntity> deactivation() { Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication(); - if (loggedInUser instanceof AnonymousAuthenticationToken) { - throw new CustomException(ResponseCode.UNAUTHORIZED); - } UUID memberId = UUID.fromString(loggedInUser.getName()); oAuthLoginService.deactivation(memberId); return ResponseEntity.ok(DataResponse.of(CustomResponseCode.MEMBER_DEACTIVATION)); } - @GetMapping("/kakao/code") public ResponseEntity code(@RequestParam String code) { return ResponseEntity.ok(code); diff --git a/src/main/java/com/fubao/project/domain/api/post/PostController.java b/src/main/java/com/fubao/project/domain/api/post/PostController.java index 33e370d..c8d2167 100644 --- a/src/main/java/com/fubao/project/domain/api/post/PostController.java +++ b/src/main/java/com/fubao/project/domain/api/post/PostController.java @@ -18,13 +18,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; import java.util.List; import java.util.UUID; @@ -43,9 +41,6 @@ public ResponseEntity> postWrite(@RequestPart(va @RequestPart(value = "data") @Validated PostWriteRequest postWriteRequest ) { Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication(); - if (loggedInUser instanceof AnonymousAuthenticationToken) { - throw new CustomException(ResponseCode.UNAUTHORIZED); - } UUID memberId = UUID.fromString(loggedInUser.getName()); return ResponseEntity.ok(DataResponse.of(postService.post(images, postWriteRequest, memberId))); } @@ -56,9 +51,6 @@ public ResponseEntity> postPatch(@RequestPart(va @RequestPart(value = "data", required = false) @Validated PostWriteRequest postWriteRequest, @PathVariable Long postId) { Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication(); - if (loggedInUser instanceof AnonymousAuthenticationToken) { - throw new CustomException(ResponseCode.UNAUTHORIZED); - } UUID memberId = UUID.fromString(loggedInUser.getName()); if ((image == null || image.isEmpty()) && postWriteRequest == null) throw new CustomException(ResponseCode.PATCH_POST_CONTENT_NOT_EXIST); @@ -81,9 +73,6 @@ public ResponseEntity>> postMailboxGet @GetMapping(value = "/my") public ResponseEntity>> myPostGet() { Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication(); - if (loggedInUser instanceof AnonymousAuthenticationToken) { - throw new CustomException(ResponseCode.UNAUTHORIZED); - } UUID memberId = UUID.fromString(loggedInUser.getName()); return ResponseEntity.ok(DataResponse.of(postService.myPostGet(memberId))); } @@ -100,9 +89,6 @@ public ResponseEntity getImage(@PathVariable Long postId) { @DeleteMapping(value = "/{postId}") public ResponseEntity> deletePost(@PathVariable Long postId) { Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication(); - if (loggedInUser instanceof AnonymousAuthenticationToken) { - throw new CustomException(ResponseCode.UNAUTHORIZED); - } UUID memberId = UUID.fromString(loggedInUser.getName()); postService.deletePost(postId,memberId); return ResponseEntity.ok(DataResponse.of(CustomResponseCode.POST_DELETE)); diff --git a/src/main/java/com/fubao/project/domain/service/OAuthLoginServiceImp.java b/src/main/java/com/fubao/project/domain/service/OAuthLoginServiceImp.java index 2b08fa3..782bbb0 100644 --- a/src/main/java/com/fubao/project/domain/service/OAuthLoginServiceImp.java +++ b/src/main/java/com/fubao/project/domain/service/OAuthLoginServiceImp.java @@ -46,7 +46,7 @@ public AuthTokens login(OAuthLoginParams params) { @Override public AuthTokens tokenRegenerate(TokenRegenerateRequest tokenRegenerateRequest) { - jwtTokenProvider.validateToken(tokenRegenerateRequest.getRefreshToken()); + jwtTokenProvider.validateToken(tokenRegenerateRequest.getRefreshToken(),null); if (!redisUtil.hasKey(tokenRegenerateRequest.getRefreshToken())) { throw new CustomException(ResponseCode.INVALID_TOKEN); } diff --git a/src/main/java/com/fubao/project/global/common/exception/ErrorResponse.java b/src/main/java/com/fubao/project/global/common/exception/ErrorResponse.java index 24f7cd2..db9c86c 100644 --- a/src/main/java/com/fubao/project/global/common/exception/ErrorResponse.java +++ b/src/main/java/com/fubao/project/global/common/exception/ErrorResponse.java @@ -1,9 +1,6 @@ package com.fubao.project.global.common.exception; import com.fubao.project.global.common.response.ResponseDto; -import lombok.Getter; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.MethodArgumentNotValidException; public class ErrorResponse extends ResponseDto { private ErrorResponse(ResponseCode errorCode) { diff --git a/src/main/java/com/fubao/project/global/common/exception/GlobalExceptionHandler.java b/src/main/java/com/fubao/project/global/common/exception/GlobalExceptionHandler.java index 9d6b59a..5434c1f 100644 --- a/src/main/java/com/fubao/project/global/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/fubao/project/global/common/exception/GlobalExceptionHandler.java @@ -5,10 +5,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @RestControllerAdvice @@ -16,6 +20,8 @@ @Slf4j public class GlobalExceptionHandler { private final SlackWebhookUtil slackWebhookUtil; + + // 직접 정의한 에러 @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(final CustomException e, HttpServletRequest request) { log.error("handleCustomException: {}", e.getResponseCode().toString()); @@ -26,6 +32,7 @@ protected ResponseEntity handleCustomException(final CustomExcept .body(errorResponse); } + // 지원하지 않는 HttpRequestMethod @ExceptionHandler(HttpRequestMethodNotSupportedException.class) protected ResponseEntity handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e, HttpServletRequest request) { log.error("handleHttpRequestMethodNotSupportedException: {}", e.getMessage()); @@ -46,6 +53,7 @@ protected ResponseEntity handleException(final Exception e, HttpS .body(errorResponse); } + //validation exception 처리 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity processValidationError(MethodArgumentNotValidException e, HttpServletRequest request) { final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.INTERNAL_SERVER_ERROR, e); @@ -54,4 +62,44 @@ public ResponseEntity processValidationError(MethodArgumentNotVal .status(e.getStatusCode()) .body(errorResponse); } + + //잘못된 자료형으로 인한 에러 + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity methodArgumentTypeMismatchExceptionError(MethodArgumentTypeMismatchException e, HttpServletRequest request) { + final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.BAD_REQUEST, e); + slackWebhookUtil.slackNotificationThread(e,request); + return ResponseEntity + .status(ResponseCode.BAD_REQUEST.getStatus()) + .body(errorResponse); + } + + //지원하지 않는 media type 에러 + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResponseEntity httpMediaTypeNotSupportedExceptionError(HttpMediaTypeNotSupportedException e, HttpServletRequest request) { + final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.BAD_REQUEST, e); + slackWebhookUtil.slackNotificationThread(e,request); + return ResponseEntity + .status(e.getStatusCode()) + .body(errorResponse); + } + + //외부 api client 에러 + @ExceptionHandler(HttpClientErrorException.class) + public ResponseEntity httpMediaTypeNotSupportedExceptionError(HttpClientErrorException e, HttpServletRequest request) { + final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.INTERNAL_SERVER_ERROR, e); + slackWebhookUtil.slackNotificationThread(e,request); + return ResponseEntity + .status(e.getStatusCode()) + .body(errorResponse); + } + + //외부 api server 에러 + @ExceptionHandler(HttpServerErrorException.class) + public ResponseEntity httpServerErrorExceptionError(HttpServerErrorException e, HttpServletRequest request) { + final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.INTERNAL_SERVER_ERROR, e); + slackWebhookUtil.slackNotificationThread(e,request); + return ResponseEntity + .status(e.getStatusCode()) + .body(errorResponse); + } } \ No newline at end of file diff --git a/src/main/java/com/fubao/project/global/common/exception/ResponseCode.java b/src/main/java/com/fubao/project/global/common/exception/ResponseCode.java index d7cc28d..862f549 100644 --- a/src/main/java/com/fubao/project/global/common/exception/ResponseCode.java +++ b/src/main/java/com/fubao/project/global/common/exception/ResponseCode.java @@ -29,7 +29,11 @@ public enum ResponseCode { //auth UNAUTHORIZED("AUTH-ERR-001", HttpStatus.UNAUTHORIZED, "접근 권한이 없는 유저입니다."), INVALID_TOKEN("AUTH-ERR-002", HttpStatus.UNAUTHORIZED, "유효한 토큰이 아닙니다."), - TEST("TEST-ERR-001", HttpStatus.BAD_REQUEST, "테스트입니다"); + EXPIRED_TOKEN("AUTH-ERR-003", HttpStatus.UNAUTHORIZED, "만료된 토근입니다."), + UNSUPPORTED_TOKEN("AUTH-ERR-004",HttpStatus.UNAUTHORIZED , "지원하지 않는 토큰입니다."), + //test + TEST("TEST-ERR-001", HttpStatus.BAD_REQUEST, "테스트입니다") + ; private final String code; private final HttpStatus status; private final String message; diff --git a/src/main/java/com/fubao/project/global/config/security/SecurityConfig.java b/src/main/java/com/fubao/project/global/config/security/SecurityConfig.java index efd55ae..ff9747a 100644 --- a/src/main/java/com/fubao/project/global/config/security/SecurityConfig.java +++ b/src/main/java/com/fubao/project/global/config/security/SecurityConfig.java @@ -30,11 +30,23 @@ @Configuration @EnableWebSecurity public class SecurityConfig { - @Value("${security.permitted-urls}") - private final List PERMITTED_URLS; - + // @Value("${security.permitted-urls}") +// private final List PERMITTED_URLS; + private final String[] GET_AUTHENTICATED_URLS = { + "/api/posts/my" + }; + private final String[] GET_PERMITTED_URLS = { + "/api/swagger-ui/**", "/api/swagger-resources/**", + "/api/v3/api-docs/**", "/api/auth/kakao", "/api/auth/refresh", "/api/auth/logout", "/api/auth/logout", + "/api/test/success", "/api/test/fail", + "/api/posts","/api/posts/*", "/api/posts/*/download", "/api/posts/fubao/love" + }; + private final String[] POST_PERMITTED_URLS = { + "/api/posts/fubao/love" + }; @Value("${security.cors-urls}") private final List CORS_URLS; + @Bean public SecurityFilterChain filterChain( HttpSecurity http, JwtAuthenticationCheckFilter jwtAuthenticationCheckFilter, @@ -51,9 +63,11 @@ public SecurityFilterChain filterChain( .authenticationEntryPoint(jwtAuthenticationEntryPoint) //인증 중 예외 발생 시 jwtAuthenticationEntryPoint 호출 ) .authorizeHttpRequests(request -> request - .requestMatchers(PERMITTED_URLS.toArray(new String[0])).permitAll() // 해당 url의 경우 검사하지 않음 - .requestMatchers(HttpMethod.GET,"/").permitAll() - .anyRequest().authenticated() // 모든 request는 검증 + .requestMatchers(HttpMethod.GET, GET_AUTHENTICATED_URLS).authenticated() + .requestMatchers(HttpMethod.GET, GET_PERMITTED_URLS).permitAll() + .requestMatchers(HttpMethod.POST, POST_PERMITTED_URLS).permitAll() + .requestMatchers(HttpMethod.GET, "/").permitAll() + .anyRequest().authenticated() ); return http.build(); } diff --git a/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationCheckFilter.java b/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationCheckFilter.java index d278f91..068ed17 100644 --- a/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationCheckFilter.java +++ b/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationCheckFilter.java @@ -24,11 +24,10 @@ public class JwtAuthenticationCheckFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = resolveToken(request); - if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) { + if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token,request)) { Authentication authentication = jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } - log.info("{}",SecurityContextHolder.getContext().getAuthentication()); chain.doFilter(request, response); } diff --git a/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationEntryPoint.java index 216c0da..1c59339 100644 --- a/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/fubao/project/global/config/security/jwt/JwtAuthenticationEntryPoint.java @@ -1,8 +1,8 @@ package com.fubao.project.global.config.security.jwt; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fubao.project.global.common.exception.ResponseCode; import com.fubao.project.global.common.exception.ErrorResponse; +import com.fubao.project.global.common.exception.ResponseCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -26,9 +26,14 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + ResponseCode responseCode = (ResponseCode) request.getAttribute("exception"); + setResponse(response, responseCode); + } + + private void setResponse(HttpServletResponse response, ResponseCode responseCode) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - objectMapper.writeValue(response.getWriter(), ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(ResponseCode.UNAUTHORIZED))); + objectMapper.writeValue(response.getWriter(), ResponseEntity.status(responseCode.getStatus()).body(ErrorResponse.of(responseCode))); } } \ No newline at end of file diff --git a/src/main/java/com/fubao/project/global/config/security/jwt/JwtTokenProvider.java b/src/main/java/com/fubao/project/global/config/security/jwt/JwtTokenProvider.java index 569e5d4..ccb217d 100644 --- a/src/main/java/com/fubao/project/global/config/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/fubao/project/global/config/security/jwt/JwtTokenProvider.java @@ -1,11 +1,12 @@ package com.fubao.project.global.config.security.jwt; import com.fubao.project.domain.api.auth.dto.response.AuthTokens; -import com.fubao.project.domain.entity.Member; +import com.fubao.project.global.common.exception.ResponseCode; import com.fubao.project.global.util.RedisUtil; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -18,7 +19,6 @@ import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; -import java.util.UUID; import static io.jsonwebtoken.Jwts.builder; @@ -57,7 +57,7 @@ public JwtTokenProvider( public AuthTokens createAccessToken(String id) { AuthTokens authTokens = AuthTokens.of(createToken(id), createRefreshToken(id)); - redisUtil.setStringData(authTokens.getRefreshToken(),authTokens.getAccessToken(), Duration.ofDays(refreshTokenValidityInDay)); + redisUtil.setStringData(authTokens.getRefreshToken(), authTokens.getAccessToken(), Duration.ofDays(refreshTokenValidityInDay)); return authTokens; } @@ -84,22 +84,26 @@ private String createRefreshToken(String username) { } // 토큰 유효성 및 만료기간 검사 - public boolean validateToken(String token) { + public boolean validateToken(String token, HttpServletRequest request) { try { jwtParser.parseClaimsJws(token); return true; } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { log.info("Invalid JWT signature."); log.trace("Invalid JWT signature trace: ", e); + request.setAttribute("exception", ResponseCode.INVALID_TOKEN); } catch (ExpiredJwtException e) { log.info("Expired JWT token."); log.trace("Expired JWT token trace: ", e); + request.setAttribute("exception", ResponseCode.EXPIRED_TOKEN); } catch (UnsupportedJwtException e) { log.info("Unsupported JWT token."); log.trace("Unsupported JWT token trace: ", e); + request.setAttribute("exception", ResponseCode.UNSUPPORTED_TOKEN); } catch (IllegalArgumentException e) { log.info("JWT token compact of handler are invalid."); log.trace("JWT token compact of handler are invalid trace: ", e); + request.setAttribute("exception", ResponseCode.INVALID_TOKEN); } return false; } @@ -110,7 +114,8 @@ public Authentication getAuthentication(String accessToken) { UserDetails userDetails = userDetailsService.loadUserByUsername(usernameFromToken); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } - public String getUsernameFromRefreshToken(String refreshToken){ + + public String getUsernameFromRefreshToken(String refreshToken) { return jwtParser.parseClaimsJws(refreshToken).getBody().getSubject(); } } \ No newline at end of file