-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from Gongjakso/feat/login
feat: 카카오 로그인 개발
- Loading branch information
Showing
30 changed files
with
1,187 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 44 additions & 7 deletions
51
src/main/java/com/gongjakso/server/domain/member/controller/AuthController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,59 @@ | ||
package com.gongjakso.server.domain.member.controller; | ||
|
||
import com.gongjakso.server.domain.member.service.MemberService; | ||
import com.gongjakso.server.domain.member.dto.LoginRes; | ||
import com.gongjakso.server.domain.member.service.AuthService; | ||
import com.gongjakso.server.global.common.ApplicationResponse; | ||
import com.gongjakso.server.global.security.PrincipalDetails; | ||
import com.gongjakso.server.global.security.jwt.dto.TokenDto; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/v1/auth") | ||
@Tag(name = "Auth", description = "인증 관련 API") | ||
public class AuthController { | ||
|
||
private final MemberService memberService; | ||
private final AuthService authService; | ||
|
||
@GetMapping("/test") | ||
private String testAPI() { | ||
return "API TEST"; | ||
public String test() { | ||
return "test"; | ||
} | ||
|
||
@Operation(summary = "로그인 API", description = "카카오 로그인 페이지로 리다이렉트되어 카카오 로그인을 수행할 수 있도록 안내") | ||
@PostMapping("/sign-in") | ||
public ApplicationResponse<LoginRes> signIn(@RequestParam(name = "code") String code) { | ||
return ApplicationResponse.ok(authService.signIn(code)); | ||
} | ||
|
||
@Operation(summary = "로그아웃 API", description = "로그아웃된 JWT 블랙리스트 등록") | ||
@PostMapping("/sign-out") | ||
public ApplicationResponse<Void> signOut(HttpServletRequest request, @AuthenticationPrincipal PrincipalDetails principalDetails) { | ||
String token = request.getHeader("Authorization"); | ||
authService.signOut(token, principalDetails.getMember()); | ||
return ApplicationResponse.ok(); | ||
} | ||
|
||
@Operation(summary = "회원탈퇴 API", description = "회원탈퇴 등록") | ||
@PostMapping("/withdrawal") | ||
public ApplicationResponse<Void> withdrawal(@AuthenticationPrincipal PrincipalDetails principalDetails) { | ||
authService.withdrawal(principalDetails.getMember()); | ||
return ApplicationResponse.ok(); | ||
} | ||
|
||
@Operation(summary = "토큰재발급 API", description = "RefreshToken 정보로 요청 시, ") | ||
@GetMapping("/reissue") | ||
public ApplicationResponse<TokenDto> reissue(HttpServletRequest request, @AuthenticationPrincipal PrincipalDetails principalDetails) { | ||
String token = request.getHeader("Authorization"); | ||
return ApplicationResponse.ok(authService.reissue(token, principalDetails.getMember())); | ||
} | ||
} | ||
|
||
// https://yeees.tistory.com/231 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
src/main/java/com/gongjakso/server/domain/member/dto/LoginRes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.gongjakso.server.domain.member.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
import com.gongjakso.server.domain.member.entity.Member; | ||
import com.gongjakso.server.domain.member.enumerate.LoginType; | ||
import com.gongjakso.server.domain.member.enumerate.MemberType; | ||
import com.gongjakso.server.global.security.jwt.dto.TokenDto; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
@JsonInclude(JsonInclude.Include.NON_NULL) | ||
public record LoginRes( | ||
@NotNull Long memberId, | ||
@NotNull String email, | ||
@NotNull String name, | ||
String profileUrl, | ||
MemberType memberType, | ||
LoginType loginType, | ||
String status, | ||
String major, | ||
String job, | ||
@NotNull String accessToken, | ||
@NotNull String refreshToken | ||
) { | ||
public static LoginRes of(Member member, TokenDto tokenDto) { | ||
return LoginRes.builder() | ||
.memberId(member.getMemberId()) | ||
.email(member.getEmail()) | ||
.name(member.getName()) | ||
.profileUrl(member.getProfileUrl()) | ||
.memberType(member.getMemberType()) | ||
.loginType(member.getLoginType()) | ||
.status(member.getStatus()) | ||
.major(member.getMajor()) | ||
.job(member.getJob()) | ||
.accessToken(tokenDto.accessToken()) | ||
.refreshToken(tokenDto.refreshToken()) | ||
.build(); | ||
} | ||
} |
7 changes: 5 additions & 2 deletions
7
src/main/java/com/gongjakso/server/domain/member/dto/MemberReq.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,11 @@ | ||
package com.gongjakso.server.domain.member.dto; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
public record MemberReq(String email, | ||
String name) { | ||
public record MemberReq(@NotNull String name, | ||
String status, | ||
String major, | ||
String job) { | ||
} |
35 changes: 34 additions & 1 deletion
35
src/main/java/com/gongjakso/server/domain/member/dto/MemberRes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,37 @@ | ||
package com.gongjakso.server.domain.member.dto; | ||
|
||
public record MemberRes() { | ||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
import com.gongjakso.server.domain.member.entity.Member; | ||
import com.gongjakso.server.domain.member.enumerate.LoginType; | ||
import com.gongjakso.server.domain.member.enumerate.MemberType; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
@JsonInclude(JsonInclude.Include.NON_NULL) | ||
public record MemberRes( | ||
@NotNull Long memberId, | ||
@NotNull String email, | ||
@NotNull String name, | ||
String profileUrl, | ||
MemberType memberType, | ||
LoginType loginType, | ||
String status, | ||
String major, | ||
String job | ||
) { | ||
|
||
public static MemberRes of(Member member) { | ||
return MemberRes.builder() | ||
.memberId(member.getMemberId()) | ||
.email(member.getEmail()) | ||
.name(member.getName()) | ||
.profileUrl(member.getProfileUrl()) | ||
.memberType(member.getMemberType()) | ||
.loginType(member.getLoginType()) | ||
.status(member.getStatus()) | ||
.major(member.getMajor()) | ||
.job(member.getJob()) | ||
.build(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 10 additions & 1 deletion
11
src/main/java/com/gongjakso/server/domain/member/enumerate/MemberType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,14 @@ | ||
package com.gongjakso.server.domain.member.enumerate; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum MemberType { | ||
GENERAL, ADMIN | ||
GENERAL("ROLE_GENERAL", "일반"), | ||
ADMIN("ROLE_ADMIN", "관리자"); | ||
|
||
private final String role; | ||
private final String title; | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/gongjakso/server/domain/member/repository/MemberRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
package com.gongjakso.server.domain.member.repository; | ||
|
||
import com.gongjakso.server.domain.member.entity.Member; | ||
import com.gongjakso.server.domain.member.enumerate.MemberType; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
import java.util.Optional; | ||
|
||
public interface MemberRepository extends JpaRepository<Member, Long> { | ||
|
||
Optional<Member> findMemberByEmailAndDeletedAtIsNull(String email); | ||
|
||
Optional<Member> findMemberByEmailAndMemberTypeAndDeletedAtIsNull(String email, MemberType memberType); | ||
} |
99 changes: 99 additions & 0 deletions
99
src/main/java/com/gongjakso/server/domain/member/service/AuthService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package com.gongjakso.server.domain.member.service; | ||
|
||
import com.gongjakso.server.domain.member.dto.LoginRes; | ||
import com.gongjakso.server.domain.member.entity.Member; | ||
import com.gongjakso.server.domain.member.repository.MemberRepository; | ||
import com.gongjakso.server.global.exception.ApplicationException; | ||
import com.gongjakso.server.global.exception.ErrorCode; | ||
import com.gongjakso.server.global.security.jwt.TokenProvider; | ||
import com.gongjakso.server.global.security.jwt.dto.TokenDto; | ||
import com.gongjakso.server.global.security.kakao.KakaoClient; | ||
import com.gongjakso.server.global.security.kakao.dto.KakaoProfile; | ||
import com.gongjakso.server.global.security.kakao.dto.KakaoToken; | ||
import com.gongjakso.server.global.util.redis.RedisClient; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
public class AuthService { | ||
|
||
private final KakaoClient kakaoClient; | ||
private final RedisClient redisClient; | ||
private final TokenProvider tokenProvider; | ||
private final MemberRepository memberRepository; | ||
|
||
@Transactional | ||
public LoginRes signIn(String code) { | ||
// Business Logic | ||
// 카카오로 액세스 토큰 요청하기 | ||
KakaoToken kakaoAccessToken = kakaoClient.getKakaoAccessToken(code); | ||
|
||
// 카카오톡에 있는 사용자 정보 반환 | ||
KakaoProfile kakaoProfile = kakaoClient.getMemberInfo(kakaoAccessToken); | ||
|
||
// 반환된 정보의 이메일 기반으로 사용자 테이블에서 계정 정보 조회 진행 | ||
// 이메일 존재 시 로그인 , 존재하지 않을 경우 회원가입 진행 | ||
Member member = memberRepository.findMemberByEmailAndDeletedAtIsNull(kakaoProfile.kakao_account().email()).orElse(null); | ||
|
||
if(member == null) { | ||
Member newMember = Member.builder() | ||
.email(kakaoProfile.kakao_account().email()) | ||
.name(kakaoProfile.kakao_account().profile().nickname()) | ||
.memberType("GENERAL") | ||
.loginType("KAKAO") | ||
.build(); | ||
|
||
member = memberRepository.save(newMember); | ||
} | ||
|
||
TokenDto tokenDto = tokenProvider.createToken(member); | ||
|
||
// Redis에 RefreshToken 저장 | ||
// TODO: timeout 관련되어 constant가 아닌 tokenProvider 내의 메소드로 관리할 수 있도록 수정 필요 | ||
redisClient.setValue(member.getEmail(), tokenDto.refreshToken(), 30 * 24 * 60 * 60 * 1000L); | ||
|
||
// Response | ||
return LoginRes.of(member, tokenDto); | ||
} | ||
|
||
public void signOut(String token, Member member) { | ||
// Validation | ||
String accessToken = token.substring(7); | ||
tokenProvider.validateToken(accessToken); | ||
|
||
// Business Logic - Refresh Token 삭제 및 Access Token 블랙리스트 등록 | ||
String key = member.getEmail(); | ||
redisClient.deleteValue(key); | ||
redisClient.setValue(accessToken, "logout", tokenProvider.getExpiration(accessToken)); | ||
|
||
// Response | ||
} | ||
|
||
@Transactional | ||
public void withdrawal(Member member) { | ||
// Validation | ||
|
||
// Business Logic - 회원 논리적 삭제 진행 | ||
memberRepository.delete(member); | ||
|
||
// Response | ||
} | ||
|
||
public TokenDto reissue(String token, Member member) { | ||
// Validation - RefreshToken 유효성 검증 | ||
String refreshToken = token.substring(7); | ||
tokenProvider.validateToken(refreshToken); | ||
String email = tokenProvider.getEmail(refreshToken); | ||
String redisRefreshToken = redisClient.getValue(email); | ||
// 입력받은 refreshToken과 Redis의 RefreshToken 간의 일치 여부 검증 | ||
if(refreshToken.isBlank() || redisRefreshToken.isEmpty() || !redisRefreshToken.equals(refreshToken)) { | ||
throw new ApplicationException(ErrorCode.WRONG_TOKEN_EXCEPTION); | ||
} | ||
|
||
// Business Logic & Response - Access Token 새로 발급 + Refresh Token의 유효 기간이 Access Token의 유효 기간보다 짧아졌을 경우 Refresh Token도 재발급 | ||
return tokenProvider.reissue(member, refreshToken); | ||
} | ||
} |
Oops, something went wrong.