From 3d29e2f7576b63284d562a25dd1de1715f082cb7 Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Sat, 28 Jun 2025 01:11:31 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../controller/AdminController.java | 6 +- .../controller/AdminControllerDocs.java | 6 +- .../controller/AuthController.java | 21 +---- .../controller/AuthControllerDocs.java | 56 ++++++----- .../controller/FollowController.java | 10 +- .../controller/FollowControllerDocs.java | 10 +- .../controller/LikeController.java | 4 +- .../controller/LikeControllerDocs.java | 4 +- .../controller/PostController.java | 6 +- .../controller/PostControllerDocs.java | 8 +- .../controller/keyword/KeywordController.java | 4 +- .../keyword/KeywordControllerDocs.java | 4 +- .../object/constants/SocialPlatform.java | 6 ++ ...UserDetails.java => CustomOAuth2User.java} | 34 ++++--- .../object/dto/SignInRequest.java | 17 +++- .../object/dto/SignUpRequest.java | 37 -------- .../object/postgres/Member.java | 10 +- .../repository/postgres/MemberRepository.java | 5 +- .../service/CustomOAuth2UserService.java | 28 ++++++ .../service/CustomUserDetailsService.java | 31 ------- .../service/MemberService.java | 63 +++++++------ .../dailysnapbackend/util/JwtUtil.java | 53 +++++++---- .../util/config/SecurityConfig.java | 34 +++---- .../util/filter/CustomLogoutHandler.java | 4 +- .../util/filter/LoginFilter.java | 92 ------------------- .../filter/TokenAuthenticationFilter.java | 4 +- 27 files changed, 221 insertions(+), 339 deletions(-) create mode 100644 src/main/java/onepiece/dailysnapbackend/object/constants/SocialPlatform.java rename src/main/java/onepiece/dailysnapbackend/object/dto/{CustomUserDetails.java => CustomOAuth2User.java} (71%) delete mode 100644 src/main/java/onepiece/dailysnapbackend/object/dto/SignUpRequest.java create mode 100644 src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java delete mode 100644 src/main/java/onepiece/dailysnapbackend/service/CustomUserDetailsService.java delete mode 100644 src/main/java/onepiece/dailysnapbackend/util/filter/LoginFilter.java diff --git a/build.gradle b/build.gradle index cfd90e2..c91cbc1 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,9 @@ dependencies { implementation 'org.redisson:redisson-spring-boot-starter:3.36.0' implementation 'org.sejda.imageio:webp-imageio:0.1.6' + + // oauth + implementation 'org.springframework.security:spring-security-oauth2-client:6.3.1' } tasks.named('test') { diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AdminController.java b/src/main/java/onepiece/dailysnapbackend/controller/AdminController.java index c942cd8..1de8dbf 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AdminController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AdminController.java @@ -4,7 +4,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.KeywordRequest; import onepiece.dailysnapbackend.service.keyword.AdminKeywordService; import onepiece.dailysnapbackend.util.log.LogMonitoringInvocation; @@ -33,7 +33,7 @@ public class AdminController implements AdminControllerDocs { @PostMapping @LogMonitoringInvocation public ResponseEntity addKeyword( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @Valid @RequestBody KeywordRequest request) { adminKeywordService.addKeyword(request); log.info("[AdminController] 키워드 추가 완료"); @@ -47,7 +47,7 @@ public ResponseEntity addKeyword( @DeleteMapping("/{keyword}") @LogMonitoringInvocation public ResponseEntity deleteKeyword( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @PathVariable String keyword) { adminKeywordService.deleteKeyword(keyword); return ResponseEntity.ok().build(); diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AdminControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/AdminControllerDocs.java index 0aca97a..d837464 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AdminControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AdminControllerDocs.java @@ -1,7 +1,7 @@ package onepiece.dailysnapbackend.controller; import io.swagger.v3.oas.annotations.Operation; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.KeywordRequest; import org.springframework.http.ResponseEntity; @@ -19,7 +19,7 @@ public interface AdminControllerDocs { - `200 OK` → 성공 """ ) - ResponseEntity addKeyword(CustomUserDetails userDetails, KeywordRequest request); + ResponseEntity addKeyword(CustomOAuth2User userDetails, KeywordRequest request); @Operation( summary = "특정 키워드 삭제 (관리자 전용)", @@ -33,5 +33,5 @@ public interface AdminControllerDocs { - `200 OK` → 성공 """ ) - ResponseEntity deleteKeyword(CustomUserDetails userDetails, String keyword); + ResponseEntity deleteKeyword(CustomOAuth2User userDetails, String keyword); } diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java b/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java index ad3d4b9..a0dd3ee 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java @@ -3,18 +3,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import onepiece.dailysnapbackend.object.dto.SignInRequest; -import onepiece.dailysnapbackend.object.dto.SignUpRequest; import onepiece.dailysnapbackend.service.MemberService; -import onepiece.dailysnapbackend.service.keyword.AdminKeywordService; -import onepiece.dailysnapbackend.service.keyword.KeywordSelectionService; import onepiece.dailysnapbackend.util.log.LogMonitoringInvocation; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @@ -26,27 +21,17 @@ public class AuthController implements AuthControllerDocs{ private final MemberService memberService; - private final KeywordSelectionService keywordSelectionService; - private final AdminKeywordService adminKeywordService; // =========================== // 인증 관련 API // =========================== - // 회원가입 - @Override - @PostMapping("/api/auth/sign-up") - @LogMonitoringInvocation - public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { - memberService.signUp(request); - return ResponseEntity.ok().build(); - } - // 로그인 @Override - @PostMapping(value = "/login", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) @LogMonitoringInvocation - public ResponseEntity signIn(SignInRequest request) { + public ResponseEntity signIn(SignInRequest request, HttpServletResponse response) { + memberService.socialSignIn(request, response); return ResponseEntity.ok().build(); } diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java index 4b79f1a..5301075 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java @@ -3,43 +3,41 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import onepiece.dailysnapbackend.object.dto.SignInRequest; -import onepiece.dailysnapbackend.object.dto.SignUpRequest; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; public interface AuthControllerDocs { @Operation( - summary = "회원가입", + summary = "소셜 로그인", description = """ - - 이 API는 인증이 필요하지 않습니다. - - ### 요청 파라미터 - - **username** (String): 사용자 이메일 (중복 불가) - - **password** (String): 사용자 비밀번호 - - **nickname** (String): 사용자 닉네임 (중복 불가) - - ### 유의사항 - - `username`과 `nickname`은 고유해야 합니다. - - """ - ) - ResponseEntity signUp(SignUpRequest request); + 클라이언트에서 받은 accessToken을 이용하여 소셜 로그인 처리 후 JWT 토큰을 발급합니다. + + ### 요청 형식 + - Content-Type: application/json - @Operation( - summary = "로그인", - description = """ - - 이 API는 인증이 필요하지 않습니다. - - ### 요청 파라미터 - - **username** (String): 사용자 이메일 - - **password** (String): 사용자 비밀번호 - - """ + ### 요청 바디 예시 + ```json + { + "provider": "KAKAO", + "username": "example@naver.com", + "accessToken": "ya29.A0ARrdaMExampleAccessToken1234567890", + "birth": "2004-01-01", + "nickname": "daily_snap_user" + } + ``` + + ### 응답 + - `200 OK`: 로그인 또는 회원가입 성공. 응답 헤더에 JWT 토큰 포함 + + ### 응답 헤더 예시 + - `Authorization: Bearer ` + - `Refresh-Token: ` + """ ) - ResponseEntity signIn(SignInRequest request); + ResponseEntity signIn(@Valid @RequestBody SignInRequest request, HttpServletResponse response); @Operation( summary = "accessToken 재발급 요청", @@ -86,4 +84,4 @@ public interface AuthControllerDocs { """ ) ResponseEntity reissue(HttpServletRequest request, HttpServletResponse response); -} \ No newline at end of file +} diff --git a/src/main/java/onepiece/dailysnapbackend/controller/FollowController.java b/src/main/java/onepiece/dailysnapbackend/controller/FollowController.java index 04ce709..fbfb269 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/FollowController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/FollowController.java @@ -4,7 +4,7 @@ import jakarta.validation.Valid; import java.util.UUID; import lombok.RequiredArgsConstructor; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.FollowRequest; import onepiece.dailysnapbackend.object.dto.MemberResponse; import onepiece.dailysnapbackend.object.postgres.Member; @@ -36,7 +36,7 @@ public class FollowController implements FollowControllerDocs{ @PostMapping("/follow") @LogMonitoringInvocation public ResponseEntity followMember( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @RequestParam UUID followeeId) { Member member = userDetails.getMember(); followService.followMember(member, followeeId); @@ -47,7 +47,7 @@ public ResponseEntity followMember( @DeleteMapping("/follow") @LogMonitoringInvocation public ResponseEntity unfollowMember( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @RequestParam UUID followeeId) { Member member = userDetails.getMember(); followService.unfollowMember(member, followeeId); @@ -58,7 +58,7 @@ public ResponseEntity unfollowMember( @GetMapping("/followers") @LogMonitoringInvocation public Page getFollowers( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @Valid @ModelAttribute FollowRequest request) { Member member = userDetails.getMember(); return followService.getFollowerList(member, request); @@ -68,7 +68,7 @@ public Page getFollowers( @GetMapping("/followings") @LogMonitoringInvocation public Page getFollowings( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @Valid @ModelAttribute FollowRequest request) { Member member = userDetails.getMember(); return followService.getFollowingList(member, request); diff --git a/src/main/java/onepiece/dailysnapbackend/controller/FollowControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/FollowControllerDocs.java index c23712a..120766c 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/FollowControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/FollowControllerDocs.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import java.util.UUID; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.FollowRequest; import onepiece.dailysnapbackend.object.dto.MemberResponse; import org.springframework.data.domain.Page; @@ -22,7 +22,7 @@ public interface FollowControllerDocs { - HTTP 200 OK: 팔로우 성공 """ ) - ResponseEntity followMember(CustomUserDetails userDetails, UUID followeeId); + ResponseEntity followMember(CustomOAuth2User userDetails, UUID followeeId); @Operation( summary = "사용자 언팔로우", @@ -36,7 +36,7 @@ public interface FollowControllerDocs { - HTTP 200 OK: 언팔로우 성공 """ ) - ResponseEntity unfollowMember(CustomUserDetails userDetails, UUID followeeId); + ResponseEntity unfollowMember(CustomOAuth2User userDetails, UUID followeeId); @Operation( summary = "팔로워 목록 조회", @@ -53,7 +53,7 @@ public interface FollowControllerDocs { - **`Page`**: 팔로워 목록 (페이지네이션 적용) """ ) - Page getFollowers(CustomUserDetails userDetails, FollowRequest request); + Page getFollowers(CustomOAuth2User userDetails, FollowRequest request); @Operation( summary = "팔로잉 목록 조회", @@ -70,5 +70,5 @@ public interface FollowControllerDocs { - **`Page`**: 팔로잉 목록 (페이지네이션 적용) """ ) - Page getFollowings(CustomUserDetails userDetails, FollowRequest request); + Page getFollowings(CustomOAuth2User userDetails, FollowRequest request); } diff --git a/src/main/java/onepiece/dailysnapbackend/controller/LikeController.java b/src/main/java/onepiece/dailysnapbackend/controller/LikeController.java index e60b32d..8af0d9d 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/LikeController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/LikeController.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.PostDetailRequest; import onepiece.dailysnapbackend.object.postgres.Member; import onepiece.dailysnapbackend.service.LikeService; @@ -29,7 +29,7 @@ public class LikeController implements LikeControllerDocs{ @PostMapping @LogMonitoringInvocation public ResponseEntity postLike( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @RequestBody PostDetailRequest request) { Member member = userDetails.getMember(); return ResponseEntity.ok(likeService.increaseLikes(request, member)); diff --git a/src/main/java/onepiece/dailysnapbackend/controller/LikeControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/LikeControllerDocs.java index 1f06cc3..5c7aa5b 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/LikeControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/LikeControllerDocs.java @@ -1,7 +1,7 @@ package onepiece.dailysnapbackend.controller; import io.swagger.v3.oas.annotations.Operation; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.PostDetailRequest; import org.springframework.http.ResponseEntity; @@ -21,5 +21,5 @@ public interface LikeControllerDocs { """ ) - ResponseEntity postLike(CustomUserDetails userDetails, PostDetailRequest request); + ResponseEntity postLike(CustomOAuth2User userDetails, PostDetailRequest request); } diff --git a/src/main/java/onepiece/dailysnapbackend/controller/PostController.java b/src/main/java/onepiece/dailysnapbackend/controller/PostController.java index 141922b..26c4640 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/PostController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/PostController.java @@ -4,7 +4,7 @@ import jakarta.validation.Valid; import java.util.UUID; import lombok.RequiredArgsConstructor; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.PostFilteredRequest; import onepiece.dailysnapbackend.object.dto.PostFilteredResponse; import onepiece.dailysnapbackend.object.dto.PostRequest; @@ -38,7 +38,7 @@ public class PostController implements PostControllerDocs { @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @LogMonitoringInvocation public ResponseEntity uploadPost( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @Valid @ModelAttribute PostRequest request) { Member member = userDetails.getMember(); return ResponseEntity.ok(postService.uploadPost(request, member)); @@ -48,7 +48,7 @@ public ResponseEntity uploadPost( @GetMapping @LogMonitoringInvocation public ResponseEntity> filteredPosts( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @Valid @ModelAttribute PostFilteredRequest request) { return ResponseEntity.ok(postService.getFilteredPosts(request)); } diff --git a/src/main/java/onepiece/dailysnapbackend/controller/PostControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/PostControllerDocs.java index b225c2a..3a83564 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/PostControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/PostControllerDocs.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import java.util.UUID; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.PostFilteredRequest; import onepiece.dailysnapbackend.object.dto.PostFilteredResponse; import onepiece.dailysnapbackend.object.dto.PostRequest; @@ -35,7 +35,7 @@ public interface PostControllerDocs { """ ) ResponseEntity uploadPost - (CustomUserDetails userDetails, PostRequest request); + (CustomOAuth2User userDetails, PostRequest request); @Operation( summary = "게시글 필터링 (페이징 및 정렬 지원)", @@ -62,7 +62,7 @@ public interface PostControllerDocs { """ ) ResponseEntity> filteredPosts - (CustomUserDetails userDetails, PostFilteredRequest request); + (CustomOAuth2User userDetails, PostFilteredRequest request); @Operation( summary = "게시글 상세 조회", @@ -84,4 +84,4 @@ public interface PostControllerDocs { """ ) ResponseEntity detailPost(UUID postId); -} \ No newline at end of file +} diff --git a/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordController.java b/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordController.java index ac251eb..f8f1824 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordController.java @@ -4,7 +4,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.DailyKeywordResponse; import onepiece.dailysnapbackend.object.dto.KeywordFilterRequest; import onepiece.dailysnapbackend.object.dto.KeywordFilterResponse; @@ -38,7 +38,7 @@ public class KeywordController implements KeywordControllerDocs { @PostMapping @LogMonitoringInvocation public ResponseEntity> filteredKeywords( - @AuthenticationPrincipal CustomUserDetails userDetails, + @AuthenticationPrincipal CustomOAuth2User userDetails, @Valid @RequestBody KeywordFilterRequest request) { return ResponseEntity.ok(keywordService.filteredKeywords (request)); } diff --git a/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordControllerDocs.java index 7c4aaa5..ea62c3c 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/keyword/KeywordControllerDocs.java @@ -1,7 +1,7 @@ package onepiece.dailysnapbackend.controller.keyword; import io.swagger.v3.oas.annotations.Operation; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.dto.DailyKeywordResponse; import onepiece.dailysnapbackend.object.dto.KeywordFilterRequest; import onepiece.dailysnapbackend.object.dto.KeywordFilterResponse; @@ -37,7 +37,7 @@ public interface KeywordControllerDocs { ) @PostMapping ResponseEntity> filteredKeywords( - CustomUserDetails userDetails, + CustomOAuth2User userDetails, @RequestBody KeywordFilterRequest request ); diff --git a/src/main/java/onepiece/dailysnapbackend/object/constants/SocialPlatform.java b/src/main/java/onepiece/dailysnapbackend/object/constants/SocialPlatform.java new file mode 100644 index 0000000..38e3a99 --- /dev/null +++ b/src/main/java/onepiece/dailysnapbackend/object/constants/SocialPlatform.java @@ -0,0 +1,6 @@ +package onepiece.dailysnapbackend.object.constants; + +public enum SocialPlatform { + KAKAO, + GOOGLE +} diff --git a/src/main/java/onepiece/dailysnapbackend/object/dto/CustomUserDetails.java b/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java similarity index 71% rename from src/main/java/onepiece/dailysnapbackend/object/dto/CustomUserDetails.java rename to src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java index 24decbe..3481cea 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/dto/CustomUserDetails.java +++ b/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java @@ -4,25 +4,24 @@ import java.util.Collections; import java.util.Map; import lombok.Getter; +import lombok.RequiredArgsConstructor; import onepiece.dailysnapbackend.object.constants.AccountStatus; +import onepiece.dailysnapbackend.object.constants.SocialPlatform; import onepiece.dailysnapbackend.object.postgres.Member; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; @Getter -public class CustomUserDetails implements UserDetails { +@RequiredArgsConstructor +public class CustomOAuth2User implements OAuth2User { private final Member member; - private Map attributes; + private final Map attributes; - public CustomUserDetails(Member member) { - this.member = member; - } - - public CustomUserDetails(Member member, Map attributes) { - this.member = member; - this.attributes = attributes; + @Override + public Map getAttributes() { + return attributes; } @Override @@ -31,39 +30,38 @@ public Collection getAuthorities() { } @Override - public String getPassword() { - return member.getPassword(); + public String getName() { + return member.getNickname(); } - @Override public String getUsername() { return member.getUsername(); } - @Override + public SocialPlatform getSocialPlatform() { + return member.getSocialPlatform(); + } + public boolean isAccountNonExpired() { // AccountStatus가 DELETE_ACCOUNT 인 경우, 계정이 만료된 것으로 간주 return member.getAccountStatus() != AccountStatus.DELETE_ACCOUNT; } - @Override public boolean isAccountNonLocked() { // AccountStatus가 DELETE_ACCOUNT 인 경우, 계정이 잠긴 것으로 간주 return member.getAccountStatus() != AccountStatus.DELETE_ACCOUNT; } - @Override public boolean isCredentialsNonExpired() { return true; // 인증 정보 항상 유효 } - @Override public boolean isEnabled() { // AccountStatus가 ACTIVE_ACCOUNT 인 경우, 계정이 활성화 return member.getAccountStatus() != AccountStatus.DELETE_ACCOUNT; } public String getMemberId() { - return member.getMemberId().toString(); // 회원의 memberId (UUID)를 string 으로 반환 + return member.getMemberId().toString(); } } diff --git a/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java b/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java index 1ccdb35..0672e3a 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java +++ b/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java @@ -13,14 +13,21 @@ @AllArgsConstructor public class SignInRequest { - // 이메일 + @NotBlank(message = "소셜 로그인 플렛폼을 입력하세요 (예: KAKAO, GOOGLE)") + @Schema(defaultValue = "KAKAO") + private String provider; + @NotBlank(message = "이메일을 입력하세요") @Schema(defaultValue = "example@naver.com") private String username; - // 비밀번호 - @NotBlank(message = "비밀번호를 입력하세요") - @Schema(defaultValue = "pw12345") - private String password; + @NotBlank(message = "accessToken을 입력하세요") + @Schema(defaultValue = "ya29.A0ARrdaMExampleAccessToken1234567890") + private String accessToken; + + @Schema(description = "생년월일 (선택)", defaultValue = "2004-01-01") + private String birth; + @Schema(description = "닉네임 (선택)", defaultValue = "daily_snap_user") + private String nickname; } diff --git a/src/main/java/onepiece/dailysnapbackend/object/dto/SignUpRequest.java b/src/main/java/onepiece/dailysnapbackend/object/dto/SignUpRequest.java deleted file mode 100644 index c05b48b..0000000 --- a/src/main/java/onepiece/dailysnapbackend/object/dto/SignUpRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package onepiece.dailysnapbackend.object.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@ToString -@AllArgsConstructor -@Getter -@Setter -@Builder -public class SignUpRequest { - - @NotBlank(message = "이메일을 입력하세요") - @Email(message = "이메일 형식이 아닙니다") - @Schema(defaultValue = "example@naver.com") - private String username; // 이메일 - - @NotBlank(message = "비밀번호를 입력하세요") - @Schema(defaultValue = "pw12345") - private String password; // 비밀번호 - - @NotBlank(message = "닉네임을 입력하세요") - @Schema(defaultValue = "nickname123") - private String nickname; // 닉네임 - - @NotBlank(message = "생년월일을 입력하세요") - @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "생년월일은 'YYYY-MM-DD' 형식이어야 합니다.") - @Schema(defaultValue = "2000-10-30") - private String birth; // 생년월일 (YYYY-MM-DD) -} diff --git a/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java b/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java index 27ba49f..3be0b3a 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java +++ b/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java @@ -15,6 +15,7 @@ import lombok.Setter; import onepiece.dailysnapbackend.object.constants.AccountStatus; import onepiece.dailysnapbackend.object.constants.Role; +import onepiece.dailysnapbackend.object.constants.SocialPlatform; @Entity @Getter @@ -34,9 +35,10 @@ public class Member extends BasePostgresEntity{ @Column(unique = true, nullable = false) private String username; - // 비밀번호 + // 소셜 제공자 + @Enumerated(EnumType.STRING) @Column(nullable = false) - private String password; + private SocialPlatform socialPlatform; // 닉네임 @Column(unique = true, nullable = false) @@ -61,6 +63,10 @@ public class Member extends BasePostgresEntity{ @Column(nullable = false) private Integer dailyUploadCount; + // 첫 로그인 여부 + @Builder.Default + private Boolean isFirstLogin = true; + // 과금 여부 @Column(nullable = false) private boolean isPaid; diff --git a/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java b/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java index b957c63..29cfdc4 100644 --- a/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java +++ b/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java @@ -2,11 +2,12 @@ import java.util.Optional; import java.util.UUID; +import onepiece.dailysnapbackend.object.constants.SocialPlatform; import onepiece.dailysnapbackend.object.postgres.Member; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { Boolean existsByUsername(String username); - Optional findByUsername(String username); -} \ No newline at end of file + Optional findByUsernameAndSocialPlatform(String username, SocialPlatform socialPlatform); +} diff --git a/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java b/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..beb921f --- /dev/null +++ b/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java @@ -0,0 +1,28 @@ +package onepiece.dailysnapbackend.service; + +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import onepiece.dailysnapbackend.object.constants.SocialPlatform; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; +import onepiece.dailysnapbackend.object.postgres.Member; +import onepiece.dailysnapbackend.repository.postgres.MemberRepository; +import onepiece.dailysnapbackend.util.exception.CustomException; +import onepiece.dailysnapbackend.util.exception.ErrorCode; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomOAuth2UserService { + + private final MemberRepository memberRepository; + + public CustomOAuth2User loadUserByUsernameAndSocialPlatform(String username, SocialPlatform socialPlatform) { + Member member = memberRepository.findByUsernameAndSocialPlatform(username, socialPlatform).orElseThrow(() -> { + log.error("회원을 찾을 수 없습니다. 회원 Username: {}, SocialPlatform: {}", username, socialPlatform); + return new CustomException(ErrorCode.MEMBER_NOT_FOUND); + }); + return new CustomOAuth2User(member, Map.of()); + } +} diff --git a/src/main/java/onepiece/dailysnapbackend/service/CustomUserDetailsService.java b/src/main/java/onepiece/dailysnapbackend/service/CustomUserDetailsService.java deleted file mode 100644 index e6892c9..0000000 --- a/src/main/java/onepiece/dailysnapbackend/service/CustomUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package onepiece.dailysnapbackend.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; -import onepiece.dailysnapbackend.object.postgres.Member; -import onepiece.dailysnapbackend.repository.postgres.MemberRepository; -import onepiece.dailysnapbackend.util.exception.CustomException; -import onepiece.dailysnapbackend.util.exception.ErrorCode; -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 MemberRepository memberRepository; - - @Override - public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - - Member member = memberRepository.findByUsername(username) - .orElseThrow(() -> { - log.error("회원을 찾을 수 없습니다. 회원 Username: {}", username); - return new CustomException(ErrorCode.MEMBER_NOT_FOUND); - }); - return new CustomUserDetails(member); - } -} diff --git a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java index ac5a5a6..1348f2a 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java @@ -7,18 +7,19 @@ import jakarta.transaction.Transactional; import java.io.BufferedReader; import java.io.IOException; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import onepiece.dailysnapbackend.object.constants.AccountStatus; import onepiece.dailysnapbackend.object.constants.Role; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; -import onepiece.dailysnapbackend.object.dto.SignUpRequest; +import onepiece.dailysnapbackend.object.constants.SocialPlatform; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; +import onepiece.dailysnapbackend.object.dto.SignInRequest; import onepiece.dailysnapbackend.object.postgres.Member; import onepiece.dailysnapbackend.repository.postgres.MemberRepository; import onepiece.dailysnapbackend.util.JwtUtil; import onepiece.dailysnapbackend.util.exception.CustomException; import onepiece.dailysnapbackend.util.exception.ErrorCode; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service @@ -27,31 +28,37 @@ public class MemberService { private final MemberRepository memberRepository; - private final BCryptPasswordEncoder bCryptPasswordEncoder; private final JwtUtil jwtUtil; - // 회원가입 @Transactional - public void signUp(SignUpRequest request) { - - // 이메일 중복 체크 - if (memberRepository.existsByUsername(request.getUsername())) { - log.error("이미 가입된 이메일입니다: {}", request.getUsername()); - throw new CustomException(ErrorCode.DUPLICATE_USERNAME); - } - - memberRepository.save(Member.builder() - .username(request.getUsername()) - .password(bCryptPasswordEncoder.encode(request.getPassword())) - .nickname(request.getNickname()) - .birth(request.getBirth()) - .role(Role.ROLE_USER) - .accountStatus(AccountStatus.ACTIVE_ACCOUNT) - .dailyUploadCount(0) - .isPaid(false) - .build() - ); - log.info("회원가입 성공: username={}", request.getUsername()); + public void socialSignIn(SignInRequest request, HttpServletResponse response) { + SocialPlatform socialPlatform = SocialPlatform.valueOf(request.getProvider()); + + // DB에서 회원 조회 + Member member = memberRepository.findByUsernameAndSocialPlatform(request.getUsername(), socialPlatform) + .orElseGet(() -> { + return memberRepository.save(Member.builder() + .username(request.getUsername()) + .socialPlatform(socialPlatform) + .nickname(request.getNickname()) + .birth(request.getBirth()) + .role(Role.ROLE_USER) + .accountStatus(AccountStatus.ACTIVE_ACCOUNT) + .dailyUploadCount(0) + .isPaid(false) + .build() + ); + }); + + log.info("소셜 로그인 성공: username={}", request.getUsername()); + + CustomOAuth2User userDetails = new CustomOAuth2User(member, Map.of()); + String accessToken = jwtUtil.createAccessToken(userDetails); + String refreshToken = jwtUtil.createRefreshToken(userDetails); + + // 응답 헤더에 토큰 설정 + response.setHeader("Authorization", "Bearer " + accessToken); + response.setHeader("Refresh-Token", refreshToken); } // 리프레시 토큰을 통해 액세스 토큰 재발급 @@ -72,8 +79,8 @@ public void reissue(HttpServletRequest request, HttpServletResponse response) { } // 새 액세스 토큰 발급 - CustomUserDetails customUserDetails = (CustomUserDetails) jwtUtil.getAuthentication(refresh).getPrincipal(); - String newAccess = jwtUtil.createAccessToken(customUserDetails); + CustomOAuth2User customOAuth2User = (CustomOAuth2User) jwtUtil.getAuthentication(refresh).getPrincipal(); + String newAccess = jwtUtil.createAccessToken(customOAuth2User); // JSON 응답 바디로 액세스 토큰 반환 response.setContentType("application/json"); @@ -122,4 +129,4 @@ private String extractRefreshTokenFromRequest(HttpServletRequest request) { throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); } } -} \ No newline at end of file +} diff --git a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java index 63a8a4e..ad8524b 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java +++ b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java @@ -14,8 +14,9 @@ import javax.crypto.SecretKey; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; -import onepiece.dailysnapbackend.service.CustomUserDetailsService; +import onepiece.dailysnapbackend.object.constants.SocialPlatform; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; +import onepiece.dailysnapbackend.service.CustomOAuth2UserService; import onepiece.dailysnapbackend.util.exception.CustomException; import onepiece.dailysnapbackend.util.exception.ErrorCode; import org.springframework.beans.factory.annotation.Value; @@ -29,7 +30,7 @@ @RequiredArgsConstructor public class JwtUtil { - private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuth2UserService customOAuth2UserService; private final RedisTemplate redisTemplate; @Value("${jwt.secret-key}") @@ -69,6 +70,16 @@ public String getRole(String token) { .get("role", String.class); } + // 토큰에서 provider 파싱 + public String getProvider(String token) { + return Jwts.parser() + .verifyWith(getSignKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .get("provider", String.class); + } + // 토큰 만료 여부 확인 public Boolean isExpired(String token) { return Jwts.parser() @@ -93,39 +104,40 @@ public String getCategory(String token) { /** * AccessToken 생성 * - * @param customUserDetails + * @param customOAuth2User * @return */ - public String createAccessToken(CustomUserDetails customUserDetails) { - log.info("엑세스 토큰 생성 중: 회원: {}", customUserDetails.getUsername()); - return createToken(ACCESS_CATEGORY, customUserDetails, accessTokenExpTime); + public String createAccessToken(CustomOAuth2User customOAuth2User) { + log.info("엑세스 토큰 생성 중: 회원: {}", customOAuth2User.getUsername()); + return createToken(ACCESS_CATEGORY, customOAuth2User, accessTokenExpTime); } /** * RefreshToken 생성 * - * @param customUserDetails + * @param customOAuth2User * @return */ - public String createRefreshToken(CustomUserDetails customUserDetails) { - log.info("리프래시 토큰 생성 중: 회원: {}", customUserDetails.getUsername()); - return createToken(REFRESH_CATEGORY, customUserDetails, refreshTokenExpTime); + public String createRefreshToken(CustomOAuth2User customOAuth2User) { + log.info("리프래시 토큰 생성 중: 회원: {}", customOAuth2User.getUsername()); + return createToken(REFRESH_CATEGORY, customOAuth2User, refreshTokenExpTime); } /** * JWT 토큰 생성 메서드 * - * @param customUserDetails 회원 상세 정보 - * @param expiredAt 만료 시간 + * @param customOAuth2User 회원 상세 정보 + * @param expiredAt 만료 시간 * @return 생성된 JWT 토큰 */ - private String createToken(String category, CustomUserDetails customUserDetails, Long expiredAt) { + private String createToken(String category, CustomOAuth2User customOAuth2User, Long expiredAt) { return Jwts.builder() - .subject(customUserDetails.getUsername()) + .subject(customOAuth2User.getUsername()) .claim("category", category) - .claim("username", customUserDetails.getUsername()) - .claim("role", customUserDetails.getMember().getRole()) + .claim("username", customOAuth2User.getUsername()) + .claim("role", customOAuth2User.getMember().getRole()) + .claim("provider", customOAuth2User.getMember().getSocialPlatform()) .issuer(issuer) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expiredAt)) @@ -236,8 +248,11 @@ public LocalDateTime getRefreshExpiryDate() { public Authentication getAuthentication(String token) { Claims claims = getClaims(token); String username = claims.getSubject(); - log.info("JWT에서 인증정보 파싱: username={}", username); - CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(username); + String provider = claims.get("provider", String.class); + SocialPlatform socialPlatform = SocialPlatform.valueOf(provider); + log.info("JWT에서 인증정보 파싱: username={}, socialPlatform={}", username, socialPlatform); + CustomOAuth2User userDetails = customOAuth2UserService.loadUserByUsernameAndSocialPlatform(username, + socialPlatform); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } diff --git a/src/main/java/onepiece/dailysnapbackend/util/config/SecurityConfig.java b/src/main/java/onepiece/dailysnapbackend/util/config/SecurityConfig.java index 72b4bb3..68ee6c3 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/config/SecurityConfig.java +++ b/src/main/java/onepiece/dailysnapbackend/util/config/SecurityConfig.java @@ -1,13 +1,14 @@ package onepiece.dailysnapbackend.util.config; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.Collections; import lombok.RequiredArgsConstructor; -import onepiece.dailysnapbackend.service.CustomUserDetailsService; +import onepiece.dailysnapbackend.service.CustomOAuth2UserService; +import onepiece.dailysnapbackend.service.MemberService; import onepiece.dailysnapbackend.util.JwtUtil; import onepiece.dailysnapbackend.util.filter.CustomLogoutHandler; -import onepiece.dailysnapbackend.util.filter.LoginFilter; import onepiece.dailysnapbackend.util.filter.TokenAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,7 +19,6 @@ 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.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -31,17 +31,19 @@ public class SecurityConfig { private final JwtUtil jwtUtil; - private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuth2UserService customOAuth2UserService; private final AuthenticationConfiguration authenticationConfiguration; private final RedisTemplate redisTemplate; private final CustomLogoutHandler customLogoutHandler; + private final ObjectMapper objectMapper; + private final MemberService memberService; /** * 허용된 CORS Origin 목록 */ private static final String[] ALLOWED_ORIGINS = { - "http://34.64.71.203:8087", // 메인 API 서버 - "http://34.64.71.203:8088", // 테스트 API 서버 + "http://3.34.61.168:8087", // 메인 API 서버 + "http://3.34.61.168:8088", // 테스트 API 서버 "http://localhost:8080", // 로컬 API 서버 "http://localhost:3000", // 로컬 웹 서버 @@ -59,8 +61,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) // ✅ 수정된 부분 - .formLogin(AbstractHttpConfigurer::disable) // ✅ 수정된 부분 + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) .authorizeHttpRequests((authorize) -> authorize .requestMatchers(SecurityUrls.AUTH_WHITELIST.toArray(new String[0])) .permitAll() // AUTH_WHITELIST에 등록된 URL은 인증 허용 @@ -78,13 +80,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .addFilterBefore( - new TokenAuthenticationFilter(jwtUtil, customUserDetailsService), - UsernamePasswordAuthenticationFilter.class - ) - .addFilterAt( - new LoginFilter(jwtUtil, - authenticationManager(authenticationConfiguration), - redisTemplate), + new TokenAuthenticationFilter(jwtUtil, customOAuth2UserService), UsernamePasswordAuthenticationFilter.class ) .build(); @@ -117,12 +113,4 @@ public CorsConfigurationSource corsConfigurationSource() { urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", configuration); return urlBasedCorsConfigurationSource; } - - /** - * 비밀번호 인코더 빈 (BCrypt) - */ - @Bean - public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } } diff --git a/src/main/java/onepiece/dailysnapbackend/util/filter/CustomLogoutHandler.java b/src/main/java/onepiece/dailysnapbackend/util/filter/CustomLogoutHandler.java index b3f32e4..fee8d1f 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/filter/CustomLogoutHandler.java +++ b/src/main/java/onepiece/dailysnapbackend/util/filter/CustomLogoutHandler.java @@ -6,7 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; +import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.util.JwtUtil; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.Authentication; @@ -32,7 +32,7 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut log.info("로그아웃 요청 바디: accessToken={}, refreshToken={}", accessToken, refreshToken); - CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + CustomOAuth2User userDetails = (CustomOAuth2User) authentication.getPrincipal(); String memberId = userDetails.getMemberId(); String redisKey = "refreshToken:" + memberId; diff --git a/src/main/java/onepiece/dailysnapbackend/util/filter/LoginFilter.java b/src/main/java/onepiece/dailysnapbackend/util/filter/LoginFilter.java deleted file mode 100644 index d099b5a..0000000 --- a/src/main/java/onepiece/dailysnapbackend/util/filter/LoginFilter.java +++ /dev/null @@ -1,92 +0,0 @@ -package onepiece.dailysnapbackend.util.filter; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.dto.CustomUserDetails; -import onepiece.dailysnapbackend.object.postgres.Member; -import onepiece.dailysnapbackend.util.JwtUtil; -import onepiece.dailysnapbackend.util.exception.CustomException; -import onepiece.dailysnapbackend.util.exception.ErrorCode; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@RequiredArgsConstructor -@Slf4j -public class LoginFilter extends UsernamePasswordAuthenticationFilter { - - private final JwtUtil jwtUtil; - private final AuthenticationManager authenticationManager; - private final RedisTemplate redisTemplate; - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { - - // 클라이언트 요청에서 username, password 추출 - String username = obtainUsername(request); - String password = obtainPassword(request); - - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken(username, password, null); - - return authenticationManager.authenticate(authToken); - } - - // 로그인 성공 (JWT 발급) - @Override - protected void successfulAuthentication( - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain, - Authentication authentication) throws IOException { - - // CustomUserDetails - CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); - Member member = customUserDetails.getMember(); - String accessToken = jwtUtil.createAccessToken(customUserDetails); - String refreshToken = jwtUtil.createRefreshToken(customUserDetails); - - log.info("로그인 성공: 엑세스 토큰 및 리프레시 토큰 생성"); - log.info("accessToken = {}", accessToken); - log.info("refreshToken = {}", refreshToken); - - // Redis 에 Refresh Token 저장 - redisTemplate.opsForValue().set( - "refreshToken:" + customUserDetails.getMemberId(), // 키: memberId를 포함한 고유 키 - refreshToken, // 값: 리프레시 토큰 - jwtUtil.getRefreshExpirationTime(), // 만료 시간 - TimeUnit.MILLISECONDS - ); - - // JSON 응답 - Map tokenMap = new HashMap<>(); - tokenMap.put("accessToken", accessToken); - tokenMap.put("refreshToken", refreshToken); - - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - new ObjectMapper().writeValue(response.getWriter(), ResponseEntity.ok(tokenMap)); - } - - // 로그인 실패 - @Override - protected void unsuccessfulAuthentication( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException failed) { - log.error("로그인 실패"); - throw new CustomException(ErrorCode.UNAUTHORIZED); - } -} diff --git a/src/main/java/onepiece/dailysnapbackend/util/filter/TokenAuthenticationFilter.java b/src/main/java/onepiece/dailysnapbackend/util/filter/TokenAuthenticationFilter.java index 0add718..e144bd2 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/filter/TokenAuthenticationFilter.java +++ b/src/main/java/onepiece/dailysnapbackend/util/filter/TokenAuthenticationFilter.java @@ -9,7 +9,7 @@ import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.service.CustomUserDetailsService; +import onepiece.dailysnapbackend.service.CustomOAuth2UserService; import onepiece.dailysnapbackend.util.JwtUtil; import onepiece.dailysnapbackend.util.config.SecurityUrls; import onepiece.dailysnapbackend.util.exception.ErrorCode; @@ -28,7 +28,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; - private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuth2UserService customOAuth2UserService; private static final AntPathMatcher pathMatcher = new AntPathMatcher(); @Override From 9256447af785c764135fa0d8dc6002634084a3d6 Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Thu, 10 Jul 2025 23:10:55 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B6=88=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EC=88=98=EC=A0=95=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AuthController.java | 5 ++- .../object/postgres/Member.java | 4 +-- .../repository/postgres/MemberRepository.java | 3 ++ .../service/CustomOAuth2UserService.java | 7 ++-- .../service/MemberService.java | 34 +++++++++++-------- .../dailysnapbackend/util/JwtUtil.java | 3 +- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java b/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java index a0dd3ee..fdd55bb 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AuthController.java @@ -7,7 +7,6 @@ import onepiece.dailysnapbackend.object.dto.SignInRequest; import onepiece.dailysnapbackend.service.MemberService; import onepiece.dailysnapbackend.util.log.LogMonitoringInvocation; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,7 +17,7 @@ name = "인증 API", description = "회원 인증 API 제공" ) -public class AuthController implements AuthControllerDocs{ +public class AuthController implements AuthControllerDocs { private final MemberService memberService; @@ -28,7 +27,7 @@ public class AuthController implements AuthControllerDocs{ // 로그인 @Override - @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/login") @LogMonitoringInvocation public ResponseEntity signIn(SignInRequest request, HttpServletResponse response) { memberService.socialSignIn(request, response); diff --git a/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java b/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java index 3be0b3a..796c7bc 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java +++ b/src/main/java/onepiece/dailysnapbackend/object/postgres/Member.java @@ -41,11 +41,11 @@ public class Member extends BasePostgresEntity{ private SocialPlatform socialPlatform; // 닉네임 - @Column(unique = true, nullable = false) + @Column(unique = true, nullable = true) private String nickname; // 생년월일 - @Column(nullable = false) + @Column(nullable = true) private String birth; // 프로필 사진 URL diff --git a/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java b/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java index 29cfdc4..402f7d4 100644 --- a/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java +++ b/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java @@ -7,6 +7,9 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + + Optional findByUsername(String username); + Boolean existsByUsername(String username); Optional findByUsernameAndSocialPlatform(String username, SocialPlatform socialPlatform); diff --git a/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java b/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java index beb921f..56e6f07 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/CustomOAuth2UserService.java @@ -3,7 +3,6 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import onepiece.dailysnapbackend.object.constants.SocialPlatform; import onepiece.dailysnapbackend.object.dto.CustomOAuth2User; import onepiece.dailysnapbackend.object.postgres.Member; import onepiece.dailysnapbackend.repository.postgres.MemberRepository; @@ -18,9 +17,9 @@ public class CustomOAuth2UserService { private final MemberRepository memberRepository; - public CustomOAuth2User loadUserByUsernameAndSocialPlatform(String username, SocialPlatform socialPlatform) { - Member member = memberRepository.findByUsernameAndSocialPlatform(username, socialPlatform).orElseThrow(() -> { - log.error("회원을 찾을 수 없습니다. 회원 Username: {}, SocialPlatform: {}", username, socialPlatform); + public CustomOAuth2User loadUserByUsername(String username) { + Member member = memberRepository.findByUsername(username).orElseThrow(() -> { + log.error("회원을 찾을 수 없습니다. 회원 Username: {}", username); return new CustomException(ErrorCode.MEMBER_NOT_FOUND); }); return new CustomOAuth2User(member, Map.of()); diff --git a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java index 1348f2a..788a3d1 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java @@ -1,5 +1,7 @@ package onepiece.dailysnapbackend.service; +import static onepiece.dailysnapbackend.util.exception.ErrorCode.DUPLICATE_USERNAME; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -34,21 +36,25 @@ public class MemberService { public void socialSignIn(SignInRequest request, HttpServletResponse response) { SocialPlatform socialPlatform = SocialPlatform.valueOf(request.getProvider()); + if (memberRepository.existsByUsername(request.getUsername())) { + log.info("이미 존재하는 사용자입니다. username: {}", request.getUsername()); + throw new CustomException(DUPLICATE_USERNAME); + } + + // DB에서 회원 조회 - Member member = memberRepository.findByUsernameAndSocialPlatform(request.getUsername(), socialPlatform) - .orElseGet(() -> { - return memberRepository.save(Member.builder() - .username(request.getUsername()) - .socialPlatform(socialPlatform) - .nickname(request.getNickname()) - .birth(request.getBirth()) - .role(Role.ROLE_USER) - .accountStatus(AccountStatus.ACTIVE_ACCOUNT) - .dailyUploadCount(0) - .isPaid(false) - .build() - ); - }); + Member member = memberRepository.findByUsername(request.getUsername()) + .orElseGet(() -> memberRepository.save(Member.builder() + .username(request.getUsername()) + .socialPlatform(socialPlatform) + .nickname(request.getNickname()) + .birth(request.getBirth()) + .role(Role.ROLE_USER) + .accountStatus(AccountStatus.ACTIVE_ACCOUNT) + .dailyUploadCount(0) + .isPaid(false) + .build() + )); log.info("소셜 로그인 성공: username={}", request.getUsername()); diff --git a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java index ad8524b..89d326f 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java +++ b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java @@ -251,8 +251,7 @@ public Authentication getAuthentication(String token) { String provider = claims.get("provider", String.class); SocialPlatform socialPlatform = SocialPlatform.valueOf(provider); log.info("JWT에서 인증정보 파싱: username={}, socialPlatform={}", username, socialPlatform); - CustomOAuth2User userDetails = customOAuth2UserService.loadUserByUsernameAndSocialPlatform(username, - socialPlatform); + CustomOAuth2User userDetails = customOAuth2UserService.loadUserByUsername(username); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } From 59681c019fda808736189e0647d1eea0369815b4 Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Sat, 28 Jun 2025 01:11:31 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B8=ED=8C=85=20?= =?UTF-8?q?#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dailysnapbackend/repository/postgres/MemberRepository.java | 3 --- .../java/onepiece/dailysnapbackend/service/MemberService.java | 1 - 2 files changed, 4 deletions(-) diff --git a/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java b/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java index 402f7d4..754073d 100644 --- a/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java +++ b/src/main/java/onepiece/dailysnapbackend/repository/postgres/MemberRepository.java @@ -2,7 +2,6 @@ import java.util.Optional; import java.util.UUID; -import onepiece.dailysnapbackend.object.constants.SocialPlatform; import onepiece.dailysnapbackend.object.postgres.Member; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,6 +10,4 @@ public interface MemberRepository extends JpaRepository { Optional findByUsername(String username); Boolean existsByUsername(String username); - - Optional findByUsernameAndSocialPlatform(String username, SocialPlatform socialPlatform); } diff --git a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java index 788a3d1..99105ea 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java @@ -41,7 +41,6 @@ public void socialSignIn(SignInRequest request, HttpServletResponse response) { throw new CustomException(DUPLICATE_USERNAME); } - // DB에서 회원 조회 Member member = memberRepository.findByUsername(request.getUsername()) .orElseGet(() -> memberRepository.save(Member.builder() From 40f39f4281d1814303173df20a30c56eca8bd30e Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Fri, 11 Jul 2025 01:48:14 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20refreshToken?= =?UTF-8?q?=20=EC=BF=A0=ED=82=A4=20=EC=A0=84=EB=8B=AC=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AuthControllerDocs.java | 4 ++-- .../dailysnapbackend/service/MemberService.java | 12 ++++++++++-- .../onepiece/dailysnapbackend/util/JwtUtil.java | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java index 5301075..3a99feb 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java @@ -33,8 +33,8 @@ public interface AuthControllerDocs { - `200 OK`: 로그인 또는 회원가입 성공. 응답 헤더에 JWT 토큰 포함 ### 응답 헤더 예시 - - `Authorization: Bearer ` - - `Refresh-Token: ` + - `Authorization: Bearer ` + - `Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Max-Age=` """ ) ResponseEntity signIn(@Valid @RequestBody SignInRequest request, HttpServletResponse response); diff --git a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java index 99105ea..eaad58b 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; @@ -63,8 +64,15 @@ public void socialSignIn(SignInRequest request, HttpServletResponse response) { // 응답 헤더에 토큰 설정 response.setHeader("Authorization", "Bearer " + accessToken); - response.setHeader("Refresh-Token", refreshToken); - } + Cookie refreshTokenCookie = jwtUtil.createRefreshTokenCookie(refreshToken); + response.addCookie(refreshTokenCookie); + + log.info("쿠키 설정: name={}, value={}, maxAge={}", + refreshTokenCookie.getName(), refreshTokenCookie.getValue(), + refreshTokenCookie.getMaxAge()); + + response.getHeaderNames().forEach(name -> + log.info("Response Header: {}={}", name, response.getHeader(name))); } // 리프레시 토큰을 통해 액세스 토큰 재발급 @Transactional diff --git a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java index 89d326f..e7993a5 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java +++ b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java @@ -8,6 +8,7 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; +import jakarta.servlet.http.Cookie; import java.time.LocalDateTime; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -145,6 +146,22 @@ private String createToken(String category, CustomOAuth2User customOAuth2User, L .compact(); } + /** + * refreshToken Cookie 생성 + * + * @param refreshToken 리프레시 토큰 + * @return 생성된 쿠키 + */ + public Cookie createRefreshTokenCookie(String refreshToken) { + Cookie cookie = new Cookie("refresh_token", refreshToken); + cookie.setHttpOnly(true); + cookie.setSecure(false); + cookie.setPath("/"); + cookie.setMaxAge(Math.toIntExact(refreshTokenExpTime)); + cookie.setAttribute("SameSite", "Strict"); + return cookie; + } + /** * JWT 토큰 유효성 검사 * From 3400d58dc63b2a937c57320b41c862c117e1a925 Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Sat, 12 Jul 2025 21:36:37 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20refreshToken?= =?UTF-8?q?=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/DAILY-SNAP-BE-CD.yaml | 4 +- .../controller/AuthControllerDocs.java | 19 ++---- .../service/MemberService.java | 58 ++++++++----------- .../dailysnapbackend/util/JwtUtil.java | 2 +- .../util/exception/ErrorCode.java | 4 ++ 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/.github/workflows/DAILY-SNAP-BE-CD.yaml b/.github/workflows/DAILY-SNAP-BE-CD.yaml index 51efb5c..a850abb 100644 --- a/.github/workflows/DAILY-SNAP-BE-CD.yaml +++ b/.github/workflows/DAILY-SNAP-BE-CD.yaml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout code @@ -29,4 +29,4 @@ jobs: # prod 프로파일을 활성화하여 빌드 ( 테스트 코드 테스트 X ) - name: Build with Gradle - run: ./gradlew clean build -x test -Dspring.profiles.active=prod \ No newline at end of file + run: ./gradlew clean build -x test -Dspring.profiles.active=prod diff --git a/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java b/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java index 3a99feb..dfe9534 100644 --- a/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java +++ b/src/main/java/onepiece/dailysnapbackend/controller/AuthControllerDocs.java @@ -44,20 +44,13 @@ public interface AuthControllerDocs { description = """ 이 API는 인증이 필요하지 않습니다. - 요청 바디에 포함된 RefreshToken만으로 새로운 AccessToken을 발급할 수 있습니다. + 요청 쿠키에 포함된 RefreshToken만으로 새로운 AccessToken을 발급할 수 있습니다. ### 요청 파라미터 - - **Request Body**: JSON 형태의 요청 바디에 포함된 리프레시 토큰 - - **Key**: `refreshToken` + - **Cookie**: JSON 형태의 요청 바디에 포함된 리프레시 토큰 + - **Name**: `refresh_token` - **Value**: `리프레시 토큰 값` - - **요청 예시:** - ```json - { - "refreshToken": "your-refresh-token-value" - } - ``` - + ### 반환값 - 새로운 액세스 토큰은 **JSON 응답 바디**에 포함되어 반환됩니다. @@ -76,10 +69,10 @@ public interface AuthControllerDocs { **응답 코드:** - **200 OK**: 새로운 액세스 토큰 발급 성공 (헤더에 포함됨) - **401 Unauthorized**: 리프레시 토큰이 유효하지 않거나 만료됨 - - **400 Bad Request**: 요청 바디에 리프레시 토큰이 없음 + - **400 Bad Request**: 요청에 쿠키가 없거나 리프레시 토큰이 없음 **추가 설명:** - - 이 API는 `HttpServletRequest`의 요청 바디에서 `refreshToken`을 추출하여 처리합니다. + - 이 API는 `HttpServletRequest`의 요청 쿠키에서 `refreshToken`을 추출하여 처리합니다. - 클라이언트는 `application/json` 형식으로 요청해야 합니다. """ ) diff --git a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java index eaad58b..2778d02 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java @@ -1,14 +1,15 @@ package onepiece.dailysnapbackend.service; +import static onepiece.dailysnapbackend.util.exception.ErrorCode.COOKIES_NOT_FOUND; import static onepiece.dailysnapbackend.util.exception.ErrorCode.DUPLICATE_USERNAME; +import static onepiece.dailysnapbackend.util.exception.ErrorCode.REFRESH_TOKEN_EMPTY; +import static onepiece.dailysnapbackend.util.exception.ErrorCode.REFRESH_TOKEN_NOT_FOUND; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.common.util.StringUtils; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.transaction.Transactional; -import java.io.BufferedReader; import java.io.IOException; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -69,10 +70,11 @@ public void socialSignIn(SignInRequest request, HttpServletResponse response) { log.info("쿠키 설정: name={}, value={}, maxAge={}", refreshTokenCookie.getName(), refreshTokenCookie.getValue(), - refreshTokenCookie.getMaxAge()); + refreshTokenCookie.getMaxAge()); response.getHeaderNames().forEach(name -> - log.info("Response Header: {}={}", name, response.getHeader(name))); } + log.info("Response Header: {}={}", name, response.getHeader(name))); + } // 리프레시 토큰을 통해 액세스 토큰 재발급 @Transactional @@ -109,37 +111,25 @@ public void reissue(HttpServletRequest request, HttpServletResponse response) { // 리프레시 토큰 추출 private String extractRefreshTokenFromRequest(HttpServletRequest request) { - try { - BufferedReader reader = request.getReader(); - StringBuilder stringBuilder = new StringBuilder(); - - String line; - while ((line = reader.readLine()) != null) { - stringBuilder.append(line); - } - - String requestBody = stringBuilder.toString(); - log.info("Request Body: {}", requestBody); - - // JSON 파싱 - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode jsonNode = objectMapper.readTree(requestBody); - - // "refreshToken" 키 추출 - String refreshToken = jsonNode.path("refreshToken").asText(null); + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + log.error("쿠키가 존재하지 않습니다."); + throw new CustomException(COOKIES_NOT_FOUND); + } - // 리프레시 토큰이 없는 경우 - if (refreshToken == null || refreshToken.isBlank()) { - log.error("요청 바디에 refresh token 이 없습니다."); - throw new CustomException(ErrorCode.REFRESH_TOKEN_NOT_FOUND); + for (Cookie cookie : cookies) { + if ("refresh_token".equals(cookie.getName())) { + String refreshToken = cookie.getValue(); + if (StringUtils.isBlank(refreshToken)) { + log.error("리프레시 토큰이 비어있습니다."); + throw new CustomException(REFRESH_TOKEN_EMPTY); + } + log.info("refresh token 추출 성공: {}", refreshToken); + return refreshToken; } - - log.info("refresh token 추출 성공: {}", refreshToken); - return refreshToken; - - } catch (IOException e) { - log.error("요청 바디를 읽는 중 오류 발생: {}", e.getMessage()); - throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); } + + log.error("요청 쿠키에 refresh token 이 없습니다."); + throw new CustomException(REFRESH_TOKEN_NOT_FOUND); } } diff --git a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java index e7993a5..da1ee71 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java +++ b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java @@ -157,7 +157,7 @@ public Cookie createRefreshTokenCookie(String refreshToken) { cookie.setHttpOnly(true); cookie.setSecure(false); cookie.setPath("/"); - cookie.setMaxAge(Math.toIntExact(refreshTokenExpTime)); + cookie.setMaxAge(Math.toIntExact(refreshTokenExpTime) / 1000); cookie.setAttribute("SameSite", "Strict"); return cookie; } diff --git a/src/main/java/onepiece/dailysnapbackend/util/exception/ErrorCode.java b/src/main/java/onepiece/dailysnapbackend/util/exception/ErrorCode.java index 9cbcdef..b490101 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/exception/ErrorCode.java +++ b/src/main/java/onepiece/dailysnapbackend/util/exception/ErrorCode.java @@ -32,6 +32,10 @@ public enum ErrorCode { REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "리프레시 토큰을 찾을 수 없습니다."), + COOKIES_NOT_FOUND(HttpStatus.BAD_REQUEST, "쿠키가 요청에 포함되지 않았습니다."), + + REFRESH_TOKEN_EMPTY(HttpStatus.BAD_REQUEST, "리프레시 토큰이 비어있습니다."), + TOKEN_BLACKLISTED(HttpStatus.UNAUTHORIZED, "블랙리스트에 등록된 토큰입니다."), DUPLICATE_USERNAME(HttpStatus.BAD_REQUEST, "이미 가입된 회원입니다."), From ba3d7d42e89f5c7b3c1e0deaedd848b23b070bc6 Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Sat, 19 Jul 2025 00:11:12 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20refreshToken?= =?UTF-8?q?=20body=20=EB=B0=98=ED=99=98=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../object/dto/CustomOAuth2User.java | 5 ----- .../object/dto/SignInRequest.java | 4 ---- .../dailysnapbackend/service/MemberService.java | 16 +++++++++------- .../onepiece/dailysnapbackend/util/JwtUtil.java | 17 ----------------- 4 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java b/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java index 3481cea..b7d39a5 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java +++ b/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java @@ -29,11 +29,6 @@ public Collection getAuthorities() { return Collections.singletonList(new SimpleGrantedAuthority(member.getRole().name())); } - @Override - public String getName() { - return member.getNickname(); - } - public String getUsername() { return member.getUsername(); } diff --git a/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java b/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java index 0672e3a..ab4f55a 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java +++ b/src/main/java/onepiece/dailysnapbackend/object/dto/SignInRequest.java @@ -21,10 +21,6 @@ public class SignInRequest { @Schema(defaultValue = "example@naver.com") private String username; - @NotBlank(message = "accessToken을 입력하세요") - @Schema(defaultValue = "ya29.A0ARrdaMExampleAccessToken1234567890") - private String accessToken; - @Schema(description = "생년월일 (선택)", defaultValue = "2004-01-01") private String birth; diff --git a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java index 2778d02..9fe4492 100644 --- a/src/main/java/onepiece/dailysnapbackend/service/MemberService.java +++ b/src/main/java/onepiece/dailysnapbackend/service/MemberService.java @@ -65,15 +65,17 @@ public void socialSignIn(SignInRequest request, HttpServletResponse response) { // 응답 헤더에 토큰 설정 response.setHeader("Authorization", "Bearer " + accessToken); - Cookie refreshTokenCookie = jwtUtil.createRefreshTokenCookie(refreshToken); - response.addCookie(refreshTokenCookie); - log.info("쿠키 설정: name={}, value={}, maxAge={}", - refreshTokenCookie.getName(), refreshTokenCookie.getValue(), - refreshTokenCookie.getMaxAge()); + try { + response.setContentType("application/json"); + response.getWriter().write(String.format("{\"refreshToken\": \"%s\"}", refreshToken)); + } catch (IOException e) { + log.error("리프레시 토큰 응답 작성 중 오류 발생", e); + throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); + } - response.getHeaderNames().forEach(name -> - log.info("Response Header: {}={}", name, response.getHeader(name))); + log.info("accessToken 헤더 설정 및 refreshToken body 응답 성공: accessToken={}, refreshToken={}: ", accessToken, + refreshToken); } // 리프레시 토큰을 통해 액세스 토큰 재발급 diff --git a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java index da1ee71..89d326f 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java +++ b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java @@ -8,7 +8,6 @@ import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; -import jakarta.servlet.http.Cookie; import java.time.LocalDateTime; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -146,22 +145,6 @@ private String createToken(String category, CustomOAuth2User customOAuth2User, L .compact(); } - /** - * refreshToken Cookie 생성 - * - * @param refreshToken 리프레시 토큰 - * @return 생성된 쿠키 - */ - public Cookie createRefreshTokenCookie(String refreshToken) { - Cookie cookie = new Cookie("refresh_token", refreshToken); - cookie.setHttpOnly(true); - cookie.setSecure(false); - cookie.setPath("/"); - cookie.setMaxAge(Math.toIntExact(refreshTokenExpTime) / 1000); - cookie.setAttribute("SameSite", "Strict"); - return cookie; - } - /** * JWT 토큰 유효성 검사 * From e7aaa3c5da1d43f08c4aa0b54302c5cced9ae756 Mon Sep 17 00:00:00 2001 From: hwangsea <134906042+hwangsea@users.noreply.github.com> Date: Sat, 19 Jul 2025 00:28:19 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[=EA=B8=B0=EB=8A=A5=EC=B6=94=EA=B0=80][?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EB=A1=9C=EA=B7=B8=EC=9D=B8]=20CustomOAuth2Us?= =?UTF-8?q?er=20getName=20username=EC=9C=BC=EB=A1=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20#89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dailysnapbackend/object/dto/CustomOAuth2User.java | 4 ---- src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java b/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java index 3481cea..d539fc8 100644 --- a/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java +++ b/src/main/java/onepiece/dailysnapbackend/object/dto/CustomOAuth2User.java @@ -31,10 +31,6 @@ public Collection getAuthorities() { @Override public String getName() { - return member.getNickname(); - } - - public String getUsername() { return member.getUsername(); } diff --git a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java index 89d326f..fd0881a 100644 --- a/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java +++ b/src/main/java/onepiece/dailysnapbackend/util/JwtUtil.java @@ -108,7 +108,7 @@ public String getCategory(String token) { * @return */ public String createAccessToken(CustomOAuth2User customOAuth2User) { - log.info("엑세스 토큰 생성 중: 회원: {}", customOAuth2User.getUsername()); + log.info("엑세스 토큰 생성 중: 회원: {}", customOAuth2User.getName()); return createToken(ACCESS_CATEGORY, customOAuth2User, accessTokenExpTime); } @@ -119,7 +119,7 @@ public String createAccessToken(CustomOAuth2User customOAuth2User) { * @return */ public String createRefreshToken(CustomOAuth2User customOAuth2User) { - log.info("리프래시 토큰 생성 중: 회원: {}", customOAuth2User.getUsername()); + log.info("리프래시 토큰 생성 중: 회원: {}", customOAuth2User.getName()); return createToken(REFRESH_CATEGORY, customOAuth2User, refreshTokenExpTime); } @@ -133,9 +133,9 @@ public String createRefreshToken(CustomOAuth2User customOAuth2User) { private String createToken(String category, CustomOAuth2User customOAuth2User, Long expiredAt) { return Jwts.builder() - .subject(customOAuth2User.getUsername()) + .subject(customOAuth2User.getName()) .claim("category", category) - .claim("username", customOAuth2User.getUsername()) + .claim("username", customOAuth2User.getName()) .claim("role", customOAuth2User.getMember().getRole()) .claim("provider", customOAuth2User.getMember().getSocialPlatform()) .issuer(issuer)