diff --git a/src/main/java/org/kakaoshare/backend/common/config/SecurityConfig.java b/src/main/java/org/kakaoshare/backend/common/config/SecurityConfig.java index 522abe1d9..45103daa4 100644 --- a/src/main/java/org/kakaoshare/backend/common/config/SecurityConfig.java +++ b/src/main/java/org/kakaoshare/backend/common/config/SecurityConfig.java @@ -1,6 +1,8 @@ package org.kakaoshare.backend.common.config; import lombok.RequiredArgsConstructor; +import org.kakaoshare.backend.common.error.handler.AuthenticationAccessDeniedHandler; +import org.kakaoshare.backend.common.error.handler.CustomAuthenticationEntryPoint; import org.kakaoshare.backend.common.filter.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,6 +29,9 @@ public class SecurityConfig { public static final String API_V_1 = "/api/v1/"; private static final List ALLOWED_HEADERS = Arrays.asList("Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"); private static final List ALLOWED_METHODS = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"); + + private final AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean @@ -53,6 +58,11 @@ public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .cors(httpSecurityCorsConfigurer -> corsConfigurationSource()) + .exceptionHandling( + httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer + .authenticationEntryPoint(customAuthenticationEntryPoint) + .accessDeniedHandler(authenticationAccessDeniedHandler) + ) .build(); } diff --git a/src/main/java/org/kakaoshare/backend/common/error/handler/AuthenticationAccessDeniedHandler.java b/src/main/java/org/kakaoshare/backend/common/error/handler/AuthenticationAccessDeniedHandler.java new file mode 100644 index 000000000..7c74d86ed --- /dev/null +++ b/src/main/java/org/kakaoshare/backend/common/error/handler/AuthenticationAccessDeniedHandler.java @@ -0,0 +1,35 @@ +package org.kakaoshare.backend.common.error.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.codec.CharEncoding; +import org.kakaoshare.backend.common.error.GlobalErrorCode; +import org.kakaoshare.backend.common.error.response.ErrorResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +@Component +public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void handle(final HttpServletRequest request, + final HttpServletResponse response, + final AccessDeniedException accessDeniedException) throws IOException { + final String accept = request.getHeader(HttpHeaders.ACCEPT); + if (MediaType.APPLICATION_JSON_VALUE.equals(accept)) { + final GlobalErrorCode errorCode = GlobalErrorCode.RESOURCE_NOT_FOUND; + response.setStatus(errorCode.getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(CharEncoding.UTF_8); + + final ErrorResponse errorResponse = ErrorResponse.from(errorCode); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/kakaoshare/backend/common/error/handler/CustomAuthenticationEntryPoint.java b/src/main/java/org/kakaoshare/backend/common/error/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 000000000..9222862ee --- /dev/null +++ b/src/main/java/org/kakaoshare/backend/common/error/handler/CustomAuthenticationEntryPoint.java @@ -0,0 +1,39 @@ +package org.kakaoshare.backend.common.error.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.CharEncoding; +import org.kakaoshare.backend.common.error.GlobalErrorCode; +import org.kakaoshare.backend.common.error.response.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + private static final String LOG_EXCEPTION_FORMAT = "Not Authenticated Request = {}"; + private static final String LOG_URI_FORMAT = "Request Uri = {}"; + private final ObjectMapper objectMapper=new ObjectMapper(); + + @Override + public void commence(final HttpServletRequest request, + final HttpServletResponse response, + final AuthenticationException authException) throws IOException { + log.error(LOG_EXCEPTION_FORMAT, authException.toString()); + log.error(LOG_URI_FORMAT, request.getRequestURI()); + final GlobalErrorCode errorCode = GlobalErrorCode.RESOURCE_NOT_FOUND; + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(CharEncoding.UTF_8); + + final ErrorResponse errorResponse = ErrorResponse.from(errorCode); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + } +} \ No newline at end of file diff --git a/src/main/java/org/kakaoshare/backend/common/error/handler/GlobalExceptionHandler.java b/src/main/java/org/kakaoshare/backend/common/error/handler/GlobalExceptionHandler.java index 41b1e2970..696138989 100644 --- a/src/main/java/org/kakaoshare/backend/common/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/org/kakaoshare/backend/common/error/handler/GlobalExceptionHandler.java @@ -15,6 +15,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.reactive.function.client.WebClientResponseException; +import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import static org.kakaoshare.backend.common.error.GlobalErrorCode.INTERNAL_SERVER_ERROR; @@ -41,6 +42,17 @@ protected ResponseEntity handleMissingServletRequestParameter(final Miss logException(e, errorCode); return handleExceptionInternal(errorCode); } + + + @Override + protected ResponseEntity handleNoHandlerFoundException(final NoHandlerFoundException e, + final HttpHeaders headers, + final HttpStatusCode status, + final WebRequest request) { + final GlobalErrorCode errorCode = GlobalErrorCode.RESOURCE_NOT_FOUND; + logException(e, errorCode); + return handleExceptionInternal(errorCode); + } @ExceptionHandler(BusinessException.class) protected ResponseEntity handleBusinessException(BusinessException e) { diff --git a/src/main/java/org/kakaoshare/backend/common/error/response/ErrorResponse.java b/src/main/java/org/kakaoshare/backend/common/error/response/ErrorResponse.java index 1ad8e8593..08c1dc8f8 100644 --- a/src/main/java/org/kakaoshare/backend/common/error/response/ErrorResponse.java +++ b/src/main/java/org/kakaoshare/backend/common/error/response/ErrorResponse.java @@ -1,21 +1,14 @@ package org.kakaoshare.backend.common.error.response; import lombok.Builder; +import org.kakaoshare.backend.common.error.ErrorCode; @Builder -public record ErrorResponse(String message, - int code -// @JsonInclude(JsonInclude.Include.NON_EMPTY) -// List errors -) { -// @Builder -// public record ValidationError(String field, String message) { -// -// public static ValidationError of(final FieldError fieldError) { -// return ValidationError.builder() -// .field(fieldError.getField()) -// .message(fieldError.getDefaultMessage()) -// .build(); -// } -// } +public record ErrorResponse(String message, int code) { + public static ErrorResponse from(final ErrorCode errorCode) { + return ErrorResponse.builder() + .code(errorCode.getHttpStatus().value()) + .message(errorCode.getMessage()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/org/kakaoshare/backend/common/filter/JwtAuthenticationFilter.java b/src/main/java/org/kakaoshare/backend/common/filter/JwtAuthenticationFilter.java index 624e6369f..3c4c50cd9 100644 --- a/src/main/java/org/kakaoshare/backend/common/filter/JwtAuthenticationFilter.java +++ b/src/main/java/org/kakaoshare/backend/common/filter/JwtAuthenticationFilter.java @@ -1,13 +1,19 @@ package org.kakaoshare.backend.common.filter; +import com.fasterxml.jackson.databind.ObjectMapper; import com.querydsl.core.util.StringUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.CharEncoding; +import org.kakaoshare.backend.common.error.response.ErrorResponse; +import org.kakaoshare.backend.jwt.exception.JwtException; import org.kakaoshare.backend.jwt.util.JwtProvider; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -18,6 +24,7 @@ import java.io.IOException; +@Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -26,14 +33,26 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; private final UserDetailsService userDetailsService; + private static void printLog(final JwtException e) { + log.error("\nException Class = {}\nResponse Code = {}\nMessage = {}", + e.getClass(), + e.getErrorCode().getHttpStatus().value(), + e.getMessage()); + } + @Override protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException { - final String accessToken = getAccessToken(request); - if (accessToken != null && jwtProvider.validateToken(accessToken)) { - SecurityContextHolder.getContext() - .setAuthentication(getAuthentication(accessToken)); + try { + final String accessToken = getAccessToken(request); + if (accessToken != null && jwtProvider.validateToken(accessToken)) { + SecurityContextHolder.getContext() + .setAuthentication(getAuthentication(accessToken)); + } + } catch (JwtException e) { + handleJwtException(response, e); + printLog(e); } filterChain.doFilter(request, response); @@ -51,6 +70,15 @@ private String getAccessToken(final HttpServletRequest request) { private Authentication getAuthentication(final String accessToken) { final String username = jwtProvider.getUsername(accessToken); final UserDetails userDetails = userDetailsService.loadUserByUsername(username); - return new UsernamePasswordAuthenticationToken(userDetails.getUsername(),null, userDetails.getAuthorities()); + return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities()); + } + + private void handleJwtException(final HttpServletResponse response, final JwtException e) throws IOException { + final ObjectMapper objectMapper = new ObjectMapper(); + response.setStatus(e.getErrorCode().getHttpStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(CharEncoding.UTF_8); + final ErrorResponse errorResponse = ErrorResponse.from(e.getErrorCode()); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); } } diff --git a/src/main/java/org/kakaoshare/backend/jwt/exception/JwtErrorCode.java b/src/main/java/org/kakaoshare/backend/jwt/exception/JwtErrorCode.java index 397d86642..f188ee3f5 100644 --- a/src/main/java/org/kakaoshare/backend/jwt/exception/JwtErrorCode.java +++ b/src/main/java/org/kakaoshare/backend/jwt/exception/JwtErrorCode.java @@ -1,18 +1,21 @@ package org.kakaoshare.backend.jwt.exception; -public enum JwtErrorCode { - INVALID("유효하지 않은 토큰입니다."), - EXPIRED("만료된 토큰입니다."), - UNSUPPORTED("JWT를 지원하지 않습니다."), - NOT_FOUND("토큰을 찾을 수 없습니다."); +import lombok.Getter; +import org.kakaoshare.backend.common.error.ErrorCode; +import org.springframework.http.HttpStatus; +@Getter +public enum JwtErrorCode implements ErrorCode { + INVALID(HttpStatus.BAD_REQUEST,"유효하지 않은 토큰입니다."), + EXPIRED(HttpStatus.BAD_REQUEST,"만료된 토큰입니다."), + UNSUPPORTED(HttpStatus.BAD_REQUEST,"JWT를 지원하지 않습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND,"토큰을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; private final String message; - JwtErrorCode(final String message) { + JwtErrorCode(final HttpStatus httpStatus, final String message) { + this.httpStatus = httpStatus; this.message = message; } - - public String getMessage() { - return message; - } } diff --git a/src/main/java/org/kakaoshare/backend/jwt/exception/JwtException.java b/src/main/java/org/kakaoshare/backend/jwt/exception/JwtException.java index 549716f3d..e74b965d2 100644 --- a/src/main/java/org/kakaoshare/backend/jwt/exception/JwtException.java +++ b/src/main/java/org/kakaoshare/backend/jwt/exception/JwtException.java @@ -1,10 +1,10 @@ package org.kakaoshare.backend.jwt.exception; -public class JwtException extends RuntimeException { - private final JwtErrorCode errorCode; +import org.kakaoshare.backend.common.error.ErrorCode; +import org.kakaoshare.backend.common.error.exception.BusinessException; - public JwtException(final JwtErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; +public class JwtException extends BusinessException { + public JwtException(final ErrorCode errorCode) { + super(errorCode); } } \ No newline at end of file