From aa35215d15c984d46770f3aa07713ad6ec72d58e Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 11:15:47 +0900 Subject: [PATCH 01/15] refactor(todo-service): code refactoring --- .../org/example/expert/domain/todo/service/TodoService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 922991ce7..89561fd12 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -19,7 +19,7 @@ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional public class TodoService { private final TodoRepository todoRepository; From ac071affacaea518d0efba368f00134bcbf59ed4 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 12:14:04 +0900 Subject: [PATCH 02/15] =?UTF-8?q?(lv1-2)refactor:=20nickname=20field=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert/config/AuthUserArgumentResolver.java | 3 ++- .../java/org/example/expert/config/JwtFilter.java | 4 ++++ .../java/org/example/expert/config/JwtUtil.java | 3 ++- .../domain/auth/dto/request/SignupRequest.java | 2 ++ .../expert/domain/auth/service/AuthService.java | 7 ++++--- .../domain/comment/service/CommentService.java | 4 ++-- .../example/expert/domain/common/dto/AuthUser.java | 4 +++- .../domain/manager/service/ManagerService.java | 4 ++-- .../expert/domain/todo/service/TodoService.java | 6 +++--- .../domain/user/dto/response/UserResponse.java | 4 +++- .../example/expert/domain/user/entity/User.java | 14 +++++++++++--- .../expert/domain/user/service/UserService.java | 14 +++++++++++++- 12 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java index db00211de..c1f5e4142 100644 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java @@ -40,7 +40,8 @@ public Object resolveArgument( Long userId = (Long) request.getAttribute("userId"); String email = (String) request.getAttribute("email"); UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); + String nickname = (String) request.getAttribute("nickname"); - return new AuthUser(userId, email, userRole); + return new AuthUser(userId, email, userRole, nickname); } } diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java index 03908abe1..ff845f9b6 100644 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ b/src/main/java/org/example/expert/config/JwtFilter.java @@ -56,10 +56,14 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class)); + String nickname = claims.get("nickname", String.class); + httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject())); httpRequest.setAttribute("email", claims.get("email")); httpRequest.setAttribute("userRole", claims.get("userRole")); + httpRequest.setAttribute("nickname", nickname); + if (url.startsWith("/admin")) { // 관리자 권한이 없는 경우 403을 반환합니다. diff --git a/src/main/java/org/example/expert/config/JwtUtil.java b/src/main/java/org/example/expert/config/JwtUtil.java index 07e0a2c7c..230bec1b8 100644 --- a/src/main/java/org/example/expert/config/JwtUtil.java +++ b/src/main/java/org/example/expert/config/JwtUtil.java @@ -34,7 +34,7 @@ public void init() { key = Keys.hmacShaKeyFor(bytes); } - public String createToken(Long userId, String email, UserRole userRole) { + public String createToken(Long userId, String email, UserRole userRole, String nickname) { Date date = new Date(); return BEARER_PREFIX + @@ -42,6 +42,7 @@ public String createToken(Long userId, String email, UserRole userRole) { .setSubject(String.valueOf(userId)) .claim("email", email) .claim("userRole", userRole) + .claim("nickname", nickname) .setExpiration(new Date(date.getTime() + TOKEN_TIME)) .setIssuedAt(date) // 발급일 .signWith(key, signatureAlgorithm) // 암호화 알고리즘 diff --git a/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java b/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java index cdb103690..084e64b34 100644 --- a/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java +++ b/src/main/java/org/example/expert/domain/auth/dto/request/SignupRequest.java @@ -17,4 +17,6 @@ public class SignupRequest { private String password; @NotBlank private String userRole; + @NotBlank + private String nickname; } diff --git a/src/main/java/org/example/expert/domain/auth/service/AuthService.java b/src/main/java/org/example/expert/domain/auth/service/AuthService.java index a662239dc..eff83a2ad 100644 --- a/src/main/java/org/example/expert/domain/auth/service/AuthService.java +++ b/src/main/java/org/example/expert/domain/auth/service/AuthService.java @@ -38,11 +38,12 @@ public SignupResponse signup(SignupRequest signupRequest) { User newUser = new User( signupRequest.getEmail(), encodedPassword, - userRole + userRole, + signupRequest.getNickname() ); User savedUser = userRepository.save(newUser); - String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole); + String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole, savedUser.getNickname()); return new SignupResponse(bearerToken); } @@ -56,7 +57,7 @@ public SigninResponse signin(SigninRequest signinRequest) { throw new AuthException("잘못된 비밀번호입니다."); } - String bearerToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole()); + String bearerToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole(), user.getNickname()); return new SigninResponse(bearerToken); } diff --git a/src/main/java/org/example/expert/domain/comment/service/CommentService.java b/src/main/java/org/example/expert/domain/comment/service/CommentService.java index 37f857491..3da98ba15 100644 --- a/src/main/java/org/example/expert/domain/comment/service/CommentService.java +++ b/src/main/java/org/example/expert/domain/comment/service/CommentService.java @@ -43,7 +43,7 @@ public CommentSaveResponse saveComment(AuthUser authUser, long todoId, CommentSa return new CommentSaveResponse( savedComment.getId(), savedComment.getContents(), - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) ); } @@ -56,7 +56,7 @@ public List getComments(long todoId) { CommentResponse dto = new CommentResponse( comment.getId(), comment.getContents(), - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) ); dtoList.add(dto); } diff --git a/src/main/java/org/example/expert/domain/common/dto/AuthUser.java b/src/main/java/org/example/expert/domain/common/dto/AuthUser.java index 7f4bc52e1..af2ab22a3 100644 --- a/src/main/java/org/example/expert/domain/common/dto/AuthUser.java +++ b/src/main/java/org/example/expert/domain/common/dto/AuthUser.java @@ -9,10 +9,12 @@ public class AuthUser { private final Long id; private final String email; private final UserRole userRole; + private final String nickname; - public AuthUser(Long id, String email, UserRole userRole) { + public AuthUser(Long id, String email, UserRole userRole, String nickname) { this.id = id; this.email = email; this.userRole = userRole; + this.nickname = nickname; } } diff --git a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java index 9e14df0f1..6f3fdbb3b 100644 --- a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java +++ b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java @@ -52,7 +52,7 @@ public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSa return new ManagerSaveResponse( savedManagerUser.getId(), - new UserResponse(managerUser.getId(), managerUser.getEmail()) + new UserResponse(managerUser.getId(), managerUser.getEmail(), managerUser.getNickname()) ); } @@ -67,7 +67,7 @@ public List getManagers(long todoId) { User user = manager.getUser(); dtoList.add(new ManagerResponse( manager.getId(), - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) )); } return dtoList; diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 89561fd12..0b2bdfd02 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -43,7 +43,7 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ savedTodo.getTitle(), savedTodo.getContents(), weather, - new UserResponse(user.getId(), user.getEmail()) + new UserResponse(user.getId(), user.getEmail(), user.getNickname()) ); } @@ -57,7 +57,7 @@ public Page getTodos(int page, int size) { todo.getTitle(), todo.getContents(), todo.getWeather(), - new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), + new UserResponse(todo.getUser().getId(), todo.getUser().getEmail(), todo.getUser().getNickname()), todo.getCreatedAt(), todo.getModifiedAt() )); @@ -74,7 +74,7 @@ public TodoResponse getTodo(long todoId) { todo.getTitle(), todo.getContents(), todo.getWeather(), - new UserResponse(user.getId(), user.getEmail()), + new UserResponse(user.getId(), user.getEmail(), user.getNickname()), todo.getCreatedAt(), todo.getModifiedAt() ); diff --git a/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java b/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java index 23794a3ca..91f3240aa 100644 --- a/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java +++ b/src/main/java/org/example/expert/domain/user/dto/response/UserResponse.java @@ -7,9 +7,11 @@ public class UserResponse { private final Long id; private final String email; + private final String nickname; - public UserResponse(Long id, String email) { + public UserResponse(Long id, String email, String nickname) { this.id = id; this.email = email; + this.nickname = nickname; } } diff --git a/src/main/java/org/example/expert/domain/user/entity/User.java b/src/main/java/org/example/expert/domain/user/entity/User.java index 30a0cc54f..87755e30b 100644 --- a/src/main/java/org/example/expert/domain/user/entity/User.java +++ b/src/main/java/org/example/expert/domain/user/entity/User.java @@ -18,23 +18,27 @@ public class User extends Timestamped { @Column(unique = true) private String email; private String password; + @Column + private String nickname; @Enumerated(EnumType.STRING) private UserRole userRole; - public User(String email, String password, UserRole userRole) { + public User(String email, String password, UserRole userRole, String nickname) { this.email = email; this.password = password; this.userRole = userRole; + this.nickname = nickname; } - private User(Long id, String email, UserRole userRole) { + private User(Long id, String email, UserRole userRole, String nickname) { this.id = id; this.email = email; this.userRole = userRole; + this.nickname = nickname; } public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole()); + return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole(), authUser.getNickname()); } public void changePassword(String password) { @@ -44,4 +48,8 @@ public void changePassword(String password) { public void updateRole(UserRole userRole) { this.userRole = userRole; } + +// public void updateNickname(String nickname) { +// this.nickname = nickname; +// } } diff --git a/src/main/java/org/example/expert/domain/user/service/UserService.java b/src/main/java/org/example/expert/domain/user/service/UserService.java index 15baec417..2e94d0ac2 100644 --- a/src/main/java/org/example/expert/domain/user/service/UserService.java +++ b/src/main/java/org/example/expert/domain/user/service/UserService.java @@ -20,7 +20,7 @@ public class UserService { public UserResponse getUser(long userId) { User user = userRepository.findById(userId).orElseThrow(() -> new InvalidRequestException("User not found")); - return new UserResponse(user.getId(), user.getEmail()); + return new UserResponse(user.getId(), user.getEmail(), user.getNickname()); } @Transactional @@ -48,4 +48,16 @@ private static void validateNewPassword(UserChangePasswordRequest userChangePass throw new InvalidRequestException("새 비밀번호는 8자 이상이어야 하고, 숫자와 대문자를 포함해야 합니다."); } } + +// @Transactional +// public void updateNickname(long userId, String newNickname) { +// if (newNickname == null || newNickname.trim().isEmpty()) { +// throw new InvalidRequestException("Nickname cannot be empty."); +// } +// +// User user = userRepository.findById(userId) +// .orElseThrow(() -> new InvalidRequestException("User not found")); +// +// user.updateNickname(newNickname); +// } } From c72e908728419e92c429130a34f6476250fc3867 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 12:45:10 +0900 Subject: [PATCH 03/15] =?UTF-8?q?(lv1-3)refactor=20(aop):=20method=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/expert/aop/AdminAccessLoggingAspect.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java index c90e8c792..319a08b70 100644 --- a/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java +++ b/src/main/java/org/example/expert/aop/AdminAccessLoggingAspect.java @@ -4,8 +4,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -18,8 +18,8 @@ public class AdminAccessLoggingAspect { private final HttpServletRequest request; - @After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))") - public void logAfterChangeUserRole(JoinPoint joinPoint) { + @Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))") + public void logBeforeChangeUserRole(JoinPoint joinPoint) { String userId = String.valueOf(request.getAttribute("userId")); String requestUrl = request.getRequestURI(); LocalDateTime requestTime = LocalDateTime.now(); From c62b7ca79e2ba5577ddc3cc919f95ea7bfe7206c Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 13:20:17 +0900 Subject: [PATCH 04/15] =?UTF-8?q?(lv1-5)refactor=20(todo-repository):=20qu?= =?UTF-8?q?ery=20method=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/todo/repository/TodoRepository.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java index a3e4e0749..fe65b9887 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; import java.util.Optional; public interface TodoRepository extends JpaRepository { @@ -18,4 +19,16 @@ public interface TodoRepository extends JpaRepository { "LEFT JOIN t.user " + "WHERE t.id = :todoId") Optional findByIdWithUser(@Param("todoId") Long todoId); + + @Query("SELECT t FROM Todo t " + + "WHERE (:weather IS NULL OR t.weather = :weather) " + + "AND (:startDate IS NULL OR t.modifiedAt >= :startDate) " + + "AND (:endDate IS NULL OR t.modifiedAt <= :endDate) " + + "ORDER BY t.modifiedAt DESC") + Page findByWeatherAndDateRange( + String weather, + LocalDateTime startDate, + LocalDateTime endDate, + Pageable pageable + ); } From f70616c60a77add0776235a8b9a08fbfc0c34367 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 13:21:27 +0900 Subject: [PATCH 05/15] =?UTF-8?q?(lv1-5)refactor=20(todo-service):=20getTo?= =?UTF-8?q?dos=20method=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/expert/domain/todo/service/TodoService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 0b2bdfd02..3c81eb607 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -16,6 +16,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; @Service @RequiredArgsConstructor @@ -47,10 +48,12 @@ public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequ ); } - public Page getTodos(int page, int size) { + public Page getTodos(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) { Pageable pageable = PageRequest.of(page - 1, size); - Page todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable); + Page todos = (weather == null && startDate == null && endDate == null) ? + todoRepository.findAllByOrderByModifiedAtDesc(pageable) : + todoRepository.findByWeatherAndDateRange(weather, startDate, endDate, pageable); return todos.map(todo -> new TodoResponse( todo.getId(), From 1950963de0d8d0546f535a28204b2fc49ae4fa0b Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 13:21:47 +0900 Subject: [PATCH 06/15] =?UTF-8?q?(lv1-5)refactor=20(todo-controller):=20ge?= =?UTF-8?q?tTodos=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert/domain/todo/controller/TodoController.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index eed1a1b46..eca8e545e 100644 --- a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java +++ b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java @@ -12,6 +12,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; + @RestController @RequiredArgsConstructor public class TodoController { @@ -29,9 +31,12 @@ public ResponseEntity saveTodo( @GetMapping("/todos") public ResponseEntity> getTodos( @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String weather, + @RequestParam(required = false) LocalDateTime startDate, + @RequestParam(required = false) LocalDateTime endDate ) { - return ResponseEntity.ok(todoService.getTodos(page, size)); + return ResponseEntity.ok(todoService.getTodos(page, size, weather, startDate, endDate)); } @GetMapping("/todos/{todoId}") From 0d3c6e382dadca95d633a87e91c458e20a1ab906 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 14:31:15 +0900 Subject: [PATCH 07/15] =?UTF-8?q?(lv1-4)refactor=20(todo-controller):=20te?= =?UTF-8?q?st=20code=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/todo/controller/TodoControllerTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java b/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java index 737193874..e2ee11af2 100644 --- a/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java +++ b/src/test/java/org/example/expert/domain/todo/controller/TodoControllerTest.java @@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpStatus; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; @@ -35,9 +34,9 @@ class TodoControllerTest { // given long todoId = 1L; String title = "title"; - AuthUser authUser = new AuthUser(1L, "email", UserRole.USER); + AuthUser authUser = new AuthUser(1L, "email", UserRole.USER, "nickname"); User user = User.fromAuthUser(authUser); - UserResponse userResponse = new UserResponse(user.getId(), user.getEmail()); + UserResponse userResponse = new UserResponse(user.getId(), user.getEmail(), user.getNickname()); TodoResponse response = new TodoResponse( todoId, title, @@ -69,9 +68,9 @@ class TodoControllerTest { // then mockMvc.perform(get("/todos/{todoId}", todoId)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value(HttpStatus.OK.name())) - .andExpect(jsonPath("$.code").value(HttpStatus.OK.value())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andExpect(jsonPath("$.code").value(400)) .andExpect(jsonPath("$.message").value("Todo not found")); } } From 6adbf884491f9023ae614546935eb0a8afac9fa3 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 15:08:37 +0900 Subject: [PATCH 08/15] =?UTF-8?q?(lv2-6)refactor=20(todo-entity):=20refact?= =?UTF-8?q?oring=20=EB=B0=8F=20method=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/expert/domain/todo/entity/Todo.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/expert/domain/todo/entity/Todo.java b/src/main/java/org/example/expert/domain/todo/entity/Todo.java index b4efcced1..b3c91ee2c 100644 --- a/src/main/java/org/example/expert/domain/todo/entity/Todo.java +++ b/src/main/java/org/example/expert/domain/todo/entity/Todo.java @@ -30,7 +30,7 @@ public class Todo extends Timestamped { @OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE) private List comments = new ArrayList<>(); - @OneToMany(mappedBy = "todo") + @OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST) private List managers = new ArrayList<>(); public Todo(String title, String contents, String weather, User user) { @@ -38,6 +38,11 @@ public Todo(String title, String contents, String weather, User user) { this.contents = contents; this.weather = weather; this.user = user; - this.managers.add(new Manager(user, this)); + addManager(user); + } + + public void addManager(User user) { + Manager manager = new Manager(user, this); + this.managers.add(manager); } } From 322bf58cdb5fa46a887b6483014be7223fbc0282 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 15:43:30 +0900 Subject: [PATCH 09/15] =?UTF-8?q?(lv2-7)refactor=20(comment-repository):?= =?UTF-8?q?=20query=20method=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert/domain/comment/repository/CommentRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java b/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java index 3c97b95dc..ecb21ce56 100644 --- a/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java +++ b/src/main/java/org/example/expert/domain/comment/repository/CommentRepository.java @@ -9,6 +9,6 @@ public interface CommentRepository extends JpaRepository { - @Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId") + @Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId") List findByTodoIdWithUser(@Param("todoId") Long todoId); } From 8f1e55f53eff9fdec2866b966a2bba6f81d686d3 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 20:30:35 +0900 Subject: [PATCH 10/15] =?UTF-8?q?(lv2-8)refactor=20:=20QueryDSL=20dependen?= =?UTF-8?q?cies=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index a7fd3e706..48c265d65 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,12 @@ dependencies { compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + + //QueryDSL + implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { From 5a7737407f1813aa4dbdfc7903c9952cccf4f283 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 20:32:21 +0900 Subject: [PATCH 11/15] =?UTF-8?q?(lv2-8)refactor(todo-repository)=20:=20Qu?= =?UTF-8?q?eryDSL=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/expert/domain/todo/repository/TodoRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java index fe65b9887..f5de1e9ee 100644 --- a/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepository.java @@ -10,7 +10,7 @@ import java.time.LocalDateTime; import java.util.Optional; -public interface TodoRepository extends JpaRepository { +public interface TodoRepository extends JpaRepository, TodoRepositoryCustom { @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC") Page findAllByOrderByModifiedAtDesc(Pageable pageable); From 4234883b1f2b53397d6de14001cb1b2f7aff44bc Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 20:33:17 +0900 Subject: [PATCH 12/15] =?UTF-8?q?(lv2-8)feat(todo-repository)=20:=20Interf?= =?UTF-8?q?ace=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/todo/repository/TodoRepositoryCustom.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustom.java diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustom.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustom.java new file mode 100644 index 000000000..f0a8b6fa9 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustom.java @@ -0,0 +1,9 @@ +package org.example.expert.domain.todo.repository; + +import org.example.expert.domain.todo.entity.Todo; + +import java.util.Optional; + +public interface TodoRepositoryCustom { + Optional findByIdWithUser(Long todoId); +} From 3e53223f981733e59fcf7553e1c36b53679a29b2 Mon Sep 17 00:00:00 2001 From: soung90 Date: Wed, 13 Nov 2024 20:34:09 +0900 Subject: [PATCH 13/15] =?UTF-8?q?(lv2-8)feat(todo-repository)=20:=20QueryD?= =?UTF-8?q?SL=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=98=EB=8A=94=20class=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/TodoRepositoryCustomImpl.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustomImpl.java diff --git a/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustomImpl.java b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustomImpl.java new file mode 100644 index 000000000..04055e758 --- /dev/null +++ b/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryCustomImpl.java @@ -0,0 +1,32 @@ +package org.example.expert.domain.todo.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.example.expert.domain.todo.entity.QTodo; +import org.example.expert.domain.todo.entity.Todo; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class TodoRepositoryCustomImpl implements TodoRepositoryCustom { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public Optional findByIdWithUser(Long todoId) { + JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); + QTodo qTodo = QTodo.todo; + + Todo todo = queryFactory.selectFrom(qTodo) + .leftJoin(qTodo.user).fetchJoin() + .where(qTodo.id.eq(todoId)) + .fetchOne(); + + return Optional.ofNullable(todo); + } +} \ No newline at end of file From 2f1a16a19d816ebac2bb48d13310567145797bae Mon Sep 17 00:00:00 2001 From: soung90 Date: Thu, 14 Nov 2024 12:03:23 +0900 Subject: [PATCH 14/15] =?UTF-8?q?refactor:=20Spring=20Security=20dependenc?= =?UTF-8?q?y=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 48c265d65..50f97ec4d 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + // bcrypt implementation 'at.favre.lib:bcrypt:0.10.2' From ab8a6fb35436b5dfc76f4b90d702228bbf888ae4 Mon Sep 17 00:00:00 2001 From: soung90 Date: Thu, 14 Nov 2024 19:51:02 +0900 Subject: [PATCH 15/15] =?UTF-8?q?(lv2-9)refactor:=20Spring=20Security=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/AuthUserArgumentResolver.java | 47 --------- .../example/expert/config/FilterConfig.java | 22 ----- .../org/example/expert/config/JwtFilter.java | 98 ------------------- .../expert/config/PasswordEncoder.java | 17 ---- .../expert/config/PersistenceConfig.java | 9 -- .../example/expert/config/SecurityConfig.java | 33 +++++++ .../org/example/expert/config/WebConfig.java | 19 ---- .../config/authdetails/AuthDetailsImpl.java | 50 ++++++++++ .../authdetails/AuthDetailsServiceImpl.java | 23 +++++ .../jwtfilter/JwtAuthenticationFilter.java | 47 +++++++++ .../expert/config/{ => jwtutil}/JwtUtil.java | 33 ++++++- .../domain/auth/service/AuthService.java | 4 +- .../comment/controller/CommentController.java | 6 +- .../comment/service/CommentService.java | 4 +- .../expert/domain/common/annotation/Auth.java | 11 --- .../expert/domain/common/dto/AuthUser.java | 20 ---- .../manager/controller/ManagerController.java | 8 +- .../manager/service/ManagerService.java | 6 +- .../todo/controller/TodoController.java | 6 +- .../domain/todo/service/TodoService.java | 5 +- .../user/controller/UserController.java | 8 +- .../expert/domain/user/entity/User.java | 13 ++- .../domain/user/service/UserService.java | 2 +- 23 files changed, 216 insertions(+), 275 deletions(-) delete mode 100644 src/main/java/org/example/expert/config/AuthUserArgumentResolver.java delete mode 100644 src/main/java/org/example/expert/config/FilterConfig.java delete mode 100644 src/main/java/org/example/expert/config/JwtFilter.java delete mode 100644 src/main/java/org/example/expert/config/PasswordEncoder.java delete mode 100644 src/main/java/org/example/expert/config/PersistenceConfig.java create mode 100644 src/main/java/org/example/expert/config/SecurityConfig.java delete mode 100644 src/main/java/org/example/expert/config/WebConfig.java create mode 100644 src/main/java/org/example/expert/config/authdetails/AuthDetailsImpl.java create mode 100644 src/main/java/org/example/expert/config/authdetails/AuthDetailsServiceImpl.java create mode 100644 src/main/java/org/example/expert/config/jwtfilter/JwtAuthenticationFilter.java rename src/main/java/org/example/expert/config/{ => jwtutil}/JwtUtil.java (66%) delete mode 100644 src/main/java/org/example/expert/domain/common/annotation/Auth.java delete mode 100644 src/main/java/org/example/expert/domain/common/dto/AuthUser.java diff --git a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java b/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java deleted file mode 100644 index c1f5e4142..000000000 --- a/src/main/java/org/example/expert/config/AuthUserArgumentResolver.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.example.expert.config; - -import jakarta.servlet.http.HttpServletRequest; -import org.example.expert.domain.auth.exception.AuthException; -import org.example.expert.domain.common.annotation.Auth; -import org.example.expert.domain.common.dto.AuthUser; -import org.example.expert.domain.user.enums.UserRole; -import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; -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; - -public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { - - @Override - public boolean supportsParameter(MethodParameter parameter) { - boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null; - boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class); - - // @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생 - if (hasAuthAnnotation != isAuthUserType) { - throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다."); - } - - return hasAuthAnnotation; - } - - @Override - public Object resolveArgument( - @Nullable MethodParameter parameter, - @Nullable ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - @Nullable WebDataBinderFactory binderFactory - ) { - HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); - - // JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴 - Long userId = (Long) request.getAttribute("userId"); - String email = (String) request.getAttribute("email"); - UserRole userRole = UserRole.of((String) request.getAttribute("userRole")); - String nickname = (String) request.getAttribute("nickname"); - - return new AuthUser(userId, email, userRole, nickname); - } -} diff --git a/src/main/java/org/example/expert/config/FilterConfig.java b/src/main/java/org/example/expert/config/FilterConfig.java deleted file mode 100644 index 34cb4088a..000000000 --- a/src/main/java/org/example/expert/config/FilterConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.example.expert.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@RequiredArgsConstructor -public class FilterConfig { - - private final JwtUtil jwtUtil; - - @Bean - public FilterRegistrationBean jwtFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new JwtFilter(jwtUtil)); - registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다. - - return registrationBean; - } -} diff --git a/src/main/java/org/example/expert/config/JwtFilter.java b/src/main/java/org/example/expert/config/JwtFilter.java deleted file mode 100644 index ff845f9b6..000000000 --- a/src/main/java/org/example/expert/config/JwtFilter.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.example.expert.config; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.example.expert.domain.user.enums.UserRole; - -import java.io.IOException; - -@Slf4j -@RequiredArgsConstructor -public class JwtFilter implements Filter { - - private final JwtUtil jwtUtil; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - Filter.super.init(filterConfig); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - HttpServletResponse httpResponse = (HttpServletResponse) response; - - String url = httpRequest.getRequestURI(); - - if (url.startsWith("/auth")) { - chain.doFilter(request, response); - return; - } - - String bearerJwt = httpRequest.getHeader("Authorization"); - - if (bearerJwt == null) { - // 토큰이 없는 경우 400을 반환합니다. - httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다."); - return; - } - - String jwt = jwtUtil.substringToken(bearerJwt); - - try { - // JWT 유효성 검사와 claims 추출 - Claims claims = jwtUtil.extractClaims(jwt); - if (claims == null) { - httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다."); - return; - } - - UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class)); - String nickname = claims.get("nickname", String.class); - - - httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject())); - httpRequest.setAttribute("email", claims.get("email")); - httpRequest.setAttribute("userRole", claims.get("userRole")); - httpRequest.setAttribute("nickname", nickname); - - - if (url.startsWith("/admin")) { - // 관리자 권한이 없는 경우 403을 반환합니다. - if (!UserRole.ADMIN.equals(userRole)) { - httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다."); - return; - } - chain.doFilter(request, response); - return; - } - - chain.doFilter(request, response); - } catch (SecurityException | MalformedJwtException e) { - log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e); - httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다."); - } catch (ExpiredJwtException e) { - log.error("Expired JWT token, 만료된 JWT token 입니다.", e); - httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다."); - } catch (UnsupportedJwtException e) { - log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e); - httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다."); - } catch (Exception e) { - log.error("Internal server error", e); - httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - - @Override - public void destroy() { - Filter.super.destroy(); - } -} diff --git a/src/main/java/org/example/expert/config/PasswordEncoder.java b/src/main/java/org/example/expert/config/PasswordEncoder.java deleted file mode 100644 index dadce8758..000000000 --- a/src/main/java/org/example/expert/config/PasswordEncoder.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.example.expert.config; - -import at.favre.lib.crypto.bcrypt.BCrypt; -import org.springframework.stereotype.Component; - -@Component -public class PasswordEncoder { - - public String encode(String rawPassword) { - return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray()); - } - - public boolean matches(String rawPassword, String encodedPassword) { - BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword); - return result.verified; - } -} diff --git a/src/main/java/org/example/expert/config/PersistenceConfig.java b/src/main/java/org/example/expert/config/PersistenceConfig.java deleted file mode 100644 index 8f6be3aa0..000000000 --- a/src/main/java/org/example/expert/config/PersistenceConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.example.expert.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; - -@Configuration -@EnableJpaAuditing -public class PersistenceConfig { -} diff --git a/src/main/java/org/example/expert/config/SecurityConfig.java b/src/main/java/org/example/expert/config/SecurityConfig.java new file mode 100644 index 000000000..6d7e300be --- /dev/null +++ b/src/main/java/org/example/expert/config/SecurityConfig.java @@ -0,0 +1,33 @@ +package org.example.expert.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/auth/signin","/admin/signup").permitAll() + .anyRequest().authenticated() + ) + + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/org/example/expert/config/WebConfig.java b/src/main/java/org/example/expert/config/WebConfig.java deleted file mode 100644 index adff06b82..000000000 --- a/src/main/java/org/example/expert/config/WebConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.example.expert.config; - -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; - -@Configuration -@RequiredArgsConstructor -public class WebConfig implements WebMvcConfigurer { - - // ArgumentResolver 등록 - @Override - public void addArgumentResolvers(List resolvers) { - resolvers.add(new AuthUserArgumentResolver()); - } -} diff --git a/src/main/java/org/example/expert/config/authdetails/AuthDetailsImpl.java b/src/main/java/org/example/expert/config/authdetails/AuthDetailsImpl.java new file mode 100644 index 000000000..0f55ecd08 --- /dev/null +++ b/src/main/java/org/example/expert/config/authdetails/AuthDetailsImpl.java @@ -0,0 +1,50 @@ +package org.example.expert.config.authdetails; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.example.expert.domain.user.entity.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +@Getter +public class AuthDetailsImpl implements UserDetails { + private final User user; + + public AuthDetailsImpl(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getUserRole().name())); + } + @Override + public String getPassword() { + return user.getPassword(); + } + @Override + public String getUsername() { + return user.getEmail(); + } + @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/org/example/expert/config/authdetails/AuthDetailsServiceImpl.java b/src/main/java/org/example/expert/config/authdetails/AuthDetailsServiceImpl.java new file mode 100644 index 000000000..566ceba08 --- /dev/null +++ b/src/main/java/org/example/expert/config/authdetails/AuthDetailsServiceImpl.java @@ -0,0 +1,23 @@ +package org.example.expert.config.authdetails; + +import lombok.RequiredArgsConstructor; +import org.example.expert.domain.user.entity.User; +import org.example.expert.domain.user.repository.UserRepository; +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 +public class AuthDetailsServiceImpl implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException(email)); + return new AuthDetailsImpl(user); + } +} diff --git a/src/main/java/org/example/expert/config/jwtfilter/JwtAuthenticationFilter.java b/src/main/java/org/example/expert/config/jwtfilter/JwtAuthenticationFilter.java new file mode 100644 index 000000000..837d977ea --- /dev/null +++ b/src/main/java/org/example/expert/config/jwtfilter/JwtAuthenticationFilter.java @@ -0,0 +1,47 @@ +package org.example.expert.config.jwtfilter; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.expert.config.authdetails.AuthDetailsServiceImpl; +import org.example.expert.config.jwtutil.JwtUtil; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final AuthDetailsServiceImpl authDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + String token = jwtUtil.resolveToken(request); + if (token != null && jwtUtil.validateToken(token)) { + Claims claims = jwtUtil.extractClaims(token); + String email = claims.get("email", String.class); + UserDetails userDetails = authDetailsService.loadUserByUsername(email); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + + } + +} diff --git a/src/main/java/org/example/expert/config/JwtUtil.java b/src/main/java/org/example/expert/config/jwtutil/JwtUtil.java similarity index 66% rename from src/main/java/org/example/expert/config/JwtUtil.java rename to src/main/java/org/example/expert/config/jwtutil/JwtUtil.java index 230bec1b8..395bcf939 100644 --- a/src/main/java/org/example/expert/config/JwtUtil.java +++ b/src/main/java/org/example/expert/config/jwtutil/JwtUtil.java @@ -1,10 +1,9 @@ -package org.example.expert.config; +package org.example.expert.config.jwtutil; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.example.expert.domain.common.exception.ServerException; import org.example.expert.domain.user.enums.UserRole; @@ -63,4 +62,30 @@ public Claims extractClaims(String token) { .parseClaimsJws(token) .getBody(); } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } + catch (ExpiredJwtException e) { + log.error("Expired JWT token.", e); + } catch (MalformedJwtException | UnsupportedJwtException | SecurityException e) { + log.error("Invalid JWT token.", e); + } catch (IllegalArgumentException e) { + log.error("JWT claims string is empty.", e); + } catch (Exception e) { + log.error("Internal error during JWT processing.", e); + } + return false; + } + + public String resolveToken(HttpServletRequest request) { + + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); // "Bearer " 제거 후 토큰 반환 + } + return null; + } } diff --git a/src/main/java/org/example/expert/domain/auth/service/AuthService.java b/src/main/java/org/example/expert/domain/auth/service/AuthService.java index eff83a2ad..2ba0c122d 100644 --- a/src/main/java/org/example/expert/domain/auth/service/AuthService.java +++ b/src/main/java/org/example/expert/domain/auth/service/AuthService.java @@ -1,8 +1,7 @@ package org.example.expert.domain.auth.service; import lombok.RequiredArgsConstructor; -import org.example.expert.config.JwtUtil; -import org.example.expert.config.PasswordEncoder; +import org.example.expert.config.jwtutil.JwtUtil; import org.example.expert.domain.auth.dto.request.SigninRequest; import org.example.expert.domain.auth.dto.request.SignupRequest; import org.example.expert.domain.auth.dto.response.SigninResponse; @@ -12,6 +11,7 @@ import org.example.expert.domain.user.entity.User; import org.example.expert.domain.user.enums.UserRole; import org.example.expert.domain.user.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/org/example/expert/domain/comment/controller/CommentController.java b/src/main/java/org/example/expert/domain/comment/controller/CommentController.java index 51264b12e..abf62f04b 100644 --- a/src/main/java/org/example/expert/domain/comment/controller/CommentController.java +++ b/src/main/java/org/example/expert/domain/comment/controller/CommentController.java @@ -2,13 +2,13 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.comment.dto.request.CommentSaveRequest; import org.example.expert.domain.comment.dto.response.CommentResponse; import org.example.expert.domain.comment.dto.response.CommentSaveResponse; import org.example.expert.domain.comment.service.CommentService; -import org.example.expert.domain.common.annotation.Auth; -import org.example.expert.domain.common.dto.AuthUser; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -21,7 +21,7 @@ public class CommentController { @PostMapping("/todos/{todoId}/comments") public ResponseEntity saveComment( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthDetailsImpl authUser, @PathVariable long todoId, @Valid @RequestBody CommentSaveRequest commentSaveRequest ) { diff --git a/src/main/java/org/example/expert/domain/comment/service/CommentService.java b/src/main/java/org/example/expert/domain/comment/service/CommentService.java index 3da98ba15..b7e12dd72 100644 --- a/src/main/java/org/example/expert/domain/comment/service/CommentService.java +++ b/src/main/java/org/example/expert/domain/comment/service/CommentService.java @@ -1,12 +1,12 @@ package org.example.expert.domain.comment.service; import lombok.RequiredArgsConstructor; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.comment.dto.request.CommentSaveRequest; import org.example.expert.domain.comment.dto.response.CommentResponse; import org.example.expert.domain.comment.dto.response.CommentSaveResponse; import org.example.expert.domain.comment.entity.Comment; import org.example.expert.domain.comment.repository.CommentRepository; -import org.example.expert.domain.common.dto.AuthUser; import org.example.expert.domain.common.exception.InvalidRequestException; import org.example.expert.domain.todo.entity.Todo; import org.example.expert.domain.todo.repository.TodoRepository; @@ -27,7 +27,7 @@ public class CommentService { private final CommentRepository commentRepository; @Transactional - public CommentSaveResponse saveComment(AuthUser authUser, long todoId, CommentSaveRequest commentSaveRequest) { + public CommentSaveResponse saveComment(AuthDetailsImpl authUser, long todoId, CommentSaveRequest commentSaveRequest) { User user = User.fromAuthUser(authUser); Todo todo = todoRepository.findById(todoId).orElseThrow(() -> new InvalidRequestException("Todo not found")); diff --git a/src/main/java/org/example/expert/domain/common/annotation/Auth.java b/src/main/java/org/example/expert/domain/common/annotation/Auth.java deleted file mode 100644 index 770061855..000000000 --- a/src/main/java/org/example/expert/domain/common/annotation/Auth.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.example.expert.domain.common.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.PARAMETER) -@Retention(RetentionPolicy.RUNTIME) -public @interface Auth { -} diff --git a/src/main/java/org/example/expert/domain/common/dto/AuthUser.java b/src/main/java/org/example/expert/domain/common/dto/AuthUser.java deleted file mode 100644 index af2ab22a3..000000000 --- a/src/main/java/org/example/expert/domain/common/dto/AuthUser.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.example.expert.domain.common.dto; - -import lombok.Getter; -import org.example.expert.domain.user.enums.UserRole; - -@Getter -public class AuthUser { - - private final Long id; - private final String email; - private final UserRole userRole; - private final String nickname; - - public AuthUser(Long id, String email, UserRole userRole, String nickname) { - this.id = id; - this.email = email; - this.userRole = userRole; - this.nickname = nickname; - } -} diff --git a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java index 327b6452b..1cc260646 100644 --- a/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java +++ b/src/main/java/org/example/expert/domain/manager/controller/ManagerController.java @@ -2,13 +2,13 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.annotation.Auth; -import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.manager.dto.request.ManagerSaveRequest; import org.example.expert.domain.manager.dto.response.ManagerResponse; import org.example.expert.domain.manager.dto.response.ManagerSaveResponse; import org.example.expert.domain.manager.service.ManagerService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -21,7 +21,7 @@ public class ManagerController { @PostMapping("/todos/{todoId}/managers") public ResponseEntity saveManager( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthDetailsImpl authUser, @PathVariable long todoId, @Valid @RequestBody ManagerSaveRequest managerSaveRequest ) { @@ -35,7 +35,7 @@ public ResponseEntity> getMembers(@PathVariable long todoI @DeleteMapping("/todos/{todoId}/managers/{managerId}") public void deleteManager( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthDetailsImpl authUser, @PathVariable long todoId, @PathVariable long managerId ) { diff --git a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java index 6f3fdbb3b..4126d05c1 100644 --- a/src/main/java/org/example/expert/domain/manager/service/ManagerService.java +++ b/src/main/java/org/example/expert/domain/manager/service/ManagerService.java @@ -1,7 +1,7 @@ package org.example.expert.domain.manager.service; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.common.exception.InvalidRequestException; import org.example.expert.domain.manager.dto.request.ManagerSaveRequest; import org.example.expert.domain.manager.dto.response.ManagerResponse; @@ -30,7 +30,7 @@ public class ManagerService { private final TodoRepository todoRepository; @Transactional - public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) { + public ManagerSaveResponse saveManager(AuthDetailsImpl authUser, long todoId, ManagerSaveRequest managerSaveRequest) { // 일정을 만든 유저 User user = User.fromAuthUser(authUser); Todo todo = todoRepository.findById(todoId) @@ -74,7 +74,7 @@ public List getManagers(long todoId) { } @Transactional - public void deleteManager(AuthUser authUser, long todoId, long managerId) { + public void deleteManager(AuthDetailsImpl authUser, long todoId, long managerId) { User user = User.fromAuthUser(authUser); Todo todo = todoRepository.findById(todoId) diff --git a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java index eca8e545e..20bacb19f 100644 --- a/src/main/java/org/example/expert/domain/todo/controller/TodoController.java +++ b/src/main/java/org/example/expert/domain/todo/controller/TodoController.java @@ -2,14 +2,14 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.annotation.Auth; -import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; import org.example.expert.domain.todo.dto.response.TodoResponse; import org.example.expert.domain.todo.dto.response.TodoSaveResponse; import org.example.expert.domain.todo.service.TodoService; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; @@ -22,7 +22,7 @@ public class TodoController { @PostMapping("/todos") public ResponseEntity saveTodo( - @Auth AuthUser authUser, + @AuthenticationPrincipal AuthDetailsImpl authUser, @Valid @RequestBody TodoSaveRequest todoSaveRequest ) { return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest)); diff --git a/src/main/java/org/example/expert/domain/todo/service/TodoService.java b/src/main/java/org/example/expert/domain/todo/service/TodoService.java index 3c81eb607..87e4d7b7d 100644 --- a/src/main/java/org/example/expert/domain/todo/service/TodoService.java +++ b/src/main/java/org/example/expert/domain/todo/service/TodoService.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.example.expert.client.WeatherClient; -import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.common.exception.InvalidRequestException; import org.example.expert.domain.todo.dto.request.TodoSaveRequest; import org.example.expert.domain.todo.dto.response.TodoResponse; @@ -16,6 +16,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import java.time.LocalDateTime; @Service @@ -26,7 +27,7 @@ public class TodoService { private final TodoRepository todoRepository; private final WeatherClient weatherClient; - public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) { + public TodoSaveResponse saveTodo(AuthDetailsImpl authUser, TodoSaveRequest todoSaveRequest) { User user = User.fromAuthUser(authUser); String weather = weatherClient.getTodayWeather(); diff --git a/src/main/java/org/example/expert/domain/user/controller/UserController.java b/src/main/java/org/example/expert/domain/user/controller/UserController.java index bb1ef7a95..f54a797b5 100644 --- a/src/main/java/org/example/expert/domain/user/controller/UserController.java +++ b/src/main/java/org/example/expert/domain/user/controller/UserController.java @@ -1,12 +1,12 @@ package org.example.expert.domain.user.controller; import lombok.RequiredArgsConstructor; -import org.example.expert.domain.common.annotation.Auth; -import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.user.dto.request.UserChangePasswordRequest; import org.example.expert.domain.user.dto.response.UserResponse; import org.example.expert.domain.user.service.UserService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -21,7 +21,7 @@ public ResponseEntity getUser(@PathVariable long userId) { } @PutMapping("/users") - public void changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) { - userService.changePassword(authUser.getId(), userChangePasswordRequest); + public void changePassword(@AuthenticationPrincipal AuthDetailsImpl authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) { + userService.changePassword(authUser.getUser().getId(), userChangePasswordRequest); } } diff --git a/src/main/java/org/example/expert/domain/user/entity/User.java b/src/main/java/org/example/expert/domain/user/entity/User.java index 87755e30b..b5b927e90 100644 --- a/src/main/java/org/example/expert/domain/user/entity/User.java +++ b/src/main/java/org/example/expert/domain/user/entity/User.java @@ -3,7 +3,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; -import org.example.expert.domain.common.dto.AuthUser; +import org.example.expert.config.authdetails.AuthDetailsImpl; import org.example.expert.domain.common.entity.Timestamped; import org.example.expert.domain.user.enums.UserRole; @@ -13,7 +13,8 @@ @Table(name = "users") public class User extends Timestamped { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) private String email; @@ -37,8 +38,12 @@ private User(Long id, String email, UserRole userRole, String nickname) { this.nickname = nickname; } - public static User fromAuthUser(AuthUser authUser) { - return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole(), authUser.getNickname()); + public static User fromAuthUser(AuthDetailsImpl authUser) { + return new User( + authUser.getUser().getId(), + authUser.getUser().getEmail(), + authUser.getUser().getUserRole(), + authUser.getUser().getNickname()); } public void changePassword(String password) { diff --git a/src/main/java/org/example/expert/domain/user/service/UserService.java b/src/main/java/org/example/expert/domain/user/service/UserService.java index 2e94d0ac2..66c85c1e0 100644 --- a/src/main/java/org/example/expert/domain/user/service/UserService.java +++ b/src/main/java/org/example/expert/domain/user/service/UserService.java @@ -1,12 +1,12 @@ package org.example.expert.domain.user.service; import lombok.RequiredArgsConstructor; -import org.example.expert.config.PasswordEncoder; import org.example.expert.domain.common.exception.InvalidRequestException; import org.example.expert.domain.user.dto.request.UserChangePasswordRequest; import org.example.expert.domain.user.dto.response.UserResponse; import org.example.expert.domain.user.entity.User; import org.example.expert.domain.user.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;