From ade4b5a8c8bda3f24198fa1267b39475d0824bd0 Mon Sep 17 00:00:00 2001 From: CYY1007 Date: Sun, 29 Jun 2025 22:02:35 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../api/user/business/UserMapper.java | 25 +++ .../api/user/business/UserService.java | 22 +++ .../user/implementation/UserQueryAdapter.java | 20 ++ .../api/user/persistence/UserRepository.java | 11 ++ .../global/annotations/Adapter.java | 16 ++ .../global/common/CommonResponse.java | 58 ++++++ .../common/exception/ExceptionAdvice.java | 146 ++++++++++++++ .../common/exception/base/BaseErrorCode.java | 11 ++ .../exception/base/GeneralException.java | 24 +++ .../exception/base/GlobalErrorCode.java | 139 ++++++++++++++ .../exception/dto/ErrorResponseDTO.java | 17 ++ .../JwtAuthenticationException.java | 11 ++ .../security/{ => config}/SecurityConfig.java | 43 ++++- .../global/security/filter/JwtAuthFilter.java | 84 +++++++++ .../handler/JwtAccessDeniedHandler.java | 39 ++++ .../handler/JwtAuthenticationEntryPoint.java | 41 ++++ .../JwtAuthenticationExceptionHandler.java | 38 ++++ .../handler/annotation/AuthMember.java | 4 + .../resolver/AuthMemberArgumentResolver.java | 62 ++++++ .../global/security/jwt/dto/TokenDto.java | 17 ++ .../security/provider/TokenProvider.java | 178 ++++++++++++++++++ src/main/resources/application.yml | 4 +- 23 files changed, 1001 insertions(+), 12 deletions(-) create mode 100644 src/main/java/rootbox/rootboxApp/api/user/business/UserMapper.java create mode 100644 src/main/java/rootbox/rootboxApp/api/user/business/UserService.java create mode 100644 src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java create mode 100644 src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java create mode 100644 src/main/java/rootbox/rootboxApp/global/annotations/Adapter.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/CommonResponse.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/base/BaseErrorCode.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/base/GeneralException.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/dto/ErrorResponseDTO.java create mode 100644 src/main/java/rootbox/rootboxApp/global/common/exception/securityError/JwtAuthenticationException.java rename src/main/java/rootbox/rootboxApp/global/security/{ => config}/SecurityConfig.java (58%) create mode 100644 src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/handler/JwtAccessDeniedHandler.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationEntryPoint.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationExceptionHandler.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/handler/annotation/AuthMember.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/jwt/dto/TokenDto.java create mode 100644 src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java diff --git a/build.gradle b/build.gradle index e7bfa5a..9e1cb48 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,9 @@ dependencies { // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + //validation + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/rootbox/rootboxApp/api/user/business/UserMapper.java b/src/main/java/rootbox/rootboxApp/api/user/business/UserMapper.java new file mode 100644 index 0000000..0a73150 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/business/UserMapper.java @@ -0,0 +1,25 @@ +package rootbox.rootboxApp.api.user.business; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import rootbox.rootboxApp.global.entity.User; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class UserMapper { + + private final UserService userService; + private static UserService staticUserService; + + @PostConstruct + public void init(){ + staticUserService = this.userService; + } + + public static Optional toUserSecurity(String id){ + return staticUserService.findById(id); + } +} diff --git a/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java b/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java new file mode 100644 index 0000000..960b89f --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/business/UserService.java @@ -0,0 +1,22 @@ +package rootbox.rootboxApp.api.user.business; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import rootbox.rootboxApp.api.user.implementation.UserQueryAdapter; +import rootbox.rootboxApp.global.entity.User; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UserService { + + private final UserQueryAdapter userQueryAdapter; + + + Optional findById(String id) { + return userQueryAdapter.findUserByIdSecurity(id); + } +} diff --git a/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java new file mode 100644 index 0000000..1f87d7c --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/implementation/UserQueryAdapter.java @@ -0,0 +1,20 @@ +package rootbox.rootboxApp.api.user.implementation; + +import lombok.RequiredArgsConstructor; +import rootbox.rootboxApp.api.user.persistence.UserRepository; +import rootbox.rootboxApp.global.annotations.Adapter; +import rootbox.rootboxApp.global.entity.User; + +import java.util.Optional; + +@Adapter +@RequiredArgsConstructor +public class UserQueryAdapter { + + private final UserRepository userRepository; + + public Optional findUserByIdSecurity(String userId){ + return userRepository.findById(Long.valueOf(userId)); + } + +} diff --git a/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java b/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java new file mode 100644 index 0000000..c939bd6 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/api/user/persistence/UserRepository.java @@ -0,0 +1,11 @@ +package rootbox.rootboxApp.api.user.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import rootbox.rootboxApp.global.entity.User; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + public Optional findById(Long id); +} diff --git a/src/main/java/rootbox/rootboxApp/global/annotations/Adapter.java b/src/main/java/rootbox/rootboxApp/global/annotations/Adapter.java new file mode 100644 index 0000000..584ae11 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/annotations/Adapter.java @@ -0,0 +1,16 @@ +package rootbox.rootboxApp.global.annotations; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Adapter { + // 스프링 빈의 이름을 지정 + @AliasFor(annotation = Component.class) + String value() default ""; +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/CommonResponse.java b/src/main/java/rootbox/rootboxApp/global/common/CommonResponse.java new file mode 100644 index 0000000..072476a --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/CommonResponse.java @@ -0,0 +1,58 @@ +package rootbox.rootboxApp.global.common; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder( {"isSuccess", "code", "message", "data"} ) +public class CommonResponse { + @Override + public String toString() { + try { + ObjectMapper mapper = new ObjectMapper(); + // Java 8 날짜/시간 모듈 등록 + mapper.registerModule(new JavaTimeModule()); + // 날짜와 시간을 ISO-8601 형식의 문자열로 직렬화 + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + // 이쁘게 출력하기 + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @JsonProperty("isSuccess") + private Boolean isSuccess; + @JsonProperty("code") + private String code; + @JsonProperty("message") + private String message; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd") + @JsonProperty("createdAt") + private LocalDateTime createdAt; + @JsonProperty("data") + private T data; + + // 성공한 경우 응답 생성 + + public static CommonResponse onSuccess(T data){ + return new CommonResponse<>(true, "200" , "요청에 성공하였습니다.", LocalDateTime.now(), data); + } + + // 실패한 경우 응답 생성 + public static CommonResponse onFailure(String code, String message, T data){ + return new CommonResponse<>(false,code, message, LocalDateTime.now(), data); + } + +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java b/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java new file mode 100644 index 0000000..6d935ca --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/ExceptionAdvice.java @@ -0,0 +1,146 @@ +package rootbox.rootboxApp.global.common.exception; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import rootbox.rootboxApp.global.common.CommonResponse; +import rootbox.rootboxApp.global.common.exception.base.GeneralException; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; +import rootbox.rootboxApp.global.common.exception.dto.ErrorResponseDTO; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}) +public class ExceptionAdvice extends ResponseEntityExceptionHandler { + + + @org.springframework.web.bind.annotation.ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .findFirst() + .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); + + return handleExceptionInternalConstraint(e, GlobalErrorCode.valueOf(errorMessage), request); + } + + @NotNull + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, HttpHeaders headers, HttpStatus status, WebRequest request) { + + Map errors = new LinkedHashMap<>(); + + e.getBindingResult().getFieldErrors() + .forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(e, GlobalErrorCode.valueOf("BAD_REQUEST"),request,errors); + } + + @org.springframework.web.bind.annotation.ExceptionHandler + public ResponseEntity exception(Exception e, WebRequest request) { + e.printStackTrace(); + + return handleExceptionInternalFalse(e, GlobalErrorCode.SERVER_ERROR.getHttpStatus(),request, e.getMessage()); + } + + @ExceptionHandler(value = GeneralException.class) + public ResponseEntity onThrowException(GeneralException generalException, + @AuthenticationPrincipal User user, HttpServletRequest request) { + getExceptionStackTrace(generalException, user, request); + ErrorResponseDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); + System.out.println(generalException.getMessage()); + System.out.println(generalException.getErrorCode()); + return handleExceptionInternal(generalException,errorReasonHttpStatus, request); + } + + + private ResponseEntity handleExceptionInternal(Exception e, ErrorResponseDTO reason, + HttpServletRequest request) { + + CommonResponse body = CommonResponse.onFailure(reason.getCode(),reason.getMessage(),null); +// e.printStackTrace(); + + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal( + e, + body, + null, + reason.getHttpStatus(), + webRequest + ); + } + + private ResponseEntity handleExceptionInternalFalse(Exception e, + HttpStatus status, WebRequest request, String errorPoint) { + CommonResponse body = CommonResponse.onFailure(GlobalErrorCode.SERVER_ERROR.getCode(), GlobalErrorCode.SERVER_ERROR.getMessage(),errorPoint); + return super.handleExceptionInternal( + e, + body, + HttpHeaders.EMPTY, + status, + request + ); + } + + private ResponseEntity handleExceptionInternalArgs(Exception e, GlobalErrorCode errorCommonStatus, + WebRequest request, Map errorArgs) { + CommonResponse body = CommonResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs); + return super.handleExceptionInternal( + e, + body, + HttpHeaders.EMPTY, + errorCommonStatus.getHttpStatus(), + request + ); + } + + private ResponseEntity handleExceptionInternalConstraint(Exception e, GlobalErrorCode errorCommonStatus, + WebRequest request) { + CommonResponse body = CommonResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + return super.handleExceptionInternal( + e, + body, + HttpHeaders.EMPTY, + errorCommonStatus.getHttpStatus(), + request + ); + } + + private void getExceptionStackTrace(Exception e, @AuthenticationPrincipal User user, + HttpServletRequest request) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + pw.append("\n==========================!!!ERROR TRACE!!!==========================\n"); + pw.append("uri: ").append(request.getRequestURI()).append(" ").append(request.getMethod()).append("\n"); + if (user != null) { + pw.append("uid: ").append(user.getUsername()).append("\n"); + } + pw.append(e.getMessage()); + System.out.println(e.getMessage()); + pw.append("\n====================================================================="); + log.error(sw.toString()); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/base/BaseErrorCode.java b/src/main/java/rootbox/rootboxApp/global/common/exception/base/BaseErrorCode.java new file mode 100644 index 0000000..969f8c6 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/base/BaseErrorCode.java @@ -0,0 +1,11 @@ +package rootbox.rootboxApp.global.common.exception.base; + +import rootbox.rootboxApp.global.common.exception.dto.ErrorResponseDTO; + +public interface BaseErrorCode { + + public ErrorResponseDTO getReason(); + + public ErrorResponseDTO getReasonHttpStatus(); +} + diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/base/GeneralException.java b/src/main/java/rootbox/rootboxApp/global/common/exception/base/GeneralException.java new file mode 100644 index 0000000..ba3fcc4 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/base/GeneralException.java @@ -0,0 +1,24 @@ +package rootbox.rootboxApp.global.common.exception.base; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import rootbox.rootboxApp.global.common.exception.dto.ErrorResponseDTO; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException { + + private final BaseErrorCode errorCode; + + public ErrorResponseDTO getErrorReason() { + return this.errorCode.getReason(); + } + + public ErrorResponseDTO getErrorReasonHttpStatus() { + return this.errorCode.getReasonHttpStatus(); + } + + public String getErrorCode(){ + return this.errorCode.getReason().getCode(); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java b/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java new file mode 100644 index 0000000..25d78ca --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/base/GlobalErrorCode.java @@ -0,0 +1,139 @@ +package rootbox.rootboxApp.global.common.exception.base; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import rootbox.rootboxApp.global.common.exception.dto.ErrorResponseDTO; + +import static org.springframework.http.HttpStatus.*; + +@Getter +@RequiredArgsConstructor +public enum GlobalErrorCode implements BaseErrorCode{ + + + // AUTH + 401 Unauthorized - 권한 없음 + TOKEN_EXPIRED(UNAUTHORIZED, "AUTH401_1", "인증 토큰이 만료 되었습니다. 토큰을 재발급 해주세요"), + INVALID_TOKEN(UNAUTHORIZED, "AUTH401_2", "인증 토큰이 유효하지 않습니다."), + INVALID_REFRESH_TOKEN(UNAUTHORIZED, "AUTH401_3", "리프레시 토큰이 유효하지 않습니다."), + REFRESH_TOKEN_EXPIRED(UNAUTHORIZED, "AUTH401_4", "리프레시 토큰이 만료 되었습니다."), + AUTHENTICATION_REQUIRED(UNAUTHORIZED, "AUTH401_5", "인증 정보가 유효하지 않습니다."), + LOGIN_REQUIRED(UNAUTHORIZED, "AUTH401_6", "로그인이 필요한 서비스입니다."), + + // AUTH + 403 Forbidden - 인증 거부 + AUTHENTICATION_DENIED(FORBIDDEN, "AUTH403_1", "인증이 거부 되었습니다."), + + // AUTH + 404 Not Found - 찾을 수 없음 + REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "AUTH404_1", "리프레시 토큰이 존재하지 않습니다."), + + // GLOBAL + 500 Server Error + SERVER_ERROR(INTERNAL_SERVER_ERROR, "GLOBAL500_1", "서버 에러, 서버 개발자에게 알려주세요."), + + // GLOBAL + Args Validation Error + BAD_ARGS_ERROR(BAD_REQUEST, "GLOBAL400_1", "request body의 validation이 실패했습니다. 응답 body를 참고해주세요"), + + // USER + 400 BAD_REQUEST - 잘못된 요청 + NOT_VALID_PHONE_NUMBER(BAD_REQUEST, "USER400_1", "유효하지 않은 전화번호 입니다."), + FILE_LIMIT_ERROR(BAD_REQUEST, "POST400_1", "파일 용량 제한."), + + // USER + 401 Unauthorized - 권한 없음 + + // USER + 403 Forbidden - 인증 거부 + + // USER + 404 Not Found - 찾을 수 없음 + USER_NOT_FOUND(NOT_FOUND, "USER404_1", "등록된 사용자 정보가 없습니다."), + + // USER + 409 CONFLICT : Resource 를 찾을 수 없음 + DUPLICATE_PHONE_NUMBER(CONFLICT, "USER409_1", "중복된 전화번호가 존재합니다."), + + // MEMBER + 404 Not Found - 찾을 수 없음 + MEMBER_NOT_FOUND(NOT_FOUND, "MEMBER404_1", "존재하지 않는 멤버입니다."), + AVAILABLE_PROFILE_NOT_FOUND(NOT_FOUND, "MEMBER404_2", "현재 선택된 멤버 정보가 없습니다."), + + // TREEHOUSE + 404 Not Found - 찾을 수 없음 + TREEHOUSE_NOT_FOUND(NOT_FOUND, "TREEHOUSE404_1", "존재하지 않는 트리입니다."), + + // INVITATION + 400 Bad Request - 잘못된 요청 + INVITATION_COUNT_ZERO(BAD_REQUEST, "INVITATION400_1", "사용 가능한 초대장 개수가 0입니다."), + + // INVITATION + 404 Not Found - 찾을 수 없음 + INVITATION_NOT_FOUND(NOT_FOUND, "INVITATION404_1", "존재하지 않는 초대장입니다."), + + // INVITATION + 409 CONFLICT : Resource 를 찾을 수 없음 + INVITATION_ALREADY_EXIST(CONFLICT, "INVITATION409_1", "이미 해당 트리하우스에 초대되었습니다."), + + // POST + 401 Unauthorized - 권한 없음 + POST_UNAUTHORIZED(UNAUTHORIZED, "POST401_1", "게시글 수정 및 삭제 권한이 없습니다."), + + // POST + 403 Forbidden - 금지됨 + POST_SELF_REPORT(FORBIDDEN, "POSt403_1", "자신의 게시글은 신고할 수 없습니다."), + + // POST + 404 Not Found - 찾을 수 없음 + POST_NOT_FOUND(NOT_FOUND, "POST404_1", "존재하지 않는 게시글입니다."), + + // COMMENT + 403 Forbidden - 금지됨 + COMMENT_SELF_REPORT(FORBIDDEN, "COMMENT403_1", "자신의 댓글은 신고할 수 없습니다."), + + // COMMENT + 403 Forbidden + COMMENT_DELETE_FORBIDDEN(FORBIDDEN, "COMMENT403_2", "자신이 작성한 게시글에 대한 댓글이나 자신이 작성한 댓글만 삭제 가능합니다."), + + // COMMENT + 404 Not Found - 찾을 수 없음 + COMMENT_NOT_FOUND(NOT_FOUND, "COMMENT404_1", "존재하지 않는 댓글입니다."), + + // REPLY + 400 Bad Request - 잘못된 요청 + REPLY_CREATE_BAD_REQUEST(BAD_REQUEST, "REPLY400_1", "댓글에 대해서만 대댓글 작성 가능합니다."), + + // REPLY + 404 Not Found - 찾을 수 없음 + REPLY_NOT_FOUND(NOT_FOUND, "REPLY404_1", "존재하지 않는 답글입니다."), + + // BRANCH + 404 Not Found - 찾을 수 없음 + BRANCH_NOT_FOUND(NOT_FOUND, "BRANCH404_1", "브랜치 정보를 찾을 수 없습니다."), + + + // NOTIFICATION + 404 Not Found - 찾을 수 없음 + NOTIFICATION_NOT_FOUND(NOT_FOUND, "NOTIFICATION404_1", "존재하지 않는 알림입니다."), + + //FEIGN + 400 BAD_REQUEST - 잘못된 요청 + FEIGN_CLIENT_ERROR_400(BAD_REQUEST, "FEIGN400", "feignClient 에서 400번대 에러가 발생했습니다."), + + //FEIGN + 500 INTERNAL_SERVER_ERROR - 서버 에러 + FEIGN_CLIENT_ERROR_500(INTERNAL_SERVER_ERROR, "FEIGN500", "feignClient 에서 500번대 에러가 발생했습니다."), + + // NCP Phone Auth + PHONE_NUMBER_EXIST(OK, "NCP200_1", "이미 인증된 전화번호입니다."), + PHONE_AUTH_NOT_FOUND(BAD_REQUEST, "NCP400_1", "인증 번호 요청이 필요합니다."), + PHONE_AUTH_WRONG(BAD_REQUEST, "NCP400_2", "잘못된 인증 번호 입니다."), + PHONE_AUTH_TIMEOUT(BAD_REQUEST, "NCP400_3", "인증 시간이 초과되었습니다."), + + FCM_ALREADY_EXISTS_TOKEN(BAD_REQUEST, "FCM400_1", "이미 저장되어 있는 FCM 토큰입니다."), + FCM_TOKEN_NOT_EXISTS(BAD_REQUEST,"FCM400_2", "해당 유저의 FCM Token 이 존재하지 않습니다."), + FCM_ACCESS_TOKEN_REQUEST_ERROR(INTERNAL_SERVER_ERROR, "FCM500_2", "서버 에러, FCM 서버에 AccessToken 요청할 때 에러 발생."), + FCM_SEND_MESSAGE_ERROR(INTERNAL_SERVER_ERROR , "FCM500_3", "서버 에러, FCM 서버에 메시지를 전송할 때 에러 발생. FcmToken이 유효한지 확인해주세요."), ; + + ; + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorResponseDTO getReason() { + return ErrorResponseDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .build(); + } + + @Override + public ErrorResponseDTO getReasonHttpStatus() { + return ErrorResponseDTO.builder() + .message(message) + .code(code) + .httpStatus(httpStatus) + .isSuccess(false) + .build(); + } + +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/dto/ErrorResponseDTO.java b/src/main/java/rootbox/rootboxApp/global/common/exception/dto/ErrorResponseDTO.java new file mode 100644 index 0000000..f178ba5 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/dto/ErrorResponseDTO.java @@ -0,0 +1,17 @@ +package rootbox.rootboxApp.global.common.exception.dto; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ErrorResponseDTO { + + + private HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; +} diff --git a/src/main/java/rootbox/rootboxApp/global/common/exception/securityError/JwtAuthenticationException.java b/src/main/java/rootbox/rootboxApp/global/common/exception/securityError/JwtAuthenticationException.java new file mode 100644 index 0000000..8ec0559 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/common/exception/securityError/JwtAuthenticationException.java @@ -0,0 +1,11 @@ +package rootbox.rootboxApp.global.common.exception.securityError; + +import org.springframework.security.core.AuthenticationException; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; + +public class JwtAuthenticationException extends AuthenticationException { + + public JwtAuthenticationException(GlobalErrorCode code) { + super(code.name()); + } +} \ No newline at end of file diff --git a/src/main/java/rootbox/rootboxApp/global/security/SecurityConfig.java b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java similarity index 58% rename from src/main/java/rootbox/rootboxApp/global/security/SecurityConfig.java rename to src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java index 3be02f2..e9710f7 100644 --- a/src/main/java/rootbox/rootboxApp/global/security/SecurityConfig.java +++ b/src/main/java/rootbox/rootboxApp/global/security/config/SecurityConfig.java @@ -1,15 +1,22 @@ -package rootbox.rootboxApp.global.security; +package rootbox.rootboxApp.global.security.config; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfigurationSource; +import rootbox.rootboxApp.global.security.filter.JwtAuthFilter; +import rootbox.rootboxApp.global.security.handler.JwtAccessDeniedHandler; +import rootbox.rootboxApp.global.security.handler.JwtAuthenticationEntryPoint; +import rootbox.rootboxApp.global.security.handler.JwtAuthenticationExceptionHandler; +import rootbox.rootboxApp.global.security.provider.TokenProvider; import java.util.Collections; @@ -18,15 +25,15 @@ //@EnableWebSecurity(debug = true) @Configuration public class SecurityConfig { -// -// private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint = new JwtAuthenticationEntryPoint(); -// -// private final JwtAccessDeniedHandler jwtAccessDeniedHandler = new JwtAccessDeniedHandler(); -// -// private final TokenProvider tokenProvider; -// -// private final JwtAuthenticationExceptionHandler jwtAuthenticationExceptionHandler = -// new JwtAuthenticationExceptionHandler(); + + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint = new JwtAuthenticationEntryPoint(); + + private final JwtAccessDeniedHandler jwtAccessDeniedHandler = new JwtAccessDeniedHandler(); + + private final TokenProvider tokenProvider; + + private final JwtAuthenticationExceptionHandler jwtAuthenticationExceptionHandler = + new JwtAuthenticationExceptionHandler(); private static final String[] JWT_WHITE_LIST ={ "/users/login-tmp","/users/reissue" @@ -53,12 +60,28 @@ public WebSecurityCustomizer webSecurityCustomizer() { @Bean public SecurityFilterChain JwtFilterChain(HttpSecurity http) throws Exception { return http.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfiguration())) + .httpBasic(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) // 비활성화 .sessionManagement( manage -> manage.sessionCreationPolicy( SessionCreationPolicy.STATELESS)) // Session 사용 안함 .formLogin(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + authorize -> { +// authorize.requestMatchers("/swagger-ui/**").permitAll(); + authorize.requestMatchers("/api/v1/users/**").permitAll(); + authorize.anyRequest().authenticated(); + }) + .exceptionHandling( + exceptionHandling -> + exceptionHandling + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + .accessDeniedHandler(jwtAccessDeniedHandler)) + .addFilterBefore( + new JwtAuthFilter(tokenProvider, JWT_WHITE_LIST), + UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationExceptionHandler, JwtAuthFilter.class) .build(); } diff --git a/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java b/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java new file mode 100644 index 0000000..951de9d --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,84 @@ +package rootbox.rootboxApp.global.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import rootbox.rootboxApp.global.common.CommonResponse; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; +import rootbox.rootboxApp.global.security.provider.TokenProvider; + +import java.io.IOException; +import java.util.Arrays; + +@Slf4j +@RequiredArgsConstructor +// 들어오는 요청 처리 +public class JwtAuthFilter extends OncePerRequestFilter { + private final TokenProvider tokenProvider; + + private final String[] whiteList; + + + /* 요청이 들어올 때마다 실행. + * 토큰 확인, 토큰 유효성 검사, 토큰에 포함된 정보를 기반으로 인증 수행 */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // HTTP 요청에서 Authorization헤더를 찾아 토큰 반환 + String accessToken = tokenProvider.resolveToken(request, "Access"); + + + // 토큰이 있다면 진행 + if(StringUtils.hasText(accessToken) && tokenProvider.validateToken(accessToken)) { + + Authentication authentication = tokenProvider.getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 SecurityContext에 설정 + + } + else{ + SecurityContextHolder.getContext().setAuthentication(null); + } + // 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행 + filterChain.doFilter(request,response); + } + + + private String getRefreshTokenFromRequest(HttpServletRequest request) { + String refreshToken = request.getHeader("Refresh-Token"); + if (StringUtils.hasText(refreshToken)) { + return refreshToken; + } + return null; + } + + + // JWT 인증과 관련된 예외 처리 + public void jwtExceptionHandler(HttpServletResponse response, GlobalErrorCode errorCode) { + response.setStatus(errorCode.getHttpStatus().value()); + response.setContentType("application/json"); + + try { + // AuthErrorCode로부터 code와 message 추출 + String code = errorCode.getCode(); + String message = errorCode.getMessage(); + String json = new ObjectMapper().writeValueAsString(CommonResponse.onFailure(code, message, null)); // ApiResponse 객체를 JSON으로 변환 + response.getWriter().write(json); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getRequestURI(); + return Arrays.stream(whiteList).anyMatch(path::startsWith); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAccessDeniedHandler.java b/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..dfd5307 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAccessDeniedHandler.java @@ -0,0 +1,39 @@ +package rootbox.rootboxApp.global.security.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import rootbox.rootboxApp.global.common.CommonResponse; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; + +import java.io.IOException; +import java.io.PrintWriter; + +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + private final Logger LOGGER = LoggerFactory.getLogger(JwtAccessDeniedHandler.class); + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) + throws IOException, ServletException { + + response.setContentType("application/json; charset=UTF-8"); + response.setStatus(403); + PrintWriter writer = response.getWriter(); + + // AuthErrorCode.AUTHENTICATION_DENIED enum에서 코드와 메시지를 얻음 + String code = GlobalErrorCode.AUTHENTICATION_DENIED.getCode(); + String message = GlobalErrorCode.AUTHENTICATION_DENIED.getMessage(); + CommonResponse apiErrorResult = CommonResponse.onFailure(code, message, null); + writer.write(apiErrorResult.toString()); + writer.flush(); + writer.close(); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationEntryPoint.java b/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..9c04f7b --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationEntryPoint.java @@ -0,0 +1,41 @@ +package rootbox.rootboxApp.global.security.handler; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import rootbox.rootboxApp.global.common.CommonResponse; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; + + +import java.io.IOException; +import java.io.PrintWriter; + +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + + private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) + throws IOException, ServletException { + response.setContentType("application/json; charset=UTF-8"); + response.setStatus(401); + PrintWriter writer = response.getWriter(); + + // AuthErrorCode.AUTHENTICATION_REQUIRED enum에서 코드와 메시지를 얻음 + String code = GlobalErrorCode.AUTHENTICATION_REQUIRED.getCode(); + String message = GlobalErrorCode.AUTHENTICATION_REQUIRED.getMessage(); + CommonResponse apiErrorResult = CommonResponse.onFailure(code, message, null); + + writer.write(apiErrorResult.toString()); + writer.flush(); + writer.close(); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationExceptionHandler.java b/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationExceptionHandler.java new file mode 100644 index 0000000..1decf99 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/handler/JwtAuthenticationExceptionHandler.java @@ -0,0 +1,38 @@ +package rootbox.rootboxApp.global.security.handler; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; +import rootbox.rootboxApp.global.common.CommonResponse; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; +import rootbox.rootboxApp.global.common.exception.securityError.JwtAuthenticationException; + +import java.io.IOException; +import java.io.PrintWriter; + +public class JwtAuthenticationExceptionHandler extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (JwtAuthenticationException authException) { + response.setContentType("application/json; charset=UTF-8"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + + PrintWriter writer = response.getWriter(); + String errorCodeName = authException.getMessage(); + GlobalErrorCode errorCode = GlobalErrorCode.valueOf(errorCodeName); + CommonResponse apiErrorResult = CommonResponse.onFailure(errorCode.getCode(),errorCode.getMessage(), null); + + writer.write(apiErrorResult.toString()); + writer.flush(); + writer.close(); + } + } +} \ No newline at end of file diff --git a/src/main/java/rootbox/rootboxApp/global/security/handler/annotation/AuthMember.java b/src/main/java/rootbox/rootboxApp/global/security/handler/annotation/AuthMember.java new file mode 100644 index 0000000..e5d2b93 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/handler/annotation/AuthMember.java @@ -0,0 +1,4 @@ +package rootbox.rootboxApp.global.security.handler.annotation; + +public @interface AuthMember { +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java b/src/main/java/rootbox/rootboxApp/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java new file mode 100644 index 0000000..96efc20 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/handler/annotation/resolver/AuthMemberArgumentResolver.java @@ -0,0 +1,62 @@ +package rootbox.rootboxApp.global.security.handler.annotation.resolver; + +import org.springframework.core.MethodParameter; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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 rootbox.rootboxApp.api.user.business.UserMapper; +import rootbox.rootboxApp.global.common.exception.base.GeneralException; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; +import rootbox.rootboxApp.global.entity.User; +import rootbox.rootboxApp.global.security.handler.annotation.AuthMember; + +import java.util.Optional; + +public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver { + /** + * supportsParameter + * - 해당 파라미터를 지원하는지 여부를 반환 + * - AuthMember 어노테이션이 없거나, Member 타입이 아닌 경우 false 반환 + */ + @Override + public boolean supportsParameter(MethodParameter parameter) { + AuthMember authUser = parameter.getParameterAnnotation(AuthMember.class); // 메서드 파라미터에서 @AuthMember 찾기 + if (authUser == null) return false; + if (parameter.getParameterType().equals(User.class) == false) { + return false; + } + return true; + } + + /** + * resolveArgument + * 실제로 파라미터의 값을 해석해주는 메서드 + * 파라미터에 전달할 객체를 반환 + * - SecurityContextHolder에서 인증 객체를 가져와서 User 객체로 변환하여 반환 + */ + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Object principal = null; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null) { + principal = authentication.getPrincipal(); + } + if (principal == null || principal.getClass() == String.class) { //Authentication 객체가 null이거나, principal이 String 타입('anonymousUser')인 경우 + throw new GeneralException(GlobalErrorCode.USER_NOT_FOUND); + } + + UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication; + + Optional user = UserMapper.toUserSecurity(authenticationToken.getName()); + + if (user.isEmpty()) + throw new GeneralException(GlobalErrorCode.USER_NOT_FOUND); + else + return user.get(); + } +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/jwt/dto/TokenDto.java b/src/main/java/rootbox/rootboxApp/global/security/jwt/dto/TokenDto.java new file mode 100644 index 0000000..f3f2eae --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/jwt/dto/TokenDto.java @@ -0,0 +1,17 @@ +package rootbox.rootboxApp.global.security.jwt.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TokenDto { + + private String accessToken; + private String refreshToken; + +} diff --git a/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java b/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java new file mode 100644 index 0000000..818d587 --- /dev/null +++ b/src/main/java/rootbox/rootboxApp/global/security/provider/TokenProvider.java @@ -0,0 +1,178 @@ +package rootbox.rootboxApp.global.security.provider; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode; +import rootbox.rootboxApp.global.common.exception.securityError.JwtAuthenticationException; +import rootbox.rootboxApp.global.entity.User; + +import java.security.Key; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TokenProvider { + public static final String ACCESS_TOKEN_HEADER = "Authorization"; + public static final String REFRESH_TOKEN_HEADER = "Refresh"; + private static final String BEARER_PREFIX = "Bearer "; + + @Value("${jwt.access-token-validity-in-seconds}") + private Long TOKEN_TIME; + + @Value("${jwt.refresh-token-validity-in-seconds}") + private Long REFRESH_TOKEN_TIME; + + + @Value("${jwt.secret.key}") + private String secretKey; + private Key key; + + // signature 생성 알고리즘 /토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드 + // signature -> jwt가 변경되지 않았음을 검증하는 역할 + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + + // key 객체 생성 + @PostConstruct + public void init() { + byte[] bytes = Base64.getDecoder().decode(secretKey); + key = Keys.hmacShaKeyFor(bytes); + int h = 7; + } + + // header 토큰을 가져오기 + public String resolveToken(HttpServletRequest request, String tokenType) { + String headerName; + + if ("Access".equals(tokenType)) { + headerName = ACCESS_TOKEN_HEADER; // 올바른 상수 이름이어야 합니다. + } else if ("Refresh".equals(tokenType)) { + headerName = REFRESH_TOKEN_HEADER; // 올바른 상수 이름이어야 합니다. + } else { + return null; // 또는 적절한 예외를 던질 수 있습니다. + } + + String token = request.getHeader(headerName); + if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) { + return token.substring(7); + } + return null; + } + + /* + TODO 아래 코드를 더 잘 짤수 있는지 나중에 수정을 시도할 것 + */ + // 토큰 생성 + public String createAccessToken(User user, Collection authorities) { + Date date = new Date(); + + return + Jwts.builder() + .setSubject(user.getId().toString()) + .claim("authoritiesKey", authorities) + .claim("socialKey", user.getSocialLoginUid()) + .claim("userId", user.getId()) + .setExpiration(new Date(date.getTime() + TOKEN_TIME)) + .setIssuedAt(date) + .signWith(key, signatureAlgorithm) + .compact(); + } + + public String createRefreshToken(){ + + Date data = new Date(); + + Claims claims = Jwts.claims(); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(data) + .setExpiration(new Date(data.getTime() + REFRESH_TOKEN_TIME)) + .signWith(key, signatureAlgorithm) + .compact(); + } + + + // 토큰 검증 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (SecurityException | MalformedJwtException e) { + log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token, 만료된 JWT token 입니다."); + throw new JwtAuthenticationException(GlobalErrorCode.TOKEN_EXPIRED); + + } catch (UnsupportedJwtException e) { + log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); + } catch (IllegalArgumentException e) { + log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); + } + catch (io.jsonwebtoken.security.SignatureException e){ + log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다"); + } + return false; + } + + public void validateRefreshToken(String refreshToken){ + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken); + } catch (SecurityException | MalformedJwtException e) { + log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); + throw new JwtAuthenticationException(GlobalErrorCode.INVALID_TOKEN); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token, 만료된 JWT 리프레시 token 입니다."); + throw new JwtAuthenticationException(GlobalErrorCode.REFRESH_TOKEN_EXPIRED); + } catch (UnsupportedJwtException e) { + log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); + throw new JwtAuthenticationException(GlobalErrorCode.INVALID_TOKEN); + } catch (IllegalArgumentException e) { + log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); + throw new JwtAuthenticationException(GlobalErrorCode.INVALID_TOKEN); + } + catch (io.jsonwebtoken.security.SignatureException e){ + log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다"); + throw new JwtAuthenticationException(GlobalErrorCode.INVALID_TOKEN); + } + } + + // 토큰에서 사용자 정보 가져오기 + public Claims getUserInfoFromToken(String token) { + // jwt 토큰을 파싱해서 그 안에 들어있는 클레임을 추출하는 코드 + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); + } + + public String getMemberIdFromToken(String token) { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject(); + } + + public Authentication getAuthentication(String token){ + Claims claims = + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); + + Collection authorities = + Arrays.stream(claims.get("authoritiesKey").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + org.springframework.security.core.userdetails.User principal = new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(principal, token, authorities); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 903bd18..88bb5f5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -127,13 +127,13 @@ spring: format_sql: true use_sql_comments: true hbm2ddl: - auto: create + auto: update default_batch_fetch_size: 1000 jwt: header: Authorization # dev server secret: - key: ${JWT_SECRET} + key: secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret # secret : ${JWT_SECRET} authorities-key: authoritiesKey access-token-validity-in-seconds: 120000 # 2 min