Skip to content

Commit

Permalink
Refactor: response 리팩토링 (#71)
Browse files Browse the repository at this point in the history
* Refacor: jwt error response 세분화
토큰 만료, 유효하지 않은 토큰, 지원하지 않는 토큰 추가

* Refactor: global exceptioin 추가
exception 추가 및 주석 추가
  • Loading branch information
wcorn authored Jan 7, 2024
1 parent 9714877 commit f2263b3
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,11 @@ public ResponseEntity<DataResponse<CustomResponseCode>> logout(@Validated @Reque
@DeleteMapping("/deactivation")
public ResponseEntity<DataResponse<CustomResponseCode>> 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<String> code(@RequestParam String code) {
return ResponseEntity.ok(code);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,9 +41,6 @@ public ResponseEntity<DataResponse<PostWriteResponse>> 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)));
}
Expand All @@ -56,9 +51,6 @@ public ResponseEntity<DataResponse<PostPatchResponse>> 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);
Expand All @@ -81,9 +73,6 @@ public ResponseEntity<DataResponse<Page<PostMailBoxGetResponse>>> postMailboxGet
@GetMapping(value = "/my")
public ResponseEntity<DataResponse<List<PostMyGetResponse>>> 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)));
}
Expand All @@ -100,9 +89,6 @@ public ResponseEntity<ByteArrayResource> getImage(@PathVariable Long postId) {
@DeleteMapping(value = "/{postId}")
public ResponseEntity<DataResponse<CustomResponseCode>> 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
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
@RequiredArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
private final SlackWebhookUtil slackWebhookUtil;

// 직접 정의한 에러
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(final CustomException e, HttpServletRequest request) {
log.error("handleCustomException: {}", e.getResponseCode().toString());
Expand All @@ -26,6 +32,7 @@ protected ResponseEntity<ErrorResponse> handleCustomException(final CustomExcept
.body(errorResponse);
}

// 지원하지 않는 HttpRequestMethod
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
log.error("handleHttpRequestMethodNotSupportedException: {}", e.getMessage());
Expand All @@ -46,6 +53,7 @@ protected ResponseEntity<ErrorResponse> handleException(final Exception e, HttpS
.body(errorResponse);
}

//validation exception 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> processValidationError(MethodArgumentNotValidException e, HttpServletRequest request) {
final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.INTERNAL_SERVER_ERROR, e);
Expand All @@ -54,4 +62,44 @@ public ResponseEntity<ErrorResponse> processValidationError(MethodArgumentNotVal
.status(e.getStatusCode())
.body(errorResponse);
}

//잘못된 자료형으로 인한 에러
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,23 @@
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${security.permitted-urls}")
private final List<String> PERMITTED_URLS;

// @Value("${security.permitted-urls}")
// private final List<String> 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<String> CORS_URLS;

@Bean
public SecurityFilterChain filterChain(
HttpSecurity http, JwtAuthenticationCheckFilter jwtAuthenticationCheckFilter,
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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();
}
}

0 comments on commit f2263b3

Please sign in to comment.