From e6979a70c3cb9ee54ea435aeabe93dfcd544378d Mon Sep 17 00:00:00 2001 From: Jeong Wonju Date: Tue, 3 Dec 2024 20:42:33 +0900 Subject: [PATCH] =?UTF-8?q?MATE-78=20:=20[FIX]=20JWT=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EA=B2=80=EC=A6=9D=20(#72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MATE-78 : [FEAT] 네이버 로그인 서비스 기존 회원 검증 여부 추가 * MATE-78 : [FEAT] 자체 회원가입 DTO 수정 * MATE-78 : [FEAT] 자체 로그인 기능 서비스 구현 * MATE-78 : [FEAT] 자체 로그인 기능 컨트롤러 구현 * MATE-61 : [FEAT] @AuthenticationPrincipal 객체 AuthMember 추가 --- .../mate/common/security/auth/AuthMember.java | 29 ++++++++++++ .../security/auth/CustomUserPrincipal.java | 20 -------- .../security/filter/JwtCheckFilter.java | 4 +- .../domain/auth/service/NaverAuthService.java | 9 +++- .../member/controller/MemberController.java | 16 +++---- .../member/dto/request/JoinRequest.java | 16 +++++++ .../dto/request/MemberLoginRequest.java | 13 ++++-- .../member/dto/response/JoinResponse.java | 2 + .../dto/response/MemberLoginResponse.java | 28 +++++++++++ .../member/repository/MemberRepository.java | 2 + .../domain/member/service/MemberService.java | 46 +++++++++++-------- .../auth/service/NaverAuthServiceTest.java | 4 ++ 12 files changed, 135 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/example/mate/common/security/auth/AuthMember.java delete mode 100644 src/main/java/com/example/mate/common/security/auth/CustomUserPrincipal.java create mode 100644 src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java diff --git a/src/main/java/com/example/mate/common/security/auth/AuthMember.java b/src/main/java/com/example/mate/common/security/auth/AuthMember.java new file mode 100644 index 00000000..96134355 --- /dev/null +++ b/src/main/java/com/example/mate/common/security/auth/AuthMember.java @@ -0,0 +1,29 @@ +package com.example.mate.common.security.auth; + +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; +import com.example.mate.domain.member.entity.Member; +import com.example.mate.domain.member.repository.MemberRepository; +import java.security.Principal; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AuthMember implements Principal { + + private final String userId; + + @Getter + private final Long memberId; // memberId 반환 + + // 사용자 ID 반환 + @Override + public String getName() { + return this.userId; + } + + public Member validateAuthMember(MemberRepository memberRepository) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_ID)); + } +} diff --git a/src/main/java/com/example/mate/common/security/auth/CustomUserPrincipal.java b/src/main/java/com/example/mate/common/security/auth/CustomUserPrincipal.java deleted file mode 100644 index de9c71f4..00000000 --- a/src/main/java/com/example/mate/common/security/auth/CustomUserPrincipal.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.mate.common.security.auth; - -import java.security.Principal; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class CustomUserPrincipal implements Principal { - - private final String userId; - - @Getter - private final Long memberId; // memberId 반환 - - // 사용자 ID 반환 - @Override - public String getName() { - return this.userId; - } -} diff --git a/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java b/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java index 2ea2d734..7378888f 100644 --- a/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java +++ b/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java @@ -1,6 +1,6 @@ package com.example.mate.common.security.filter; -import com.example.mate.common.security.auth.CustomUserPrincipal; +import com.example.mate.common.security.auth.AuthMember; import com.example.mate.common.security.util.JwtUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -91,7 +91,7 @@ private void setAuthentication(Map claims) { Long memberId = Long.valueOf(claims.get("memberId").toString()); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - new CustomUserPrincipal(userId, memberId), + new AuthMember(userId, memberId), null, // 이미 인증되었으므로 null Arrays.stream(roles) .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) diff --git a/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java b/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java index 2de68368..d26675de 100644 --- a/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java +++ b/src/main/java/com/example/mate/domain/auth/service/NaverAuthService.java @@ -5,6 +5,7 @@ import com.example.mate.domain.auth.config.OAuthConfig; import com.example.mate.domain.auth.dto.response.LoginResponse; import com.example.mate.domain.auth.dto.response.NaverProfileResponse; +import com.example.mate.domain.member.repository.MemberRepository; import com.nimbusds.jose.shaded.gson.JsonObject; import com.nimbusds.jose.shaded.gson.JsonParser; import java.util.Map; @@ -24,6 +25,7 @@ public class NaverAuthService { private final OAuthConfig oAuthConfig; private final RestTemplate restTemplate; + private final MemberRepository memberRepository; /** * 네이버 로그인 연결 URL을 생성 @@ -154,8 +156,13 @@ private LoginResponse createLoginTokenResponse(Map tokens, Naver .grantType("Bearer") .accessToken(tokens.get("accessToken")) .refreshToken(tokens.get("refreshToken")) - .isNewMember(true) + .isNewMember(isNewMemberByEmail(userInfo.getEmail())) .naverProfileResponse(userInfo) .build(); } + + // email을 통해 새로운 회원인지 검증 + private boolean isNewMemberByEmail(String email) { + return !memberRepository.existsByEmail(email); + } } diff --git a/src/main/java/com/example/mate/domain/member/controller/MemberController.java b/src/main/java/com/example/mate/domain/member/controller/MemberController.java index f586d2d8..76806446 100644 --- a/src/main/java/com/example/mate/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/mate/domain/member/controller/MemberController.java @@ -1,12 +1,12 @@ package com.example.mate.domain.member.controller; -import com.example.mate.common.jwt.JwtToken; import com.example.mate.common.response.ApiResponse; -import com.example.mate.common.security.auth.CustomUserPrincipal; +import com.example.mate.common.security.auth.AuthMember; import com.example.mate.domain.member.dto.request.JoinRequest; import com.example.mate.domain.member.dto.request.MemberInfoUpdateRequest; import com.example.mate.domain.member.dto.request.MemberLoginRequest; import com.example.mate.domain.member.dto.response.JoinResponse; +import com.example.mate.domain.member.dto.response.MemberLoginResponse; import com.example.mate.domain.member.dto.response.MemberProfileResponse; import com.example.mate.domain.member.dto.response.MyProfileResponse; import com.example.mate.domain.member.service.MemberService; @@ -56,11 +56,11 @@ public ResponseEntity> join( */ @Operation(summary = "CATCH Mi 서비스 로그인", description = "캐치미 서비스에 로그인합니다.") @PostMapping("/login") - public ResponseEntity> catchMiLogin( + public ResponseEntity> catchMiLogin( @Parameter(description = "회원 로그인 요청 정보", required = true) @Valid @RequestBody MemberLoginRequest request ) { - JwtToken token = memberService.loginByEmail(request); - return ResponseEntity.ok(ApiResponse.success(token)); + MemberLoginResponse response = memberService.loginByEmail(request); + return ResponseEntity.ok(ApiResponse.success(response)); } // TODO : 2024/11/29 - 내 프로필 조회 : 추후 @AuthenticationPrincipal Long memberId 받음 @@ -101,8 +101,8 @@ public ResponseEntity deleteMember(@RequestParam Long memberId) { } @GetMapping("/test") - public String test(@AuthenticationPrincipal CustomUserPrincipal principal) { - return "principal getName == " + principal.getName() + " || " + "principal getMemberId == " - + principal.getMemberId(); + public String test(@AuthenticationPrincipal AuthMember authMember) { + return "principal getName == " + authMember.getName() + " || " + "principal getMemberId == " + + authMember.getMemberId(); } } diff --git a/src/main/java/com/example/mate/domain/member/dto/request/JoinRequest.java b/src/main/java/com/example/mate/domain/member/dto/request/JoinRequest.java index 3b925197..63c1664c 100644 --- a/src/main/java/com/example/mate/domain/member/dto/request/JoinRequest.java +++ b/src/main/java/com/example/mate/domain/member/dto/request/JoinRequest.java @@ -1,7 +1,9 @@ package com.example.mate.domain.member.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Getter; @@ -10,15 +12,29 @@ @Builder public class JoinRequest { + @Schema(description = "사용자 이름", example = "홍길동") + @NotBlank(message = "이름은 필수 항목입니다.") + @Size(max = 10, message = "이름은 최대 10자까지 입력 가능합니다.") private String name; + + @Schema(description = "사용자 이메일", example = "tester@example.com") + @NotBlank(message = "이메일은 필수 항목입니다.") + @Size(max = 40, message = "이메일은 최대 40자까지 입력 가능합니다.") private String email; + + @Schema(description = "사용자 성별", example = "M") private String gender; + + @Schema(description = "사용자 출생연도", example = "2000") private String birthyear; + @Schema(description = "선택한 마이팀 ID", example = "1") @Min(value = 0, message = "teamId는 0 이상이어야 합니다.") @Max(value = 10, message = "teamId는 10 이하이어야 합니다.") private Long teamId; + @Schema(description = "사용자 닉네임", example = "tester") + @NotBlank(message = "이메일은 필수 항목입니다.") @Size(max = 20, message = "nickname은 최대 20자까지 입력할 수 있습니다.") private String nickname; } diff --git a/src/main/java/com/example/mate/domain/member/dto/request/MemberLoginRequest.java b/src/main/java/com/example/mate/domain/member/dto/request/MemberLoginRequest.java index c8b3d39f..b0464d70 100644 --- a/src/main/java/com/example/mate/domain/member/dto/request/MemberLoginRequest.java +++ b/src/main/java/com/example/mate/domain/member/dto/request/MemberLoginRequest.java @@ -1,19 +1,24 @@ package com.example.mate.domain.member.dto.request; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; @Getter @Builder -@NoArgsConstructor -@AllArgsConstructor public class MemberLoginRequest { @Schema(description = "사용자 이메일", example = "test@example.com") @NotBlank(message = "이메일은 필수 항목입니다.") + @Size(max = 40, message = "이메일은 최대 40자까지 입력 가능합니다.") private String email; + + @JsonCreator + public MemberLoginRequest(@JsonProperty("email") String email) { + this.email = email; + } } diff --git a/src/main/java/com/example/mate/domain/member/dto/response/JoinResponse.java b/src/main/java/com/example/mate/domain/member/dto/response/JoinResponse.java index bffc2939..d02b4caa 100644 --- a/src/main/java/com/example/mate/domain/member/dto/response/JoinResponse.java +++ b/src/main/java/com/example/mate/domain/member/dto/response/JoinResponse.java @@ -12,6 +12,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class JoinResponse { + private Long memberId; private String name; private String nickname; private String email; @@ -21,6 +22,7 @@ public class JoinResponse { public static JoinResponse from(Member member) { return JoinResponse.builder() + .memberId(member.getId()) .name(member.getName()) .nickname(member.getNickname()) .email(member.getEmail()) diff --git a/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java b/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java new file mode 100644 index 00000000..6104fac3 --- /dev/null +++ b/src/main/java/com/example/mate/domain/member/dto/response/MemberLoginResponse.java @@ -0,0 +1,28 @@ +package com.example.mate.domain.member.dto.response; + +import com.example.mate.domain.member.entity.Member; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MemberLoginResponse { + + private final Long memberId; + private final String grantType; + private final String accessToken; + private final String refreshToken; + + // TODO : 파라미터로 JwtToken 추가 및 토큰 매핑 + public static MemberLoginResponse from(Member member) { + return MemberLoginResponse.builder() + .memberId(member.getId()) + .grantType("Bearer") + .accessToken("accessToken") + .refreshToken("refreshToken") + .build(); + } +} diff --git a/src/main/java/com/example/mate/domain/member/repository/MemberRepository.java b/src/main/java/com/example/mate/domain/member/repository/MemberRepository.java index 45c4e246..d32a3a8f 100644 --- a/src/main/java/com/example/mate/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/mate/domain/member/repository/MemberRepository.java @@ -10,5 +10,7 @@ public interface MemberRepository extends JpaRepository { boolean existsByNickname(String nickname); + boolean existsByEmail(String email); + Optional findByEmail(String email); } diff --git a/src/main/java/com/example/mate/domain/member/service/MemberService.java b/src/main/java/com/example/mate/domain/member/service/MemberService.java index aa56b0a4..a1f4c8c3 100644 --- a/src/main/java/com/example/mate/domain/member/service/MemberService.java +++ b/src/main/java/com/example/mate/domain/member/service/MemberService.java @@ -16,6 +16,7 @@ import com.example.mate.domain.member.dto.request.MemberInfoUpdateRequest; import com.example.mate.domain.member.dto.request.MemberLoginRequest; import com.example.mate.domain.member.dto.response.JoinResponse; +import com.example.mate.domain.member.dto.response.MemberLoginResponse; import com.example.mate.domain.member.dto.response.MemberProfileResponse; import com.example.mate.domain.member.dto.response.MyProfileResponse; import com.example.mate.domain.member.entity.Member; @@ -48,6 +49,32 @@ public JoinResponse join(JoinRequest request) { return JoinResponse.from(savedMember); } + // TODO : JWT 토큰 발급 + // 자체 로그인 기능 + public MemberLoginResponse loginByEmail(MemberLoginRequest request) { + Member member = findByEmail(request.getEmail()); + // 토큰 발급한 뒤 member와 함께 넘기기 + JwtToken jwtToken = makeToken(member); + return MemberLoginResponse.from(member); + } + + // JWT 토큰 생성 + private JwtToken makeToken(Member member) { + Map payloadMap = member.getPayload(); + String accessToken = jwtUtil.createToken(payloadMap, 60 * 24 * 3); // 3일 유효 + String refreshToken = jwtUtil.createToken(Map.of("memberId", member.getId()), 60 * 24 * 7); // 7일 유효 + return JwtToken.builder() + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + private Member findByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_EMAIL)); + } + // TODO : JWT 도입 이후 본인만 접근할 수 있도록 수정 // 내 프로필 조회 public MyProfileResponse getMyProfile(Long memberId) { @@ -123,23 +150,4 @@ private Member findByMemberId(Long memberId) { return memberRepository.findById(memberId) .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_ID)); } - - public JwtToken loginByEmail(MemberLoginRequest request) { - Member member = memberRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND_BY_EMAIL)); - return makeToken(member); - } - - // JWT 토큰 생성 - private JwtToken makeToken(Member member) { - Map payloadMap = member.getPayload(); - String accessToken = jwtUtil.createToken(payloadMap, 60 * 24 * 3); // 3일 유효 - String refreshToken = jwtUtil.createToken(Map.of("memberId", member.getId()), 60 * 24 * 7); // 7일 유효 - return JwtToken.builder() - .grantType("Bearer") - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); - } - } \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java b/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java index 296ac09f..d47f5b6d 100644 --- a/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java +++ b/src/test/java/com/example/mate/domain/auth/service/NaverAuthServiceTest.java @@ -11,6 +11,7 @@ import com.example.mate.common.error.ErrorCode; import com.example.mate.domain.auth.config.OAuthConfig; import com.example.mate.domain.auth.dto.response.LoginResponse; +import com.example.mate.domain.member.repository.MemberRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,6 +35,9 @@ public class NaverAuthServiceTest { @Mock private RestTemplate restTemplate; + @Mock + private MemberRepository memberRepository; + @Test @DisplayName("네이버 로그인 연결 URL 생성") public void getAuthUrl_Success() {