From 82a8196e1b95da8a583c085ddb45b046ea7e300e Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 19:56:11 +0900 Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20feat:=20User=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=88=98=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petition/domain/account/entity/User.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/smunity/petition/domain/account/entity/User.java b/src/main/java/com/smunity/petition/domain/account/entity/User.java index d0b17de..38025dd 100644 --- a/src/main/java/com/smunity/petition/domain/account/entity/User.java +++ b/src/main/java/com/smunity/petition/domain/account/entity/User.java @@ -9,7 +9,6 @@ import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; -import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @@ -22,9 +21,10 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @EntityListeners(AuditingEntityListener.class) -@Table(name = "auth_user") +@Table(name = "accounts_user") @Entity public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -39,9 +39,8 @@ public class User { @Column(name = "email", nullable = false) private String email; - @Column(name = "is_superuser", nullable = false) - @ColumnDefault("false") - private Boolean isSuperUser; + @Column(name = "name", nullable = false) + private String name; @Column(name = "is_staff", nullable = false) @ColumnDefault("false") @@ -54,12 +53,19 @@ public class User { @Column(name = "last_login") private LocalDateTime lastLogin; - @Column(name = "date_joined") - @CreatedDate - private LocalDateTime dateJoined; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "year_id") + private Year year; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "department_id") + private Department department; - @OneToOne(mappedBy = "user", fetch = FetchType.LAZY) - private Profile profile; + @Column(name = "current_year") + private int currentYear; + + @Column(name = "completed_semester") + private int completedSemesters; @OneToMany(mappedBy = "user") private List questions; @@ -69,8 +75,4 @@ public class User { @OneToMany(mappedBy = "user") private List petitions; - - public void setProfile(Profile profile) { - this.profile = profile; - } } From 7d3e8cda11f8258291a0c0441a0a8a714813f044 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 19:58:50 +0900 Subject: [PATCH 02/17] =?UTF-8?q?=F0=9F=94=A5=20delete:=20Profile=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=82=AD=EC=A0=9C=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/entity/Department.java | 2 +- .../domain/account/entity/Profile.java | 35 ------------------- .../petition/domain/account/entity/Year.java | 14 +++----- 3 files changed, 5 insertions(+), 46 deletions(-) delete mode 100644 src/main/java/com/smunity/petition/domain/account/entity/Profile.java diff --git a/src/main/java/com/smunity/petition/domain/account/entity/Department.java b/src/main/java/com/smunity/petition/domain/account/entity/Department.java index 62c3ccb..ba5d4ba 100644 --- a/src/main/java/com/smunity/petition/domain/account/entity/Department.java +++ b/src/main/java/com/smunity/petition/domain/account/entity/Department.java @@ -26,5 +26,5 @@ public class Department { private String url; @OneToMany(mappedBy = "department") - private List profiles; + private List users; } diff --git a/src/main/java/com/smunity/petition/domain/account/entity/Profile.java b/src/main/java/com/smunity/petition/domain/account/entity/Profile.java deleted file mode 100644 index 1baaf65..0000000 --- a/src/main/java/com/smunity/petition/domain/account/entity/Profile.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.smunity.petition.domain.account.entity; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@Table(name = "accounts_profile") -@Entity -public class Profile { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "name", nullable = false) - private String name; - - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JoinColumn(name = "user_id") - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "year_id") - private Year year; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "department_id") - private Department department; -} diff --git a/src/main/java/com/smunity/petition/domain/account/entity/Year.java b/src/main/java/com/smunity/petition/domain/account/entity/Year.java index 1110085..f64fe7b 100644 --- a/src/main/java/com/smunity/petition/domain/account/entity/Year.java +++ b/src/main/java/com/smunity/petition/domain/account/entity/Year.java @@ -1,16 +1,10 @@ package com.smunity.petition.domain.account.entity; -import java.util.List; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Getter; +import java.util.List; + @Getter @Table(name = "accounts_year") @Entity @@ -38,5 +32,5 @@ public class Year { private int all; @OneToMany(mappedBy = "year") - private List profiles; + private List users; } From 7038dedac06b97cd210ffab998df83722239361e Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 20:55:27 +0900 Subject: [PATCH 03/17] =?UTF-8?q?=F0=9F=94=A5=20delete:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/repository/UserRepository.java | 18 ------------------ .../global/config/WebClientConfig.java | 19 ------------------- 2 files changed, 37 deletions(-) delete mode 100644 src/main/java/com/smunity/petition/domain/account/repository/UserRepository.java delete mode 100644 src/main/java/com/smunity/petition/global/config/WebClientConfig.java diff --git a/src/main/java/com/smunity/petition/domain/account/repository/UserRepository.java b/src/main/java/com/smunity/petition/domain/account/repository/UserRepository.java deleted file mode 100644 index 03ccc43..0000000 --- a/src/main/java/com/smunity/petition/domain/account/repository/UserRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.smunity.petition.domain.account.repository; - -import com.smunity.petition.domain.account.entity.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface UserRepository extends JpaRepository { - - @Query("select u from User u where u.id = :userId") - User findByuserId(@Param("userId") Long userid); - - Optional findByUserName(String userName); -} diff --git a/src/main/java/com/smunity/petition/global/config/WebClientConfig.java b/src/main/java/com/smunity/petition/global/config/WebClientConfig.java deleted file mode 100644 index be6fbea..0000000 --- a/src/main/java/com/smunity/petition/global/config/WebClientConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.smunity.petition.global.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient(WebClient.Builder webClientBuilder) { - return WebClient.builder() - .baseUrl("https://smunity.co.kr") - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .build(); - } -} From 0de8cbb8739a81607fc00bb8ec3caba7b7a89ea7 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 20:56:52 +0900 Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=9A=80=20chore:=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 310485a..1ca932c 100644 --- a/build.gradle +++ b/build.gradle @@ -26,10 +26,17 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.0.4' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '3.2.0' + implementation 'org.springframework.session:spring-session-data-redis:3.1.1' implementation 'org.jsoup:jsoup:1.17.2' implementation 'org.json:json:20240205' + implementation 'com.google.guava:guava:30.1-jre' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' From edbc50452280b94de1fa6e6a6ca228593e6f02d8 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 20:57:28 +0900 Subject: [PATCH 05/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/entity/Department.java | 6 +-- .../petition/domain/account/entity/User.java | 8 ++++ .../petition/domain/account/entity/Year.java | 6 +-- .../petition/service/PetitionService.java | 2 +- .../question/service/AnswerService.java | 2 +- .../question/service/QuestionService.java | 2 +- .../petition/global/common/ApiResponse.java | 4 ++ .../global/common/code/BaseErrorCode.java | 12 ++++- .../global/common/code/status/ErrorCode.java | 28 +++++------ .../common/exception/ExceptionAdvice.java | 47 +++++++++++-------- .../common/exception/GeneralException.java | 11 +---- 11 files changed, 68 insertions(+), 60 deletions(-) diff --git a/src/main/java/com/smunity/petition/domain/account/entity/Department.java b/src/main/java/com/smunity/petition/domain/account/entity/Department.java index ba5d4ba..f366b99 100644 --- a/src/main/java/com/smunity/petition/domain/account/entity/Department.java +++ b/src/main/java/com/smunity/petition/domain/account/entity/Department.java @@ -3,12 +3,11 @@ import jakarta.persistence.*; import lombok.Getter; -import java.util.List; - @Getter @Table(name = "accounts_department") @Entity public class Department { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -24,7 +23,4 @@ public class Department { @Column(name = "url") private String url; - - @OneToMany(mappedBy = "department") - private List users; } diff --git a/src/main/java/com/smunity/petition/domain/account/entity/User.java b/src/main/java/com/smunity/petition/domain/account/entity/User.java index 38025dd..1cac200 100644 --- a/src/main/java/com/smunity/petition/domain/account/entity/User.java +++ b/src/main/java/com/smunity/petition/domain/account/entity/User.java @@ -75,4 +75,12 @@ public class User { @OneToMany(mappedBy = "user") private List petitions; + + public void setYear(Year year) { + this.year = year; + } + + public void setDepartment(Department department) { + this.department = department; + } } diff --git a/src/main/java/com/smunity/petition/domain/account/entity/Year.java b/src/main/java/com/smunity/petition/domain/account/entity/Year.java index f64fe7b..4f349d5 100644 --- a/src/main/java/com/smunity/petition/domain/account/entity/Year.java +++ b/src/main/java/com/smunity/petition/domain/account/entity/Year.java @@ -3,12 +3,11 @@ import jakarta.persistence.*; import lombok.Getter; -import java.util.List; - @Getter @Table(name = "accounts_year") @Entity public class Year { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,7 +29,4 @@ public class Year { @Column(name = "all_score") private int all; - - @OneToMany(mappedBy = "year") - private List users; } diff --git a/src/main/java/com/smunity/petition/domain/petition/service/PetitionService.java b/src/main/java/com/smunity/petition/domain/petition/service/PetitionService.java index 1dda7f0..07c0f2e 100644 --- a/src/main/java/com/smunity/petition/domain/petition/service/PetitionService.java +++ b/src/main/java/com/smunity/petition/domain/petition/service/PetitionService.java @@ -1,7 +1,7 @@ package com.smunity.petition.domain.petition.service; import com.smunity.petition.domain.account.entity.User; -import com.smunity.petition.domain.account.repository.UserRepository; +import com.smunity.petition.domain.account.repository.user.UserRepository; import com.smunity.petition.domain.petition.dto.PetitionRequest; import com.smunity.petition.domain.petition.dto.PetitionResponse; import com.smunity.petition.domain.petition.entity.Petition; diff --git a/src/main/java/com/smunity/petition/domain/question/service/AnswerService.java b/src/main/java/com/smunity/petition/domain/question/service/AnswerService.java index fad86cd..e19fa9f 100644 --- a/src/main/java/com/smunity/petition/domain/question/service/AnswerService.java +++ b/src/main/java/com/smunity/petition/domain/question/service/AnswerService.java @@ -1,7 +1,7 @@ package com.smunity.petition.domain.question.service; import com.smunity.petition.domain.account.entity.User; -import com.smunity.petition.domain.account.repository.UserRepository; +import com.smunity.petition.domain.account.repository.user.UserRepository; import com.smunity.petition.domain.question.dto.AnswerRequestDto; import com.smunity.petition.domain.question.dto.AnswerResponseDto; import com.smunity.petition.domain.question.entity.Answer; diff --git a/src/main/java/com/smunity/petition/domain/question/service/QuestionService.java b/src/main/java/com/smunity/petition/domain/question/service/QuestionService.java index 3bf0813..ca45bf5 100644 --- a/src/main/java/com/smunity/petition/domain/question/service/QuestionService.java +++ b/src/main/java/com/smunity/petition/domain/question/service/QuestionService.java @@ -1,7 +1,7 @@ package com.smunity.petition.domain.question.service; import com.smunity.petition.domain.account.entity.User; -import com.smunity.petition.domain.account.repository.UserRepository; +import com.smunity.petition.domain.account.repository.user.UserRepository; import com.smunity.petition.domain.question.dto.QuestionListDto; import com.smunity.petition.domain.question.dto.QuestionRequestDto; import com.smunity.petition.domain.question.dto.QuestionResponseDto; diff --git a/src/main/java/com/smunity/petition/global/common/ApiResponse.java b/src/main/java/com/smunity/petition/global/common/ApiResponse.java index 3d98bfd..6a3ee46 100644 --- a/src/main/java/com/smunity/petition/global/common/ApiResponse.java +++ b/src/main/java/com/smunity/petition/global/common/ApiResponse.java @@ -34,6 +34,10 @@ public static ApiResponse onFailure(String code, String message, T data) return new ApiResponse<>(code, message, data); } + public static ApiResponse onFailure(String statusCode, String message) { + return new ApiResponse<>(statusCode, message, null); + } + // 게시된 경우 응답 생성 public static ApiResponse created(T result) { return new ApiResponse<>(SuccessStatus._CREATED.getCode(), SuccessStatus._CREATED.getMessage(), result); diff --git a/src/main/java/com/smunity/petition/global/common/code/BaseErrorCode.java b/src/main/java/com/smunity/petition/global/common/code/BaseErrorCode.java index 9a987df..621f027 100644 --- a/src/main/java/com/smunity/petition/global/common/code/BaseErrorCode.java +++ b/src/main/java/com/smunity/petition/global/common/code/BaseErrorCode.java @@ -1,7 +1,15 @@ package com.smunity.petition.global.common.code; +import com.smunity.petition.global.common.ApiResponse; +import org.springframework.http.HttpStatus; + public interface BaseErrorCode { - ErrorReasonDTO getReason(); + + HttpStatus getHttpStatus(); + + String getCode(); + + String getMessage(); - ErrorReasonDTO getReasonHttpStatus(); + ApiResponse getErrorResponse(); } diff --git a/src/main/java/com/smunity/petition/global/common/code/status/ErrorCode.java b/src/main/java/com/smunity/petition/global/common/code/status/ErrorCode.java index 1d5386d..a86fbc8 100644 --- a/src/main/java/com/smunity/petition/global/common/code/status/ErrorCode.java +++ b/src/main/java/com/smunity/petition/global/common/code/status/ErrorCode.java @@ -1,7 +1,7 @@ package com.smunity.petition.global.common.code.status; +import com.smunity.petition.global.common.ApiResponse; import com.smunity.petition.global.common.code.BaseErrorCode; -import com.smunity.petition.global.common.code.ErrorReasonDTO; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -9,18 +9,26 @@ @Getter @AllArgsConstructor public enum ErrorCode implements BaseErrorCode { + // 가장 일반적인 응답 _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + // 멤버 관련 에러 + USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "USER4001", "사용자가 없습니다."), + NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "USER4002", "닉네임은 필수 입니다."), + PASSWORD_NOT_EQUAL(HttpStatus.BAD_REQUEST, "USER4003", "비밀번호가 일치하지 않습니다."), + USER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "USER4004", "사용자가 이미 존재합니다."), + SAMNUL_ERROR(HttpStatus.BAD_REQUEST, "SAM4001", "샘물 에러입니다."), + // 샘물 인증 관련 에러 AUTH_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH401", "아이디 및 비밀번호가 일치하지 않습니다."), AUTH_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH500", "인증 서버 에러, 관리자에게 문의 바랍니다."), //question 관련 에러 - QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND,"QUESTION404", "해당 질문이 존재하지 않습니다."), + QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, "QUESTION404", "해당 질문이 존재하지 않습니다."), ANSWER_NOT_FOUND(HttpStatus.NOT_FOUND, "ANSWER404", "해당 답변이 존재하지 않습니다."), // petition 관련 에러 PETITION_NOT_FOUND(HttpStatus.NOT_FOUND, "PETITION404", "해당 청원이 존재하지 않습니다."); @@ -30,19 +38,7 @@ public enum ErrorCode implements BaseErrorCode { private final String message; @Override - public ErrorReasonDTO getReason() { - return ErrorReasonDTO.builder() - .message(message) - .code(code) - .build(); - } - - @Override - public ErrorReasonDTO getReasonHttpStatus() { - return ErrorReasonDTO.builder() - .message(message) - .code(code) - .httpStatus(httpStatus) - .build(); + public ApiResponse getErrorResponse() { + return ApiResponse.onFailure(code, message); } } diff --git a/src/main/java/com/smunity/petition/global/common/exception/ExceptionAdvice.java b/src/main/java/com/smunity/petition/global/common/exception/ExceptionAdvice.java index 0ece105..16a6024 100644 --- a/src/main/java/com/smunity/petition/global/common/exception/ExceptionAdvice.java +++ b/src/main/java/com/smunity/petition/global/common/exception/ExceptionAdvice.java @@ -1,37 +1,46 @@ package com.smunity.petition.global.common.exception; import com.smunity.petition.global.common.ApiResponse; -import com.smunity.petition.global.common.code.ErrorReasonDTO; -import jakarta.servlet.http.HttpServletRequest; +import com.smunity.petition.global.common.code.BaseErrorCode; +import com.smunity.petition.global.common.code.status.ErrorCode; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.ResponseEntity; 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; @Slf4j @RestControllerAdvice(annotations = {RestController.class}) public class ExceptionAdvice extends ResponseEntityExceptionHandler { - @ExceptionHandler(value = GeneralException.class) - public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { - ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); - return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request); + + @ExceptionHandler({Exception.class}) + public ResponseEntity> handleAllException(Exception e) { + log.error(">>>>> Internal Server Error : ", e); + BaseErrorCode errorCode = ErrorCode._INTERNAL_SERVER_ERROR; + ApiResponse errorResponse = ApiResponse.onFailure( + errorCode.getCode(), + errorCode.getMessage(), + e.getMessage() + ); + return ResponseEntity.internalServerError().body(errorResponse); + } + + @ExceptionHandler({GeneralException.class}) + public ResponseEntity> handleCustomException(GeneralException e) { + log.warn(">>>>> Custom Exception : {}", e.getMessage()); + BaseErrorCode errorCode = e.getErrorCode(); + return ResponseEntity.status(errorCode.getHttpStatus()).body(errorCode.getErrorResponse()); } - private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, - HttpHeaders headers, HttpServletRequest request) { - ApiResponse body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null); - WebRequest webRequest = new ServletWebRequest(request); - return super.handleExceptionInternal( - e, - body, - headers, - reason.getHttpStatus(), - webRequest + @ExceptionHandler({DataIntegrityViolationException.class}) + public ApiResponse handleIntegrityConstraint(DataIntegrityViolationException e) { + log.warn(">>>>> Data Integrity Violation Exception : {}", e.getMessage()); + BaseErrorCode errorStatus = ErrorCode.USER_ALREADY_EXIST; + return ApiResponse.onFailure( + errorStatus.getCode(), + errorStatus.getMessage() ); } } diff --git a/src/main/java/com/smunity/petition/global/common/exception/GeneralException.java b/src/main/java/com/smunity/petition/global/common/exception/GeneralException.java index 81209d6..4399682 100644 --- a/src/main/java/com/smunity/petition/global/common/exception/GeneralException.java +++ b/src/main/java/com/smunity/petition/global/common/exception/GeneralException.java @@ -1,7 +1,6 @@ package com.smunity.petition.global.common.exception; import com.smunity.petition.global.common.code.BaseErrorCode; -import com.smunity.petition.global.common.code.ErrorReasonDTO; import lombok.AllArgsConstructor; import lombok.Getter; @@ -9,13 +8,5 @@ @AllArgsConstructor public class GeneralException extends RuntimeException { - private BaseErrorCode code; - - public ErrorReasonDTO getErrorReason() { - return this.code.getReason(); - } - - public ErrorReasonDTO getErrorReasonHttpStatus() { - return this.code.getReasonHttpStatus(); - } + private final BaseErrorCode errorCode; } From 26cff16a5e99cd0460d6dbb29de70e453e9dd7d5 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 20:57:46 +0900 Subject: [PATCH 06/17] =?UTF-8?q?=E2=9C=A8=20feat:=20Swagger=20token=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petition/global/config/SwaggerConfig.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/smunity/petition/global/config/SwaggerConfig.java b/src/main/java/com/smunity/petition/global/config/SwaggerConfig.java index 391c0cc..06b5a7f 100644 --- a/src/main/java/com/smunity/petition/global/config/SwaggerConfig.java +++ b/src/main/java/com/smunity/petition/global/config/SwaggerConfig.java @@ -1,26 +1,45 @@ package com.smunity.petition.global.config; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SwaggerConfig { + @Bean public OpenAPI getOpenApi() { - return new OpenAPI().info(getSwaggerInfo()); + Server server = new Server().url("/"); + return new OpenAPI() + .info(getSwaggerInfo()) + .components(authSetting()) + .addServersItem(server) + .addSecurityItem(new SecurityRequirement().addList("access-token")); } private Info getSwaggerInfo() { License license = new License(); - license.setName("Copyright (c) 2024 Smunity"); - + license.setName("{Application}"); return new Info() .title("Smunity API Document") - .description("Smunity의 API 문서 입니다.") + .description("This is Smunity's API document.") .version("v0.0.1") .license(license); } + + private Components authSetting() { + return new Components() + .addSecuritySchemes( + "access-token", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT")); + } } From 379cc982e958cef9b2a40e15febc44fcd5f685ba Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 20:58:02 +0900 Subject: [PATCH 07/17] =?UTF-8?q?=E2=9C=A8=20feat:=20DB=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20sql=EB=AC=B8=20?= =?UTF-8?q?(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/sql/accounts_department.sql | 105 ++++++++++++++++++ src/main/resources/sql/accounts_year.sql | 22 ++++ 2 files changed, 127 insertions(+) create mode 100644 src/main/resources/sql/accounts_department.sql create mode 100644 src/main/resources/sql/accounts_year.sql diff --git a/src/main/resources/sql/accounts_department.sql b/src/main/resources/sql/accounts_department.sql new file mode 100644 index 0000000..ea65590 --- /dev/null +++ b/src/main/resources/sql/accounts_department.sql @@ -0,0 +1,105 @@ +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '컴퓨터과학전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03005&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '전기공학전공', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03208&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '지능IOT융합전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03209&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '게임전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?&srYear=2024&srShyr=all&srSust=03006'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '애니메이션전공', '예술', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03007&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '휴먼지능정보공학전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03204&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '핀테크전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2023&srSust=03205&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '빅데이터융합전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03206&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '스마트생산전공', '공학', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03207&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '생명공학전공', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03000&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '화학에너지공학전공', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03001&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '화공신소재전공', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03002&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('융합공과대학', '한일문화콘텐츠전공', '인문', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03008&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '역사콘텐츠전공', '인문', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02988&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '지적재산권전공', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02989&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '문헌정보학전공', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02990&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '공간환경학부', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00962&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '행정학부', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=03183&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '가족복지학과', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00951&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('인문사회과학대학', '국가안보학과', '인문', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00964&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('사범대학', '국어교육과', '인문', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00971&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('사범대학', '영어교육과', '인문', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00972&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('사범대학', '교육학과', '인문', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00975&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('사범대학', '수학교육과', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=00976&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('경영경제대학', '경제금융학부', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=01178&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('경영경제대학', '경영학부', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=01179&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('경영경제대학', '글로벌경영학과', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=01180&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('경영경제대학', '융합경영학과', '사회', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=01181&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '식품영양학전공', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02994&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '의류학전공', '자연', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02995&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '스포츠건강관리전공', '예술', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02997&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '무용예술전공', '예술', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02998&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '조형예술전공', '예술', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02991&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '생활예술전공', '예술', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=02992&srShyr=all'); +INSERT INTO accounts_department (college, name, type, url) +VALUES ('문화예술대학', '음악학부', '예술', + 'https://www.smu.ac.kr/_custom/smu/_app/curriculum.do?srYear=2024&srSust=01132&srShyr=all'); diff --git a/src/main/resources/sql/accounts_year.sql b/src/main/resources/sql/accounts_year.sql new file mode 100644 index 0000000..eb1c611 --- /dev/null +++ b/src/main/resources/sql/accounts_year.sql @@ -0,0 +1,22 @@ +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2017', 15, 45, 36, 3, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2018', 15, 45, 33, 3, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2019', 15, 45, 33, 3, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2020', 15, 45, 33, 4, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2021', 15, 45, 33, 4, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2022', 15, 45, 33, 4, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2023', 15, 60, 33, 4, 130); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2015', 0, 0, 0, 0, 0); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2016', 0, 0, 0, 0, 0); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2012', 0, 0, 0, 0, 0); +INSERT INTO accounts_year (year, major_i, major_s, culture, culture_cnt, all_score) +VALUES ('2024', 0, 0, 0, 0, 0); From 302c8ac61a5d5a482cc831febc3972b677ec8fe4 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:04:54 +0900 Subject: [PATCH 08/17] =?UTF-8?q?=E2=9C=A8=20feat:=20Cors,=20WebMvc=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petition/global/config/CorsConfig.java | 34 +++++++++++++++++++ .../petition/global/config/WebMvcConfig.java | 21 ++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/main/java/com/smunity/petition/global/config/CorsConfig.java create mode 100644 src/main/java/com/smunity/petition/global/config/WebMvcConfig.java diff --git a/src/main/java/com/smunity/petition/global/config/CorsConfig.java b/src/main/java/com/smunity/petition/global/config/CorsConfig.java new file mode 100644 index 0000000..0563f30 --- /dev/null +++ b/src/main/java/com/smunity/petition/global/config/CorsConfig.java @@ -0,0 +1,34 @@ +package com.smunity.petition.global.config; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.ArrayList; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CorsConfig implements WebMvcConfigurer { + + public static CorsConfigurationSource apiConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + ArrayList allowedOriginPatterns = new ArrayList<>(); + allowedOriginPatterns.add("http://localhost:8080"); + allowedOriginPatterns.add("http://localhost:3000"); + + ArrayList allowedHttpMethods = new ArrayList<>(); + allowedHttpMethods.add("GET"); + allowedHttpMethods.add("POST"); + + configuration.setAllowedOrigins(allowedOriginPatterns); + configuration.setAllowedMethods(allowedHttpMethods); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} diff --git a/src/main/java/com/smunity/petition/global/config/WebMvcConfig.java b/src/main/java/com/smunity/petition/global/config/WebMvcConfig.java new file mode 100644 index 0000000..4db9d7a --- /dev/null +++ b/src/main/java/com/smunity/petition/global/config/WebMvcConfig.java @@ -0,0 +1,21 @@ +package com.smunity.petition.global.config; + +import com.smunity.petition.domain.account.annotation.AccountArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final AccountArgumentResolver accountArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(accountArgumentResolver); + } +} From 2b96da98ecd3a944d6be6fd16d7f41294549d2e4 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:05:22 +0900 Subject: [PATCH 09/17] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=84=A4=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/DepartmentJpaRepository.java | 11 ++++++++ .../account/repository/YearJpaRepository.java | 12 +++++++++ .../repository/user/UserJpaRepository.java | 11 ++++++++ .../repository/user/UserRepository.java | 13 ++++++++++ .../repository/user/UserRepositoryImpl.java | 25 +++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/repository/DepartmentJpaRepository.java create mode 100644 src/main/java/com/smunity/petition/domain/account/repository/YearJpaRepository.java create mode 100644 src/main/java/com/smunity/petition/domain/account/repository/user/UserJpaRepository.java create mode 100644 src/main/java/com/smunity/petition/domain/account/repository/user/UserRepository.java create mode 100644 src/main/java/com/smunity/petition/domain/account/repository/user/UserRepositoryImpl.java diff --git a/src/main/java/com/smunity/petition/domain/account/repository/DepartmentJpaRepository.java b/src/main/java/com/smunity/petition/domain/account/repository/DepartmentJpaRepository.java new file mode 100644 index 0000000..fd6e856 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/repository/DepartmentJpaRepository.java @@ -0,0 +1,11 @@ +package com.smunity.petition.domain.account.repository; + +import com.smunity.petition.domain.account.entity.Department; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface DepartmentJpaRepository extends JpaRepository { + + Optional findByName(String name); +} diff --git a/src/main/java/com/smunity/petition/domain/account/repository/YearJpaRepository.java b/src/main/java/com/smunity/petition/domain/account/repository/YearJpaRepository.java new file mode 100644 index 0000000..6a3b673 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/repository/YearJpaRepository.java @@ -0,0 +1,12 @@ +package com.smunity.petition.domain.account.repository; + + +import com.smunity.petition.domain.account.entity.Year; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface YearJpaRepository extends JpaRepository { + + Optional findByYear(String year); +} diff --git a/src/main/java/com/smunity/petition/domain/account/repository/user/UserJpaRepository.java b/src/main/java/com/smunity/petition/domain/account/repository/user/UserJpaRepository.java new file mode 100644 index 0000000..17de07e --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/repository/user/UserJpaRepository.java @@ -0,0 +1,11 @@ +package com.smunity.petition.domain.account.repository.user; + +import com.smunity.petition.domain.account.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserJpaRepository extends JpaRepository { + + Optional findByUserName(String username); +} diff --git a/src/main/java/com/smunity/petition/domain/account/repository/user/UserRepository.java b/src/main/java/com/smunity/petition/domain/account/repository/user/UserRepository.java new file mode 100644 index 0000000..04445e5 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/repository/user/UserRepository.java @@ -0,0 +1,13 @@ +package com.smunity.petition.domain.account.repository.user; + + +import com.smunity.petition.domain.account.entity.User; + +import java.util.Optional; + +public interface UserRepository { + + Optional findByUserName(String username); + + User save(User user); +} diff --git a/src/main/java/com/smunity/petition/domain/account/repository/user/UserRepositoryImpl.java b/src/main/java/com/smunity/petition/domain/account/repository/user/UserRepositoryImpl.java new file mode 100644 index 0000000..2276bf8 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/repository/user/UserRepositoryImpl.java @@ -0,0 +1,25 @@ +package com.smunity.petition.domain.account.repository.user; + +import com.smunity.petition.domain.account.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@RequiredArgsConstructor +@Repository +public class UserRepositoryImpl implements UserRepository { + + private final UserJpaRepository userJpaRepository; + + + @Override + public Optional findByUserName(String username) { + return userJpaRepository.findByUserName(username); + } + + @Override + public User save(User user) { + return userJpaRepository.save(user); + } +} From 0b09b8a87d83f17ac9508bb66865d805ba108d9a Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:06:04 +0900 Subject: [PATCH 10/17] =?UTF-8?q?=E2=9C=A8=20feat:=20redis=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/jwt/util/RedisUtil.java | 30 +++++++++++++++++ .../petition/global/config/RedisConfig.java | 33 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/util/RedisUtil.java create mode 100644 src/main/java/com/smunity/petition/global/config/RedisConfig.java diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/util/RedisUtil.java b/src/main/java/com/smunity/petition/domain/account/jwt/util/RedisUtil.java new file mode 100644 index 0000000..6d67a69 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/util/RedisUtil.java @@ -0,0 +1,30 @@ +package com.smunity.petition.domain.account.jwt.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + public void save(String key, Object val, Long time, TimeUnit timeUnit) { + redisTemplate.opsForValue().set(key, val, time, timeUnit); + } + + public boolean hasKey(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + public Object get(String key) { + return redisTemplate.opsForValue().get(key); + } + + public boolean delete(String key) { + return Boolean.TRUE.equals(redisTemplate.delete(key)); + } +} diff --git a/src/main/java/com/smunity/petition/global/config/RedisConfig.java b/src/main/java/com/smunity/petition/global/config/RedisConfig.java new file mode 100644 index 0000000..96cc3a4 --- /dev/null +++ b/src/main/java/com/smunity/petition/global/config/RedisConfig.java @@ -0,0 +1,33 @@ +package com.smunity.petition.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${redis.host}") + private String redisHost; + + @Value("${redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} From 53ad70afb282698060751d85993d8d05cbe6c73d Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:07:04 +0900 Subject: [PATCH 11/17] =?UTF-8?q?=E2=9C=A8=20feat:=20Pbkdf2PasswordEncoder?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/encoder/Pbkdf2PasswordEncoder.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/java/com/smunity/petition/global/config/encoder/Pbkdf2PasswordEncoder.java diff --git a/src/main/java/com/smunity/petition/global/config/encoder/Pbkdf2PasswordEncoder.java b/src/main/java/com/smunity/petition/global/config/encoder/Pbkdf2PasswordEncoder.java new file mode 100644 index 0000000..44d5ffe --- /dev/null +++ b/src/main/java/com/smunity/petition/global/config/encoder/Pbkdf2PasswordEncoder.java @@ -0,0 +1,64 @@ +package com.smunity.petition.global.config.encoder; + +import com.google.common.base.Charsets; +import lombok.val; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.crypto.keygen.StringKeyGenerator; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Base64; + +public class Pbkdf2PasswordEncoder implements PasswordEncoder { + private final StringKeyGenerator saltGenerator = KeyGenerators.string(); + private final String PREFIX = "pbkdf2_sha256"; + private final int ITERATIONS = 390000; + private final int HASH_WIDTH = 256; + private final String ALGORITHM = "PBKDF2WithHmacSHA256"; + + private byte[] base64Decode(String string) { + return Base64.getDecoder().decode(string); + } + + private String base64Encode(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + private byte[] encodeWithSaltAndIterations(CharSequence rawPassword, byte[] salt, int iterations) { + val keySpec = new PBEKeySpec(rawPassword.toString().toCharArray(), salt, iterations, HASH_WIDTH); + try { + return SecretKeyFactory.getInstance(ALGORITHM).generateSecret(keySpec).getEncoded(); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Could not create hash", e); + } + } + + private byte[] encodeWithSalt(CharSequence rawPassword, byte[] salt) { + return encodeWithSaltAndIterations(rawPassword, salt, ITERATIONS); + } + + @Override + public String encode(CharSequence rawPassword) { + val salt = saltGenerator.generateKey(); + val hash = encodeWithSalt(rawPassword, salt.getBytes(Charsets.US_ASCII)); + val encodedHash = base64Encode(hash); + return String.join("$", Arrays.asList(PREFIX, Integer.toString(ITERATIONS), salt, encodedHash)); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + if (!encodedPassword.startsWith(PREFIX)) + throw new IllegalArgumentException("Encoded password does not start with: $PREFIX"); + val parts = encodedPassword.split("\\$"); + if (parts.length != 4) + throw new IllegalArgumentException("The encoded password format does not have 4 parts"); + val iterations = Integer.parseInt(parts[1]); + val salt = parts[2].getBytes(Charsets.US_ASCII); + val hash = base64Decode(parts[3]); + return MessageDigest.isEqual(hash, encodeWithSaltAndIterations(rawPassword, salt, iterations)); + } +} From b14d06fec811b03d160ed810d1193d17e3176771 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:07:14 +0900 Subject: [PATCH 12/17] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8A=A4=ED=94=84?= =?UTF-8?q?=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SecurityConfig.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/main/java/com/smunity/petition/global/config/SecurityConfig.java diff --git a/src/main/java/com/smunity/petition/global/config/SecurityConfig.java b/src/main/java/com/smunity/petition/global/config/SecurityConfig.java new file mode 100644 index 0000000..ddd45f2 --- /dev/null +++ b/src/main/java/com/smunity/petition/global/config/SecurityConfig.java @@ -0,0 +1,128 @@ +package com.smunity.petition.global.config; + +import com.smunity.petition.domain.account.jwt.exception.JwtAccessDeniedHandler; +import com.smunity.petition.domain.account.jwt.exception.JwtAuthenticationEntryPoint; +import com.smunity.petition.domain.account.jwt.filter.CustomLoginFilter; +import com.smunity.petition.domain.account.jwt.filter.CustomLogoutHandler; +import com.smunity.petition.domain.account.jwt.filter.JwtAuthenticationFilter; +import com.smunity.petition.domain.account.jwt.filter.JwtExceptionFilter; +import com.smunity.petition.domain.account.jwt.util.HttpResponseUtil; +import com.smunity.petition.domain.account.jwt.util.JwtUtil; +import com.smunity.petition.domain.account.jwt.util.RedisUtil; +import com.smunity.petition.global.config.encoder.Pbkdf2PasswordEncoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.util.Arrays; +import java.util.stream.Stream; + +@Slf4j +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final String[] swaggerUrls = {"/swagger-ui/**", "/v3/**"}; + private final String[] authUrls = {"/", "/api/v1/accounts/register/**", "/api/v1/accounts/login/**", "/api/v1/auth", "/api/v1/questions", "/api/v1/petitions"}; + private final String[] allowedUrls = Stream.concat(Arrays.stream(swaggerUrls), Arrays.stream(authUrls)) + .toArray(String[]::new); + + private final AuthenticationConfiguration authenticationConfiguration; + + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new Pbkdf2PasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http + .cors(cors -> cors + .configurationSource(CorsConfig.apiConfigurationSource())); + + // csrf disable + http + .csrf(AbstractHttpConfigurer::disable); + + // form 로그인 방식 disable + http + .formLogin(AbstractHttpConfigurer::disable); + + // http basic 인증 방식 disable + http + .httpBasic(AbstractHttpConfigurer::disable); + + // 경로별 인가 작업 + http + .authorizeHttpRequests(auth -> auth + .requestMatchers(allowedUrls).permitAll() + .requestMatchers("/**").authenticated() + .anyRequest().permitAll() + ); + + // Jwt Filter (with login) + CustomLoginFilter loginFilter = new CustomLoginFilter( + authenticationManager(authenticationConfiguration), jwtUtil + ); + loginFilter.setFilterProcessesUrl("/api/v1/accounts/login"); + + http + .addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class); + + http + .addFilterBefore(new JwtAuthenticationFilter(jwtUtil, redisUtil), CustomLoginFilter.class); + + http + .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class); + + http + .exceptionHandling(exception -> exception + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + .accessDeniedHandler(jwtAccessDeniedHandler) + ); + + // 세션 사용 안함 + http + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ); + + // Logout Filter + http + .logout(logout -> logout + .logoutUrl("/api/v1/accounts/logout") + .addLogoutHandler(new CustomLogoutHandler(redisUtil, jwtUtil)) + .logoutSuccessHandler((request, response, authentication) + -> HttpResponseUtil.setSuccessResponse( + response, + HttpStatus.OK, + "로그아웃 성공" + )) + ); + + return http.build(); + } +} From 4bd5b47c5f07424a6d3b006cf14b44b82544e774 Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:07:26 +0900 Subject: [PATCH 13/17] =?UTF-8?q?=E2=9C=A8=20feat:=20jwt=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/account/jwt/dto/JwtDto.java | 7 + .../jwt/exception/JwtAccessDeniedHandler.java | 26 +++ .../JwtAuthenticationEntryPoint.java | 35 +++++ .../jwt/filter/JwtAuthenticationFilter.java | 81 ++++++++++ .../jwt/filter/JwtExceptionFilter.java | 55 +++++++ .../domain/account/jwt/util/JwtUtil.java | 148 ++++++++++++++++++ 6 files changed, 352 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/dto/JwtDto.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAccessDeniedHandler.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAuthenticationEntryPoint.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtExceptionFilter.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/util/JwtUtil.java diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/dto/JwtDto.java b/src/main/java/com/smunity/petition/domain/account/jwt/dto/JwtDto.java new file mode 100644 index 0000000..1d27035 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/dto/JwtDto.java @@ -0,0 +1,7 @@ +package com.smunity.petition.domain.account.jwt.dto; + +public record JwtDto( + String accessToken, + String refreshToken +) { +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAccessDeniedHandler.java b/src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..85ea2dc --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAccessDeniedHandler.java @@ -0,0 +1,26 @@ +package com.smunity.petition.domain.account.jwt.exception; + +import com.smunity.petition.domain.account.jwt.util.HttpResponseUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + log.warn("Access Denied: ", accessDeniedException); + + HttpResponseUtil.setErrorResponse(response, HttpStatus.FORBIDDEN, + TokenErrorCode.FORBIDDEN.getErrorResponse()); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAuthenticationEntryPoint.java b/src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..f732dc6 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/exception/JwtAuthenticationEntryPoint.java @@ -0,0 +1,35 @@ +package com.smunity.petition.domain.account.jwt.exception; + +import com.smunity.petition.domain.account.jwt.util.HttpResponseUtil; +import com.smunity.petition.global.common.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +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 JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) + throws IOException { + HttpStatus httpStatus; + ApiResponse errorResponse; + + log.error(">>>>>> AuthenticationException: ", authException); + httpStatus = HttpStatus.UNAUTHORIZED; + errorResponse = ApiResponse.onFailure( + TokenErrorCode.UNAUTHORIZED.getCode(), + TokenErrorCode.UNAUTHORIZED.getMessage(), + authException.getMessage()); + + HttpResponseUtil.setErrorResponse(response, httpStatus, errorResponse); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..66d8bb4 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtAuthenticationFilter.java @@ -0,0 +1,81 @@ +package com.smunity.petition.domain.account.jwt.filter; + +import com.smunity.petition.domain.account.jwt.userdetails.CustomUserDetails; +import com.smunity.petition.domain.account.jwt.util.JwtUtil; +import com.smunity.petition.domain.account.jwt.util.RedisUtil; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + log.info("[*] Jwt Filter"); + + try { + String accessToken = jwtUtil.resolveAccessToken(request); + + // accessToken 없이 접근할 경우 + if (accessToken == null) { + filterChain.doFilter(request, response); + return; + } + + // logout 처리된 accessToken + if (redisUtil.get(accessToken) != null && redisUtil.get(accessToken).equals("logout")) { + log.info("[*] Logout accessToken"); + // TODO InsufficientAuthenticationException 예외 처리 + log.info("=================="); + filterChain.doFilter(request, response); + log.info("=================="); + return; + } + + log.info("[*] Authorization with Token"); + authenticateAccessToken(accessToken); + filterChain.doFilter(request, response); + } catch (ExpiredJwtException e) { + log.warn("[*] case : accessToken Expired"); + } + } + + private void authenticateAccessToken(String accessToken) { + CustomUserDetails userDetails = new CustomUserDetails( + jwtUtil.getUsername(accessToken), + null, + jwtUtil.isStaff(accessToken) + ); + + log.info("[*] Authority Registration"); + + // 스프링 시큐리티 인증 토큰 생성 + Authentication authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities()); + + // 컨텍스트 홀더에 저장 + SecurityContextHolder.getContext().setAuthentication(authToken); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtExceptionFilter.java b/src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtExceptionFilter.java new file mode 100644 index 0000000..ce84c13 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/filter/JwtExceptionFilter.java @@ -0,0 +1,55 @@ +package com.smunity.petition.domain.account.jwt.filter; + +import com.smunity.petition.domain.account.jwt.exception.SecurityCustomException; +import com.smunity.petition.domain.account.jwt.exception.TokenErrorCode; +import com.smunity.petition.domain.account.jwt.util.HttpResponseUtil; +import com.smunity.petition.global.common.ApiResponse; +import com.smunity.petition.global.common.code.BaseErrorCode; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +public class JwtExceptionFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws IOException { + try { + filterChain.doFilter(request, response); + } catch (SecurityCustomException e) { + log.warn(">>>>> SecurityCustomException : ", e); + BaseErrorCode errorCode = e.getErrorCode(); + ApiResponse errorResponse = ApiResponse.onFailure( + errorCode.getCode(), + errorCode.getMessage(), + e.getMessage() + ); + HttpResponseUtil.setErrorResponse( + response, + errorCode.getHttpStatus(), + errorResponse + ); + } catch (Exception e) { + log.error(">>>>> Exception : ", e); + ApiResponse errorResponse = ApiResponse.onFailure( + TokenErrorCode.INTERNAL_SECURITY_ERROR.getCode(), + TokenErrorCode.INTERNAL_SECURITY_ERROR.getMessage(), + e.getMessage() + ); + HttpResponseUtil.setErrorResponse( + response, + HttpStatus.INTERNAL_SERVER_ERROR, + errorResponse + ); + } + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/util/JwtUtil.java b/src/main/java/com/smunity/petition/domain/account/jwt/util/JwtUtil.java new file mode 100644 index 0000000..bf418bb --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/util/JwtUtil.java @@ -0,0 +1,148 @@ +package com.smunity.petition.domain.account.jwt.util; + +import com.smunity.petition.domain.account.jwt.dto.JwtDto; +import com.smunity.petition.domain.account.jwt.exception.SecurityCustomException; +import com.smunity.petition.domain.account.jwt.exception.TokenErrorCode; +import com.smunity.petition.domain.account.jwt.userdetails.CustomUserDetails; +import com.smunity.petition.domain.account.jwt.userdetails.CustomUserDetailsService; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.SignatureException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class JwtUtil { + + private final CustomUserDetailsService customUserDetailsService; + private final SecretKey secretKey; + private final Long accessExpMs; + private final Long refreshExpMs; + private final RedisUtil redisUtil; + + // TODO 따로 뺄지 고민. 추상화 문제 + public JwtUtil( + CustomUserDetailsService customUserDetailsService, @Value("${jwt.secret}") String secret, + @Value("${jwt.token.access-expiration-time}") Long access, + @Value("${jwt.token.refresh-expiration-time}") Long refresh, + RedisUtil redis) { + this.customUserDetailsService = customUserDetailsService; + + secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS256.key().build().getAlgorithm()); + accessExpMs = access; + refreshExpMs = refresh; + redisUtil = redis; + } + + public String getUsername(String token) throws SignatureException { + return Jwts.parser() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody() + .get("username", String.class); + } + + public Boolean isStaff(String token) throws SignatureException { + return (Boolean) Jwts.parser().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("is_staff"); + } + + public Boolean isExpired(String token) throws SignatureException { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration() + .before(new Date()); + } + + public Long getExpTime(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration() + .getTime(); + } + + public String createJwtAccessToken(CustomUserDetails customUserDetails) { + Instant issuedAt = Instant.now(); + Instant expiration = issuedAt.plusMillis(accessExpMs); + + return Jwts.builder() + .header() + .add("alg", "HS256") + .add("typ", "JWT") + .and() + .claim("username", customUserDetails.getUsername()) + .claim("is_staff", customUserDetails.getStaff()) + .issuedAt(Date.from(issuedAt)) + .expiration(Date.from(expiration)) + .signWith(secretKey) + .compact(); + } + + public String createJwtRefreshToken(CustomUserDetails customUserDetails) { + Instant issuedAt = Instant.now(); + Instant expiration = issuedAt.plusMillis(refreshExpMs); + + String refreshToken = Jwts.builder() + .header() + .add("alg", "HS256") + .add("typ", "JWT") + .and() + .claim("username", customUserDetails.getUsername()) + .claim("is_staff", customUserDetails.getStaff()) + .issuedAt(Date.from(issuedAt)) + .expiration(Date.from(expiration)) + .signWith(secretKey) + .compact(); + + redisUtil.save( + customUserDetails.getUsername(), + refreshToken, + refreshExpMs, + TimeUnit.MILLISECONDS + ); + return refreshToken; + } + + public JwtDto reissueToken(String refreshToken) throws SignatureException { + UserDetails userDetails = customUserDetailsService.loadUserByUsername(getUsername(refreshToken)); + + return new JwtDto( + createJwtAccessToken((CustomUserDetails) userDetails), + createJwtRefreshToken((CustomUserDetails) userDetails) + ); + } + + public String resolveAccessToken(HttpServletRequest request) { + String authorization = request.getHeader("Authorization"); + + if (authorization == null || !authorization.startsWith("Bearer ")) { + + log.warn("[*] No Token in req"); + + return null; + } + + log.info("[*] Token exists"); + + return authorization.split(" ")[1]; + } + + public boolean validateRefreshToken(String refreshToken) { + // refreshToken validate + String username = getUsername(refreshToken); + + //redis 확인 + if (!redisUtil.hasKey(username)) { + throw new SecurityCustomException(TokenErrorCode.INVALID_TOKEN); + } + return true; + } + +} From b29864e5776ea6fb0e906fee154b105c8b141fbd Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:07:58 +0900 Subject: [PATCH 14/17] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20API=20=EC=9E=91=EC=84=B1=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AccountsController.java | 52 +++++++++++++++++++ .../account/dto/UserRegisterRequestDto.java | 48 +++++++++++++++++ .../account/dto/UserRegisterResponseDto.java | 31 +++++++++++ .../exception/AccountsExceptionHandler.java | 11 ++++ .../account/service/AccountsQueryService.java | 22 ++++++++ .../account/service/AccountsService.java | 47 +++++++++++++++++ 6 files changed, 211 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/controller/AccountsController.java create mode 100644 src/main/java/com/smunity/petition/domain/account/dto/UserRegisterRequestDto.java create mode 100644 src/main/java/com/smunity/petition/domain/account/dto/UserRegisterResponseDto.java create mode 100644 src/main/java/com/smunity/petition/domain/account/exception/AccountsExceptionHandler.java create mode 100644 src/main/java/com/smunity/petition/domain/account/service/AccountsQueryService.java create mode 100644 src/main/java/com/smunity/petition/domain/account/service/AccountsService.java diff --git a/src/main/java/com/smunity/petition/domain/account/controller/AccountsController.java b/src/main/java/com/smunity/petition/domain/account/controller/AccountsController.java new file mode 100644 index 0000000..bcb80b6 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/controller/AccountsController.java @@ -0,0 +1,52 @@ +package com.smunity.petition.domain.account.controller; + +import com.smunity.petition.domain.account.annotation.AccountResolver; +import com.smunity.petition.domain.account.dto.UserRegisterRequestDto; +import com.smunity.petition.domain.account.dto.UserRegisterResponseDto; +import com.smunity.petition.domain.account.entity.User; +import com.smunity.petition.domain.account.jwt.dto.JwtDto; +import com.smunity.petition.domain.account.jwt.exception.SecurityCustomException; +import com.smunity.petition.domain.account.jwt.exception.TokenErrorCode; +import com.smunity.petition.domain.account.jwt.util.JwtUtil; +import com.smunity.petition.domain.account.service.AccountsService; +import com.smunity.petition.global.common.ApiResponse; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/v1/accounts") +@RestController +public class AccountsController { + + private final AccountsService accountsService; + + private final JwtUtil jwtUtil; + + @PostMapping("/register") + public ApiResponse register(@Valid @RequestBody UserRegisterRequestDto request) { + return ApiResponse.onSuccess(accountsService.register(request)); + } + + @GetMapping("/reissue") + public ApiResponse reissueToken(@RequestHeader("RefreshToken") String refreshToken) { + try { + jwtUtil.validateRefreshToken(refreshToken); + return ApiResponse.onSuccess( + jwtUtil.reissueToken(refreshToken) + ); + } catch (ExpiredJwtException eje) { + throw new SecurityCustomException(TokenErrorCode.TOKEN_EXPIRED, eje); + } catch (IllegalArgumentException iae) { + throw new SecurityCustomException(TokenErrorCode.INVALID_TOKEN, iae); + } + } + + @GetMapping("/test") + public ApiResponse register(@AccountResolver User user) { + return ApiResponse.onSuccess(user.getUserName()); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/dto/UserRegisterRequestDto.java b/src/main/java/com/smunity/petition/domain/account/dto/UserRegisterRequestDto.java new file mode 100644 index 0000000..56e9948 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/dto/UserRegisterRequestDto.java @@ -0,0 +1,48 @@ +package com.smunity.petition.domain.account.dto; + +import com.smunity.petition.domain.account.entity.User; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record UserRegisterRequestDto( + @NotBlank(message = "[ERROR] 이름 입력은 필수 입니다.") + String name, + @NotBlank(message = "[ERROR] 학번 입력은 필수 입니다.") + String username, + @NotBlank(message = "[ERROR] 학과 입력은 필수 입니다.") + String department, + + @NotBlank(message = "[ERROR] 이메일 입력은 필수입니다.") + @Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}$", message = "[ERROR] 이메일 형식에 맞지 않습니다.") + String email, + + @NotBlank(message = "[ERROR] 비밀번호 입력은 필수 입니다.") + @Size(min = 8, message = "[ERROR] 비밀번호는 최소 8자리 이이어야 합니다.") + @Pattern(regexp = "^(?=.*[a-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,64}$", message = "[ERROR] 비밀번호는 8자 이상, 64자 이하이며 특수문자 한 개를 포함해야 합니다.") + String password, + + @NotBlank(message = "[ERROR] 비밀번호 재확인 입력은 필수 입니다.") + String passwordCheck, + + @NotNull(message = "[ERROR] 현재 학년 입력은 필수 입니다.") + int year, + @NotNull(message = "[ERROR] 이수한 학기 입력은 필수 입니다.") + int semester +) { + + public User toEntity(String encodedPw) { + return User.builder() + .userName(username) + .password(encodedPw) + .email(email) + .name(name) + .year(null) + .department(null) + .currentYear(year) + .completedSemesters(semester) + .build(); + + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/dto/UserRegisterResponseDto.java b/src/main/java/com/smunity/petition/domain/account/dto/UserRegisterResponseDto.java new file mode 100644 index 0000000..c5698c6 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/dto/UserRegisterResponseDto.java @@ -0,0 +1,31 @@ +package com.smunity.petition.domain.account.dto; + +import com.smunity.petition.domain.account.entity.User; +import lombok.Builder; + +@Builder +public record UserRegisterResponseDto( + Long id, + String email, + String userName, + String name, + String year, + String department, + int currentYear, + int completedSemesters + +) { + + public static UserRegisterResponseDto from(User user) { + return UserRegisterResponseDto.builder() + .id(user.getId()) + .email(user.getEmail()) + .userName(user.getUserName()) + .name(user.getName()) + .year(user.getYear().getYear()) + .department(user.getDepartment().getName()) + .currentYear(user.getCurrentYear()) + .completedSemesters(user.getCompletedSemesters()) + .build(); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/exception/AccountsExceptionHandler.java b/src/main/java/com/smunity/petition/domain/account/exception/AccountsExceptionHandler.java new file mode 100644 index 0000000..c2ced54 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/exception/AccountsExceptionHandler.java @@ -0,0 +1,11 @@ +package com.smunity.petition.domain.account.exception; + + +import com.smunity.petition.global.common.code.BaseErrorCode; +import com.smunity.petition.global.common.exception.GeneralException; + +public class AccountsExceptionHandler extends GeneralException { + public AccountsExceptionHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/service/AccountsQueryService.java b/src/main/java/com/smunity/petition/domain/account/service/AccountsQueryService.java new file mode 100644 index 0000000..afd0666 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/service/AccountsQueryService.java @@ -0,0 +1,22 @@ +package com.smunity.petition.domain.account.service; + +import com.smunity.petition.domain.account.entity.User; +import com.smunity.petition.domain.account.exception.AccountsExceptionHandler; +import com.smunity.petition.domain.account.repository.user.UserRepository; +import com.smunity.petition.global.common.code.status.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class AccountsQueryService { + + private final UserRepository userRepository; + + public User findByUserName(String username) { + return userRepository.findByUserName(username) + .orElseThrow(() -> new AccountsExceptionHandler(ErrorCode.USER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/service/AccountsService.java b/src/main/java/com/smunity/petition/domain/account/service/AccountsService.java new file mode 100644 index 0000000..7d63b90 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/service/AccountsService.java @@ -0,0 +1,47 @@ +package com.smunity.petition.domain.account.service; + +import com.smunity.petition.domain.account.dto.UserRegisterRequestDto; +import com.smunity.petition.domain.account.dto.UserRegisterResponseDto; +import com.smunity.petition.domain.account.entity.Department; +import com.smunity.petition.domain.account.entity.User; +import com.smunity.petition.domain.account.entity.Year; +import com.smunity.petition.domain.account.exception.AccountsExceptionHandler; +import com.smunity.petition.domain.account.repository.DepartmentJpaRepository; +import com.smunity.petition.domain.account.repository.YearJpaRepository; +import com.smunity.petition.domain.account.repository.user.UserRepository; +import com.smunity.petition.global.common.code.status.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class AccountsService { + + private final UserRepository userRepository; + private final YearJpaRepository yearJpaRepository; + private final DepartmentJpaRepository departmentJpaRepository; + private final PasswordEncoder passwordEncoder; + + public UserRegisterResponseDto register(UserRegisterRequestDto request) { + + if (!request.password().equals(request.passwordCheck())) + throw new AccountsExceptionHandler(ErrorCode.PASSWORD_NOT_EQUAL); + + String encodedPw = passwordEncoder.encode(request.password()); + User newUser = request.toEntity(encodedPw); + + Year year = yearJpaRepository.findByYear(request.username().substring(0, 4)) + .orElseThrow(() -> new AccountsExceptionHandler( + ErrorCode.SAMNUL_ERROR)); + Department department = departmentJpaRepository.findByName(request.department()) + .orElseThrow(() -> new AccountsExceptionHandler(ErrorCode.SAMNUL_ERROR)); + + newUser.setYear(year); + newUser.setDepartment(department); + + return UserRegisterResponseDto.from(userRepository.save(newUser)); + } +} From d7b64d93a62c20c25c4c12441f7d1065b8e28e8f Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:08:11 +0900 Subject: [PATCH 15/17] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EC=9E=91=EC=84=B1=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/SecurityCustomException.java | 21 +++ .../account/jwt/exception/TokenErrorCode.java | 30 +++++ .../account/jwt/filter/CustomLoginFilter.java | 127 ++++++++++++++++++ .../jwt/userdetails/CustomUserDetails.java | 65 +++++++++ .../userdetails/CustomUserDetailsService.java | 35 +++++ .../account/jwt/util/HttpResponseUtil.java | 39 ++++++ 6 files changed, 317 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/exception/SecurityCustomException.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/exception/TokenErrorCode.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLoginFilter.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetails.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetailsService.java create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/util/HttpResponseUtil.java diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/exception/SecurityCustomException.java b/src/main/java/com/smunity/petition/domain/account/jwt/exception/SecurityCustomException.java new file mode 100644 index 0000000..79f3d6a --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/exception/SecurityCustomException.java @@ -0,0 +1,21 @@ +package com.smunity.petition.domain.account.jwt.exception; + +import com.smunity.petition.global.common.code.BaseErrorCode; +import com.smunity.petition.global.common.exception.GeneralException; +import lombok.Getter; + +@Getter +public class SecurityCustomException extends GeneralException { + + private final Throwable cause; + + public SecurityCustomException(BaseErrorCode errorCode) { + super(errorCode); + this.cause = null; + } + + public SecurityCustomException(BaseErrorCode errorCode, Throwable cause) { + super(errorCode); + this.cause = cause; + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/exception/TokenErrorCode.java b/src/main/java/com/smunity/petition/domain/account/jwt/exception/TokenErrorCode.java new file mode 100644 index 0000000..e8374a9 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/exception/TokenErrorCode.java @@ -0,0 +1,30 @@ +package com.smunity.petition.domain.account.jwt.exception; + +import com.smunity.petition.global.common.ApiResponse; +import com.smunity.petition.global.common.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum TokenErrorCode implements BaseErrorCode { + + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "SEC4001", "잘못된 형식의 토큰입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "SEC4010", "인증이 필요합니다."), + TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "SEC4011", "토큰이 만료되었습니다."), + TOKEN_SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "SEC4012", "토큰이 위조되었거나 손상되었습니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "SEC4030", "권한이 없습니다."), + TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "SEC4041", "토큰이 존재하지 않습니다."), + INTERNAL_SECURITY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SEC5000", "인증 처리 중 서버 에러가 발생했습니다."), + INTERNAL_TOKEN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SEC5001", "토큰 처리 중 서버 에러가 발생했습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ApiResponse getErrorResponse() { + return ApiResponse.onFailure(code, message); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLoginFilter.java b/src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLoginFilter.java new file mode 100644 index 0000000..cf853be --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLoginFilter.java @@ -0,0 +1,127 @@ +package com.smunity.petition.domain.account.jwt.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.smunity.petition.domain.account.jwt.dto.JwtDto; +import com.smunity.petition.domain.account.jwt.userdetails.CustomUserDetails; +import com.smunity.petition.domain.account.jwt.util.HttpResponseUtil; +import com.smunity.petition.domain.account.jwt.util.JwtUtil; +import com.smunity.petition.global.common.ApiResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + @Override + public Authentication attemptAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response + ) throws AuthenticationException { + log.info("[*] Login Filter"); + + Map requestBody; + try { + requestBody = getBody(request); + } catch (IOException e) { + throw new AuthenticationServiceException("Error of request body."); + } + + log.info("[*] Request Body : " + requestBody); + + String username = (String) requestBody.get("username"); + String password = (String) requestBody.get("password"); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, + null); + + return authenticationManager.authenticate(authToken); + } + + @Override + protected void successfulAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain chain, + @NonNull Authentication authentication) throws IOException { + log.info("[*] Login Success"); + + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + + log.info("[*] Login with " + customUserDetails.getUsername()); + + JwtDto jwtDto = new JwtDto( + jwtUtil.createJwtAccessToken(customUserDetails), + jwtUtil.createJwtRefreshToken(customUserDetails) + ); + + HttpResponseUtil.setSuccessResponse(response, HttpStatus.CREATED, jwtDto); + } + + @Override + protected void unsuccessfulAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull AuthenticationException failed) throws IOException { + + logger.info("[*] Login Fail"); + + String errorMessage; + if (failed instanceof BadCredentialsException) { + errorMessage = "Bad credentials"; + } else if (failed instanceof LockedException) { + errorMessage = "Account is locked"; + } else if (failed instanceof DisabledException) { + errorMessage = "Account is disabled"; + } else if (failed instanceof UsernameNotFoundException) { + errorMessage = "Account not found"; + } else if (failed instanceof AuthenticationServiceException) { + errorMessage = "Error occurred while parsing request body"; + } else { + errorMessage = "Authentication failed"; + } + HttpResponseUtil.setErrorResponse( + response, HttpStatus.UNAUTHORIZED, + ApiResponse.onFailure( + HttpStatus.BAD_REQUEST.name(), + errorMessage, + null + ) + ); + } + + private Map getBody(HttpServletRequest request) throws IOException { + + StringBuilder stringBuilder = new StringBuilder(); + String line; + + try (BufferedReader bufferedReader = request.getReader()) { + while ((line = bufferedReader.readLine()) != null) { + stringBuilder.append(line); + } + } + + String requestBody = stringBuilder.toString(); + ObjectMapper objectMapper = new ObjectMapper(); + + return objectMapper.readValue(requestBody, Map.class); + } +} + diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetails.java b/src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetails.java new file mode 100644 index 0000000..0d50de5 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetails.java @@ -0,0 +1,65 @@ +package com.smunity.petition.domain.account.jwt.userdetails; + +import com.smunity.petition.domain.account.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + + private final String username; + private final String password; + private final Boolean isStaff; + + public CustomUserDetails(User user) { + username = user.getUserName(); + password = user.getPassword(); + isStaff = user.getIsStaff(); + } + + public CustomUserDetails(String username, String password, Boolean isStaff) { + this.username = username; + this.password = password; + this.isStaff = isStaff; + } + + public Boolean getStaff() { + return isStaff; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetailsService.java b/src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetailsService.java new file mode 100644 index 0000000..2a3167f --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/userdetails/CustomUserDetailsService.java @@ -0,0 +1,35 @@ +package com.smunity.petition.domain.account.jwt.userdetails; + +import com.smunity.petition.domain.account.entity.User; +import com.smunity.petition.domain.account.exception.AccountsExceptionHandler; +import com.smunity.petition.domain.account.repository.user.UserRepository; +import com.smunity.petition.global.common.code.status.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUserName(username) + .orElseThrow(() -> new AccountsExceptionHandler(ErrorCode.USER_NOT_FOUND)); + + log.info("[*] User found : " + user.getUserName()); + + return new CustomUserDetails(user); + } + + public User userDetailsToUser(UserDetails userDetails) { + return userRepository.findByUserName(userDetails.getUsername()) + .orElseThrow(() -> new AccountsExceptionHandler(ErrorCode.USER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/util/HttpResponseUtil.java b/src/main/java/com/smunity/petition/domain/account/jwt/util/HttpResponseUtil.java new file mode 100644 index 0000000..9421469 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/util/HttpResponseUtil.java @@ -0,0 +1,39 @@ +package com.smunity.petition.domain.account.jwt.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.smunity.petition.global.common.ApiResponse; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.io.IOException; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class HttpResponseUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void setSuccessResponse(HttpServletResponse response, HttpStatus httpStatus, Object body) throws + IOException { + log.info("[*] Success Response"); + String responseBody = objectMapper.writeValueAsString(ApiResponse.onSuccess(body)); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(httpStatus.value()); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(responseBody); + } + + public static void setErrorResponse(HttpServletResponse response, HttpStatus httpStatus, Object body) throws + IOException { + log.info("[*] Failure Response"); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(httpStatus.value()); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getOutputStream(), body); + } + +} From 88157524989ce193a7fce99c066fb74106ab86ec Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:08:20 +0900 Subject: [PATCH 16/17] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwt/filter/CustomLogoutHandler.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLogoutHandler.java diff --git a/src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLogoutHandler.java b/src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLogoutHandler.java new file mode 100644 index 0000000..7fa408d --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/jwt/filter/CustomLogoutHandler.java @@ -0,0 +1,47 @@ +package com.smunity.petition.domain.account.jwt.filter; + +import com.smunity.petition.domain.account.jwt.exception.SecurityCustomException; +import com.smunity.petition.domain.account.jwt.exception.TokenErrorCode; +import com.smunity.petition.domain.account.jwt.util.JwtUtil; +import com.smunity.petition.domain.account.jwt.util.RedisUtil; +import io.jsonwebtoken.ExpiredJwtException; +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.web.authentication.logout.LogoutHandler; + +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +@Slf4j +public class CustomLogoutHandler implements LogoutHandler { + + private final RedisUtil redisUtil; + private final JwtUtil jwtUtil; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + try { + log.info("[*] Logout Filter"); + + String accessToken = jwtUtil.resolveAccessToken(request); + + redisUtil.save( + accessToken, + "logout", + jwtUtil.getExpTime(accessToken), + TimeUnit.MILLISECONDS + ); + + redisUtil.delete( + jwtUtil.getUsername(accessToken) + ); + + } catch (ExpiredJwtException e) { + log.warn("[*] case : accessToken expired"); + throw new SecurityCustomException(TokenErrorCode.TOKEN_EXPIRED); + } + } +} From 2fecb9478567130960b3305671305fc820b8981c Mon Sep 17 00:00:00 2001 From: hyunmin0317 Date: Wed, 28 Feb 2024 21:08:38 +0900 Subject: [PATCH 17/17] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=9D=B8=EC=A6=9D=20API=20=EC=9E=91=EC=84=B1=20(#5?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../annotation/AccountArgumentResolver.java | 38 +++++++++++++++++++ .../account/annotation/AccountResolver.java | 14 +++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/com/smunity/petition/domain/account/annotation/AccountArgumentResolver.java create mode 100644 src/main/java/com/smunity/petition/domain/account/annotation/AccountResolver.java diff --git a/src/main/java/com/smunity/petition/domain/account/annotation/AccountArgumentResolver.java b/src/main/java/com/smunity/petition/domain/account/annotation/AccountArgumentResolver.java new file mode 100644 index 0000000..3d70f97 --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/annotation/AccountArgumentResolver.java @@ -0,0 +1,38 @@ +package com.smunity.petition.domain.account.annotation; + +import com.smunity.petition.domain.account.entity.User; +import com.smunity.petition.domain.account.jwt.userdetails.CustomUserDetails; +import com.smunity.petition.domain.account.service.AccountsQueryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +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; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional +public class AccountArgumentResolver implements HandlerMethodArgumentResolver { + + private final AccountsQueryService accountsQueryService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasParameterAnnotation = parameter.hasParameterAnnotation(AccountResolver.class); + boolean isOrganizationParameterType = parameter.getParameterType().isAssignableFrom(User.class); + return hasParameterAnnotation && isOrganizationParameterType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + Object userDetails = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return accountsQueryService.findByUserName(((CustomUserDetails) userDetails).getUsername()); + } +} diff --git a/src/main/java/com/smunity/petition/domain/account/annotation/AccountResolver.java b/src/main/java/com/smunity/petition/domain/account/annotation/AccountResolver.java new file mode 100644 index 0000000..bcc42bd --- /dev/null +++ b/src/main/java/com/smunity/petition/domain/account/annotation/AccountResolver.java @@ -0,0 +1,14 @@ +package com.smunity.petition.domain.account.annotation; + +import io.swagger.v3.oas.annotations.Parameter; + +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) +@Parameter(hidden = true) +public @interface AccountResolver { +}