From 1c523994809b44ed6a4c9cebdcd3d5082a953ab1 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:12:26 +0900 Subject: [PATCH] Feat: setting --- build.gradle | 5 ++ .../itpick/backend/BackendApplication.java | 2 - .../JwtAuthHandlerArgumentResolver.java | 34 +++++++++ .../argument_resolver/PreAuthorize.java | 11 +++ .../common/exception/BadRequestException.java | 15 ++++ .../common/exception/DatabaseException.java | 15 ++++ .../InternalServerErrorException.java | 15 ++++ .../bad_request/JwtBadRequestException.java | 15 ++++ .../jwt/bad_request/JwtNoTokenException.java | 15 ++++ .../JwtUnsupportedTokenException.java | 15 ++++ .../JwtExpiredTokenException.java | 15 ++++ .../JwtInvalidTokenException.java | 15 ++++ .../JwtMalformedTokenException.java | 15 ++++ .../JwtUnauthorizedTokenException.java | 15 ++++ .../BaseExceptionControllerAdvice.java | 66 +++++++++++++++++ .../DatabaseExceptionControllerAdvice.java | 34 +++++++++ .../JwtExceptionControllerAdvice.java | 31 ++++++++ .../interceptor/JwtAuthInterceptor.java | 70 ++++++++++++++++++ .../common/response/BaseErrorResponse.java | 47 ++++++++++++ .../backend/common/response/BaseResponse.java | 43 +++++++++++ .../status/BaseExceptionResponseStatus.java | 73 +++++++++++++++++++ .../response/status/ResponseStatus.java | 11 +++ .../itpick/backend/config/SecurityConfig.java | 2 +- .../itpick/backend/config/WebConfig.java | 33 +++++++++ .../backend/controller/TestController.java | 14 ---- .../itpick/backend/dto/auth/LoginRequest.java | 19 +++++ .../backend/dto/auth/LoginResponse.java | 13 ++++ .../store/itpick/backend/jwt/JwtProvider.java | 69 ++++++++++++++++++ .../support/logging/LoggingAspect.java | 35 +++++++++ .../backend/util/BindingResultUtils.java | 14 ++++ 30 files changed, 759 insertions(+), 17 deletions(-) create mode 100644 src/main/java/store/itpick/backend/common/argument_resolver/JwtAuthHandlerArgumentResolver.java create mode 100644 src/main/java/store/itpick/backend/common/argument_resolver/PreAuthorize.java create mode 100644 src/main/java/store/itpick/backend/common/exception/BadRequestException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/DatabaseException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/InternalServerErrorException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtBadRequestException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtNoTokenException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtExpiredTokenException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtInvalidTokenException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtMalformedTokenException.java create mode 100644 src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java create mode 100644 src/main/java/store/itpick/backend/common/exception_handler/BaseExceptionControllerAdvice.java create mode 100644 src/main/java/store/itpick/backend/common/exception_handler/DatabaseExceptionControllerAdvice.java create mode 100644 src/main/java/store/itpick/backend/common/exception_handler/JwtExceptionControllerAdvice.java create mode 100644 src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java create mode 100644 src/main/java/store/itpick/backend/common/response/BaseErrorResponse.java create mode 100644 src/main/java/store/itpick/backend/common/response/BaseResponse.java create mode 100644 src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java create mode 100644 src/main/java/store/itpick/backend/common/response/status/ResponseStatus.java create mode 100644 src/main/java/store/itpick/backend/config/WebConfig.java delete mode 100644 src/main/java/store/itpick/backend/controller/TestController.java create mode 100644 src/main/java/store/itpick/backend/dto/auth/LoginRequest.java create mode 100644 src/main/java/store/itpick/backend/dto/auth/LoginResponse.java create mode 100644 src/main/java/store/itpick/backend/jwt/JwtProvider.java create mode 100644 src/main/java/store/itpick/backend/support/logging/LoggingAspect.java create mode 100644 src/main/java/store/itpick/backend/util/BindingResultUtils.java diff --git a/build.gradle b/build.gradle index b573797..83aa169 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', 'io.jsonwebtoken:jjwt-jackson:0.11.2' + } tasks.named('test') { diff --git a/src/main/java/store/itpick/backend/BackendApplication.java b/src/main/java/store/itpick/backend/BackendApplication.java index 9d368e5..0ce8b3a 100644 --- a/src/main/java/store/itpick/backend/BackendApplication.java +++ b/src/main/java/store/itpick/backend/BackendApplication.java @@ -5,8 +5,6 @@ @SpringBootApplication public class BackendApplication { - // cicd test 3 - public static void main(String[] args) { SpringApplication.run(BackendApplication.class, args); diff --git a/src/main/java/store/itpick/backend/common/argument_resolver/JwtAuthHandlerArgumentResolver.java b/src/main/java/store/itpick/backend/common/argument_resolver/JwtAuthHandlerArgumentResolver.java new file mode 100644 index 0000000..2d6fb44 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/argument_resolver/JwtAuthHandlerArgumentResolver.java @@ -0,0 +1,34 @@ +package store.itpick.backend.common.argument_resolver; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; + +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.Arrays; + +@Slf4j +@Component +public class JwtAuthHandlerArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasAnnotation = parameter.hasParameterAnnotation(PreAuthorize.class); + log.info(Arrays.toString(parameter.getParameterAnnotations())); + boolean hasType = long.class.isAssignableFrom(parameter.getParameterType()); + log.info("hasAnnotation={}, hasType={}, hasAnnotation && hasType={}", hasAnnotation, hasType, hasAnnotation&&hasType); + return hasAnnotation && hasType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + log.info("userId={}", request.getAttribute("userId")); + return request.getAttribute("userId"); + + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/argument_resolver/PreAuthorize.java b/src/main/java/store/itpick/backend/common/argument_resolver/PreAuthorize.java new file mode 100644 index 0000000..5346cea --- /dev/null +++ b/src/main/java/store/itpick/backend/common/argument_resolver/PreAuthorize.java @@ -0,0 +1,11 @@ +package store.itpick.backend.common.argument_resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface PreAuthorize { +} diff --git a/src/main/java/store/itpick/backend/common/exception/BadRequestException.java b/src/main/java/store/itpick/backend/common/exception/BadRequestException.java new file mode 100644 index 0000000..9e7c0cd --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/BadRequestException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class BadRequestException extends RuntimeException { + + private final ResponseStatus exceptionStatus; + + public BadRequestException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } +} diff --git a/src/main/java/store/itpick/backend/common/exception/DatabaseException.java b/src/main/java/store/itpick/backend/common/exception/DatabaseException.java new file mode 100644 index 0000000..815091c --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/DatabaseException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class DatabaseException extends RuntimeException{ + + private final ResponseStatus exceptionStatus; + + public DatabaseException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } +} diff --git a/src/main/java/store/itpick/backend/common/exception/InternalServerErrorException.java b/src/main/java/store/itpick/backend/common/exception/InternalServerErrorException.java new file mode 100644 index 0000000..01308ca --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/InternalServerErrorException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class InternalServerErrorException extends RuntimeException { + + private final ResponseStatus exceptionStatus; + + public InternalServerErrorException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtBadRequestException.java b/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtBadRequestException.java new file mode 100644 index 0000000..cb44b07 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtBadRequestException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.bad_request; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtBadRequestException extends RuntimeException { + + private final ResponseStatus exceptionStatus; + + public JwtBadRequestException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtNoTokenException.java b/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtNoTokenException.java new file mode 100644 index 0000000..2bf0cea --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtNoTokenException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.bad_request; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtNoTokenException extends JwtBadRequestException { + + private final ResponseStatus exceptionStatus; + + public JwtNoTokenException(ResponseStatus exceptionStatus) { + super(exceptionStatus); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java b/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java new file mode 100644 index 0000000..d5bbf62 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/bad_request/JwtUnsupportedTokenException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.bad_request; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtUnsupportedTokenException extends JwtBadRequestException { + + private final ResponseStatus exceptionStatus; + + public JwtUnsupportedTokenException(ResponseStatus exceptionStatus) { + super(exceptionStatus); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtExpiredTokenException.java b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtExpiredTokenException.java new file mode 100644 index 0000000..70384b1 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtExpiredTokenException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.unauthorized; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtExpiredTokenException extends JwtUnauthorizedTokenException { + + private final ResponseStatus exceptionStatus; + + public JwtExpiredTokenException(ResponseStatus exceptionStatus) { + super(exceptionStatus); + this.exceptionStatus = exceptionStatus; + } +} diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtInvalidTokenException.java b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtInvalidTokenException.java new file mode 100644 index 0000000..834e403 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtInvalidTokenException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.unauthorized; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtInvalidTokenException extends JwtUnauthorizedTokenException { + + private final ResponseStatus exceptionStatus; + + public JwtInvalidTokenException(ResponseStatus exceptionStatus) { + super(exceptionStatus); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtMalformedTokenException.java b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtMalformedTokenException.java new file mode 100644 index 0000000..e51c238 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtMalformedTokenException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.unauthorized; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtMalformedTokenException extends JwtUnauthorizedTokenException { + + private final ResponseStatus exceptionStatus; + + public JwtMalformedTokenException(ResponseStatus exceptionStatus) { + super(exceptionStatus); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java new file mode 100644 index 0000000..8bcf1a6 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception/jwt/unauthorized/JwtUnauthorizedTokenException.java @@ -0,0 +1,15 @@ +package store.itpick.backend.common.exception.jwt.unauthorized; + +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class JwtUnauthorizedTokenException extends RuntimeException { + + private final ResponseStatus exceptionStatus; + + public JwtUnauthorizedTokenException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception_handler/BaseExceptionControllerAdvice.java b/src/main/java/store/itpick/backend/common/exception_handler/BaseExceptionControllerAdvice.java new file mode 100644 index 0000000..76f2635 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception_handler/BaseExceptionControllerAdvice.java @@ -0,0 +1,66 @@ +package store.itpick.backend.common.exception_handler; + +import jakarta.validation.ConstraintViolationException; +import kuit3.backend.common.exception.BadRequestException; +import kuit3.backend.common.exception.InternalServerErrorException; +import kuit3.backend.common.response.BaseErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpStatus; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; + +import static kuit3.backend.common.response.status.BaseExceptionResponseStatus.*; + +@Slf4j +@RestControllerAdvice +public class BaseExceptionControllerAdvice { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({BadRequestException.class, NoHandlerFoundException.class, TypeMismatchException.class}) + public BaseErrorResponse handle_BadRequest(Exception e) { + log.error("[handle_BadRequest]", e); + return new BaseErrorResponse(URL_NOT_FOUND); + } + + // 위와 동일 (return ResponseEntity<>) + /* + @ExceptionHandler({BadRequestException.class, NoHandlerFoundException.class, TypeMismatchException.class, HttpRequestMethodNotSupportedException.class}) + public ResponseEntity handle_BadRequest(BadRequestException e) { + log.error("[handle_BadRequest]", e); + return ResponseEntity.badRequest().body(new BaseErrorResponse(e.getExceptionStatus())); + } + */ + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public BaseErrorResponse handle_HttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + log.error("[handle_HttpRequestMethodNotSupportedException]", e); + return new BaseErrorResponse(METHOD_NOT_ALLOWED); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(ConstraintViolationException.class) + public BaseErrorResponse handle_ConstraintViolationException(ConstraintViolationException e) { + log.error("[handle_ConstraintViolationException]", e); + return new BaseErrorResponse(BAD_REQUEST, e.getMessage()); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(InternalServerErrorException.class) + public BaseErrorResponse handle_InternalServerError(InternalServerErrorException e) { + log.error("[handle_InternalServerError]", e); + return new BaseErrorResponse(e.getExceptionStatus()); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(RuntimeException.class) + public BaseErrorResponse handle_RuntimeException(Exception e) { + log.error("[handle_RuntimeException]", e); + return new BaseErrorResponse(SERVER_ERROR); + } + +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/exception_handler/DatabaseExceptionControllerAdvice.java b/src/main/java/store/itpick/backend/common/exception_handler/DatabaseExceptionControllerAdvice.java new file mode 100644 index 0000000..7aff76f --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception_handler/DatabaseExceptionControllerAdvice.java @@ -0,0 +1,34 @@ +package store.itpick.backend.common.exception_handler; + +import jakarta.annotation.Priority; +import kuit3.backend.common.response.BaseErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import static kuit3.backend.common.response.status.BaseExceptionResponseStatus.BAD_SQL_GRAMMAR; +import static kuit3.backend.common.response.status.BaseExceptionResponseStatus.DATABASE_ERROR; + +@Slf4j +@Priority(0) +@RestControllerAdvice +public class DatabaseExceptionControllerAdvice { + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(BadSqlGrammarException.class) + public BaseErrorResponse handle_BadSqlGrammarException(BadSqlGrammarException e) { + log.error("[handle_BadSqlGrammarException]", e); + return new BaseErrorResponse(BAD_SQL_GRAMMAR); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(DataAccessException.class) + public BaseErrorResponse handle_DataAccessException(DataAccessException e) { + log.error("[handle_DataAccessException]", e); + return new BaseErrorResponse(DATABASE_ERROR); + } +} diff --git a/src/main/java/store/itpick/backend/common/exception_handler/JwtExceptionControllerAdvice.java b/src/main/java/store/itpick/backend/common/exception_handler/JwtExceptionControllerAdvice.java new file mode 100644 index 0000000..b9d6275 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/exception_handler/JwtExceptionControllerAdvice.java @@ -0,0 +1,31 @@ +package store.itpick.backend.common.exception_handler; + +import jakarta.annotation.Priority; +import kuit3.backend.common.exception.jwt.bad_request.JwtBadRequestException; +import kuit3.backend.common.exception.jwt.unauthorized.JwtUnauthorizedTokenException; +import kuit3.backend.common.response.BaseErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@Priority(0) +@RestControllerAdvice +public class JwtExceptionControllerAdvice { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(JwtBadRequestException.class) + public BaseErrorResponse handle_JwtBadRequestException(JwtBadRequestException e) { + log.error("[handle_JwtBadRequestException]", e); + return new BaseErrorResponse(e.getExceptionStatus()); + } + + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(JwtUnauthorizedTokenException.class) + public BaseErrorResponse handle_JwtUnauthorizedException(JwtUnauthorizedTokenException e) { + log.error("[handle_JwtUnauthorizedException]", e); + return new BaseErrorResponse(e.getExceptionStatus()); + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java b/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java new file mode 100644 index 0000000..573f8c1 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/interceptor/JwtAuthInterceptor.java @@ -0,0 +1,70 @@ +package store.itpick.backend.common.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtExpiredTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtInvalidTokenException; +import store.itpick.backend.common.exception.jwt.bad_request.JwtNoTokenException; +import store.itpick.backend.common.exception.jwt.bad_request.JwtUnsupportedTokenException; +import store.itpick.backend.jwt.JwtProvider; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthInterceptor implements HandlerInterceptor { + + private static final String JWT_TOKEN_PREFIX = "Bearer "; + + private final JwtProvider jwtProvider; + + // 컨트롤러 호출전에 JWT 검증 + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + + String accessToken = resolveAccessToken(request); + validateAccessToken(accessToken); + + String email = jwtProvider.getPrincipal(accessToken); + validatePayload(email); + + long userId = authService.getUserIdByEmail(email); + request.setAttribute("userId", userId); + return true; + } + + private String resolveAccessToken(HttpServletRequest request) { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + validateToken(token); + return token.substring(JWT_TOKEN_PREFIX.length()); + } + + private void validateToken(String token) { + if (token == null) { + throw new JwtNoTokenException(TOKEN_NOT_FOUND); + } + if (!token.startsWith(JWT_TOKEN_PREFIX)) { + throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE); + } + } + + private void validateAccessToken(String accessToken) { + if (jwtProvider.isExpiredToken(accessToken)) { + throw new JwtExpiredTokenException(EXPIRED_TOKEN); + } + } + + private void validatePayload(String email) { + if (email == null) { + throw new JwtInvalidTokenException(INVALID_TOKEN); + } + } + +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/response/BaseErrorResponse.java b/src/main/java/store/itpick/backend/common/response/BaseErrorResponse.java new file mode 100644 index 0000000..121b449 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/response/BaseErrorResponse.java @@ -0,0 +1,47 @@ +package store.itpick.backend.common.response; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@JsonPropertyOrder({"code", "status", "message", "timestamp"}) +public class BaseErrorResponse implements ResponseStatus { + + private final int code; + private final int status; + private final String message; + private final LocalDateTime timestamp; + + public BaseErrorResponse(ResponseStatus status) { + this.code = status.getCode(); + this.status = status.getStatus(); + this.message = status.getMessage(); + this.timestamp = LocalDateTime.now(); + } + + public BaseErrorResponse(ResponseStatus status, String message) { + this.code = status.getCode(); + this.status = status.getStatus(); + this.message = message; + this.timestamp = LocalDateTime.now(); + } + + @Override + public int getCode() { + return code; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } + +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/response/BaseResponse.java b/src/main/java/store/itpick/backend/common/response/BaseResponse.java new file mode 100644 index 0000000..492c793 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/response/BaseResponse.java @@ -0,0 +1,43 @@ +package store.itpick.backend.common.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import kuit3.backend.common.response.status.ResponseStatus; +import lombok.Getter; + +import static kuit3.backend.common.response.status.BaseExceptionResponseStatus.SUCCESS; + +@Getter +@JsonPropertyOrder({"code", "status", "message", "result"}) +public class BaseResponse implements ResponseStatus { + + private final int code; + private final int status; + private final String message; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final T result; + + public BaseResponse(T result) { + this.code = SUCCESS.getCode(); + this.status = SUCCESS.getStatus(); + this.message = SUCCESS.getMessage(); + this.result = result; + } + + @Override + public int getCode() { + return code; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } + +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java new file mode 100644 index 0000000..4adfd70 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/response/status/BaseExceptionResponseStatus.java @@ -0,0 +1,73 @@ +package store.itpick.backend.common.response.status; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum BaseExceptionResponseStatus implements ResponseStatus { + + /** + * 1000: 요청 성공 (OK) + */ + SUCCESS(1000, HttpStatus.OK.value(), "요청에 성공하였습니다."), + + /** + * 2000: Request 오류 (BAD_REQUEST) + */ + BAD_REQUEST(2000, HttpStatus.BAD_REQUEST.value(), "유효하지 않은 요청입니다."), + URL_NOT_FOUND(2001, HttpStatus.BAD_REQUEST.value(), "유효하지 않은 URL 입니다."), + METHOD_NOT_ALLOWED(2002, HttpStatus.METHOD_NOT_ALLOWED.value(), "해당 URL에서는 지원하지 않는 HTTP Method 입니다."), + + /** + * 3000: Server, Database 오류 (INTERNAL_SERVER_ERROR) + */ + SERVER_ERROR(3000, HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버에서 오류가 발생하였습니다."), + DATABASE_ERROR(3001, HttpStatus.INTERNAL_SERVER_ERROR.value(), "데이터베이스에서 오류가 발생하였습니다."), + BAD_SQL_GRAMMAR(3002, HttpStatus.INTERNAL_SERVER_ERROR.value(), "SQL에 오류가 있습니다."), + + /** + * 4000: Authorization 오류 + */ + JWT_ERROR(4000, HttpStatus.UNAUTHORIZED.value(), "JWT에서 오류가 발생하였습니다."), + TOKEN_NOT_FOUND(4001, HttpStatus.BAD_REQUEST.value(), "토큰이 HTTP Header에 없습니다."), + UNSUPPORTED_TOKEN_TYPE(4002, HttpStatus.BAD_REQUEST.value(), "지원되지 않는 토큰 형식입니다."), + INVALID_TOKEN(4003, HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), + MALFORMED_TOKEN(4004, HttpStatus.UNAUTHORIZED.value(), "토큰이 올바르게 구성되지 않았습니다."), + EXPIRED_TOKEN(4005, HttpStatus.UNAUTHORIZED.value(), "만료된 토큰입니다."), + TOKEN_MISMATCH(4006, HttpStatus.UNAUTHORIZED.value(), "로그인 정보가 토큰 정보와 일치하지 않습니다."), + + /** + * 5000: User 오류 + */ + INVALID_USER_VALUE(5000, HttpStatus.BAD_REQUEST.value(), "회원가입 요청에서 잘못된 값이 존재합니다."), + DUPLICATE_EMAIL(5001, HttpStatus.BAD_REQUEST.value(), "이미 존재하는 이메일입니다."), + DUPLICATE_NICKNAME(5002, HttpStatus.BAD_REQUEST.value(), "이미 존재하는 닉네임입니다."), + USER_NOT_FOUND(4003, HttpStatus.BAD_REQUEST.value(), "존재하지 않는 회원입니다."), + PASSWORD_NO_MATCH(4004, HttpStatus.BAD_REQUEST.value(), "비밀번호가 일치하지 않습니다."), + INVALID_USER_STATUS(4005, HttpStatus.BAD_REQUEST.value(), "잘못된 회원 status 값입니다."), + EMAIL_NOT_FOUND(4006, HttpStatus.BAD_REQUEST.value(), "존재하지 않는 이메일입니다."), + INVALID_PASSWORD(4007, HttpStatus.BAD_REQUEST.value(), "유효하지 않는 password입니다."), + + INVALID_STORE_VALUE(6000, HttpStatus.BAD_REQUEST.value(), "가게 등록 요청에서 잘못된 값이 존재합니다."), + DUPLICATE_STORENAME(6001, HttpStatus.BAD_REQUEST.value(), "중복된 가게 이름이 존재합니다."); + + private final int code; + private final int status; + private final String message; + + @Override + public int getCode() { + return code; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } + +} diff --git a/src/main/java/store/itpick/backend/common/response/status/ResponseStatus.java b/src/main/java/store/itpick/backend/common/response/status/ResponseStatus.java new file mode 100644 index 0000000..a579a85 --- /dev/null +++ b/src/main/java/store/itpick/backend/common/response/status/ResponseStatus.java @@ -0,0 +1,11 @@ +package store.itpick.backend.common.response.status; + +public interface ResponseStatus { + + int getCode(); + + int getStatus(); + + String getMessage(); + +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/config/SecurityConfig.java b/src/main/java/store/itpick/backend/config/SecurityConfig.java index c7423b1..08f9500 100644 --- a/src/main/java/store/itpick/backend/config/SecurityConfig.java +++ b/src/main/java/store/itpick/backend/config/SecurityConfig.java @@ -34,4 +34,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ); return http.build(); } -} +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/config/WebConfig.java b/src/main/java/store/itpick/backend/config/WebConfig.java new file mode 100644 index 0000000..120ad07 --- /dev/null +++ b/src/main/java/store/itpick/backend/config/WebConfig.java @@ -0,0 +1,33 @@ +package store.itpick.backend.config; + +import kuit3.backend.common.argument_resolver.JwtAuthHandlerArgumentResolver; +import kuit3.backend.common.interceptor.JwtAuthInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final JwtAuthInterceptor jwtAuthenticationInterceptor; + private final JwtAuthHandlerArgumentResolver jwtAuthHandlerArgumentResolver; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtAuthenticationInterceptor) + .order(1) + .addPathPatterns("/auth/test","/users/**") + .excludePathPatterns("/users"); + //인터셉터 적용 범위 수정 + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(jwtAuthHandlerArgumentResolver); + } +} \ No newline at end of file diff --git a/src/main/java/store/itpick/backend/controller/TestController.java b/src/main/java/store/itpick/backend/controller/TestController.java deleted file mode 100644 index 9d200c4..0000000 --- a/src/main/java/store/itpick/backend/controller/TestController.java +++ /dev/null @@ -1,14 +0,0 @@ -package store.itpick.backend.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@RequiredArgsConstructor -@Controller -public class TestController { - @RequestMapping("/test") - public String test() { - return "test"; - } -} diff --git a/src/main/java/store/itpick/backend/dto/auth/LoginRequest.java b/src/main/java/store/itpick/backend/dto/auth/LoginRequest.java new file mode 100644 index 0000000..642647d --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/auth/LoginRequest.java @@ -0,0 +1,19 @@ +package store.itpick.backend.dto.auth; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class LoginRequest { + + @NotBlank(message = "email: {NotBlank}") + private String email; + + @NotBlank(message = "password: {NotBlank}") + private String password; + +} diff --git a/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java b/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java new file mode 100644 index 0000000..8f262d3 --- /dev/null +++ b/src/main/java/store/itpick/backend/dto/auth/LoginResponse.java @@ -0,0 +1,13 @@ +package store.itpick.backend.dto.auth; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoginResponse { + + private long userId; + private String jwt; + +} diff --git a/src/main/java/store/itpick/backend/jwt/JwtProvider.java b/src/main/java/store/itpick/backend/jwt/JwtProvider.java new file mode 100644 index 0000000..5aa7469 --- /dev/null +++ b/src/main/java/store/itpick/backend/jwt/JwtProvider.java @@ -0,0 +1,69 @@ +package store.itpick.backend.jwt; + +import io.jsonwebtoken.*; +import store.itpick.backend.common.exception.jwt.bad_request.JwtUnsupportedTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtInvalidTokenException; +import store.itpick.backend.common.exception.jwt.unauthorized.JwtMalformedTokenException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +import static store.itpick.backend.common.response.status.BaseExceptionResponseStatus.*; + +@Slf4j +@Component +public class JwtProvider { + + @Value("${secret.jwt-secret-key}") + private String JWT_SECRET_KEY; + + @Value("${secret.jwt-expired-in}") + private long JWT_EXPIRED_IN; + + public String createToken(String principal, long userId) { + log.info("JWT key={}", JWT_SECRET_KEY); + + Claims claims = Jwts.claims().setSubject(principal); + Date now = new Date(); + Date validity = new Date(now.getTime() + JWT_EXPIRED_IN); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .claim("userId", userId) + .signWith(SignatureAlgorithm.HS256, JWT_SECRET_KEY) + .compact(); + } + + public boolean isExpiredToken(String token) throws JwtInvalidTokenException { + try { + Jws claims = Jwts.parserBuilder() + .setSigningKey(JWT_SECRET_KEY).build() + .parseClaimsJws(token); + return claims.getBody().getExpiration().before(new Date()); + + } catch (ExpiredJwtException e) { + return true; + + } catch (UnsupportedJwtException e) { + throw new JwtUnsupportedTokenException(UNSUPPORTED_TOKEN_TYPE); + } catch (MalformedJwtException e) { + throw new JwtMalformedTokenException(MALFORMED_TOKEN); + } catch (IllegalArgumentException e) { + throw new JwtInvalidTokenException(INVALID_TOKEN); + } catch (JwtException e) { + log.error("[JwtTokenProvider.validateAccessToken]", e); + throw e; + } + } + + public String getPrincipal(String token) { + return Jwts.parserBuilder() + .setSigningKey(JWT_SECRET_KEY).build() + .parseClaimsJws(token) + .getBody().getSubject(); + } +} diff --git a/src/main/java/store/itpick/backend/support/logging/LoggingAspect.java b/src/main/java/store/itpick/backend/support/logging/LoggingAspect.java new file mode 100644 index 0000000..4ccd013 --- /dev/null +++ b/src/main/java/store/itpick/backend/support/logging/LoggingAspect.java @@ -0,0 +1,35 @@ +package store.itpick.backend.support.logging; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@Aspect +public class LoggingAspect { + + @Pointcut("execution(public * kuit3.backend..*(..))) " + + "&& !execution(public * kuit3.backend.support.logging..*(..))") + private void allComponents() {} + + @Pointcut("execution(public * kuit3.backend.controller..*(..))") + private void allController() {} + + @Pointcut("execution(public * kuit3.backend.service..*(..))") + private void allService() {} + + @Pointcut("execution(public * kuit3.backend.common.argument_resolver..*(..))") + private void allArgumentResolver() {} + + @Pointcut("execution(public * kuit3.backend.common.interceptor..*(..))") + private void allInterceptor() {} + + @Before("allComponents()") + public void doLog(JoinPoint joinPoint) { + log.info("[{}]", joinPoint.getSignature().toShortString()); + } +} diff --git a/src/main/java/store/itpick/backend/util/BindingResultUtils.java b/src/main/java/store/itpick/backend/util/BindingResultUtils.java new file mode 100644 index 0000000..f3b4d03 --- /dev/null +++ b/src/main/java/store/itpick/backend/util/BindingResultUtils.java @@ -0,0 +1,14 @@ +package store.itpick.backend.util; + +import org.springframework.validation.BindingResult; + +public class BindingResultUtils { + + public static String getErrorMessages(BindingResult bindingResult) { + StringBuilder errorMessages = new StringBuilder(); + bindingResult.getAllErrors() + .forEach(error -> errorMessages.append(error.getDefaultMessage()).append(". ")); + return errorMessages.toString(); + } + +}