Skip to content

Commit

Permalink
Merge pull request #25 from Central-MakeUs/feature/kakao-login
Browse files Browse the repository at this point in the history
feat: 카카오 로그인, 회원탈퇴, JWT 재발급 API 구현
  • Loading branch information
leeeeeyeon authored Jan 14, 2024
2 parents aa17572 + 0113e25 commit 40cb556
Show file tree
Hide file tree
Showing 21 changed files with 353 additions and 191 deletions.
19 changes: 19 additions & 0 deletions packy-api/src/main/java/com/dilly/auth/api/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dilly.auth.api;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -13,6 +14,7 @@
import com.dilly.auth.dto.request.SignupRequest;
import com.dilly.global.response.DataResponseDto;
import com.dilly.jwt.JwtService;
import com.dilly.jwt.dto.JwtRequest;
import com.dilly.jwt.dto.JwtResponse;

import lombok.RequiredArgsConstructor;
Expand All @@ -33,6 +35,23 @@ public DataResponseDto<JwtResponse> signUp(
return DataResponseDto.from(authService.signUp(providerAccessToken, signupRequest));
}

@GetMapping("/sign-in/{provider}")
public DataResponseDto<JwtResponse> signIn(
@PathVariable(name = "provider") String provider,
@RequestHeader("Authorization") String providerAccessToken) {
return DataResponseDto.from(authService.signIn(provider, providerAccessToken));
}

@DeleteMapping("/withdraw")
public DataResponseDto<String> withdraw() {
return DataResponseDto.from(authService.withdraw());
}

@PostMapping("/reissue")
public DataResponseDto<JwtResponse> reissue(@RequestBody JwtRequest jwtRequest) {
return DataResponseDto.from(jwtService.reissueJwt(jwtRequest));
}

@GetMapping("/token/kakao/{code}")
public DataResponseDto<String> getKakaoAccessToken(@PathVariable(name = "code") String code) {
return DataResponseDto.from(kakaoService.getKakaoAccessToken(code));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.dilly.auth.application;

import static com.dilly.member.LoginType.*;
import static com.dilly.member.Provider.*;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -13,10 +13,13 @@
import com.dilly.auth.model.KakaoResource;
import com.dilly.global.exception.NotSupportedException;
import com.dilly.global.response.ErrorCode;
import com.dilly.global.utils.SecurityUtil;
import com.dilly.jwt.JwtService;
import com.dilly.jwt.domain.JwtWriter;
import com.dilly.jwt.dto.JwtResponse;
import com.dilly.member.LoginType;
import com.dilly.member.Member;
import com.dilly.member.Provider;
import com.dilly.member.domain.MemberReader;
import com.dilly.member.domain.MemberWriter;

import lombok.RequiredArgsConstructor;
Expand All @@ -30,33 +33,64 @@ public class AuthService {

private final JwtService jwtService;
private final KakaoService kakaoService;
private final MemberReader memberReader;
private final MemberWriter memberWriter;
private final ProfileImageReader profileImageReader;
private final KakaoAccountReader kakaoAccountReader;
private final KakaoAccountWriter kakaoAccountWriter;
private final JwtWriter jwtWriter;

public JwtResponse signUp(String providerAccessToken, SignupRequest signupRequest) {
LoginType loginType;
Provider provider;
ProfileImage profileImage = profileImageReader.findById(signupRequest.profileImg());

Member member = null;
switch (signupRequest.loginType()) {
switch (signupRequest.provider()) {
case "kakao" -> {
loginType = KAKAO;
provider = KAKAO;
KakaoResource kakaoResource = kakaoService.getKaKaoAccount(providerAccessToken);
kakaoAccountReader.isKakaoAccountPresent(kakaoResource.getId());

member = memberWriter.save(signupRequest.toEntity(loginType, profileImage));
member = memberWriter.save(signupRequest.toEntity(provider, profileImage));
kakaoAccountWriter.save(kakaoResource.toEntity(member));
}

case "apple" -> {
loginType = APPLE;
provider = APPLE;
}

default -> throw new NotSupportedException(ErrorCode.NOT_SUPPORTED_LOGIN_TYPE);
}

return jwtService.issueJwt(member);
}

public JwtResponse signIn(String provider, String providerAccessToken) {
Member member = null;
switch (provider) {
case "kakao" -> {
KakaoResource kakaoResource = kakaoService.getKaKaoAccount(providerAccessToken);
member = kakaoAccountReader.getMemberById(kakaoResource.getId());
}

default -> throw new NotSupportedException(ErrorCode.NOT_SUPPORTED_LOGIN_TYPE);
}

return jwtService.issueJwt(member);
}

public String withdraw() {
Long memberId = SecurityUtil.getMemberId();
Member member = memberReader.findById(memberId);

switch (member.getProvider()) {
case KAKAO -> kakaoService.unlinkKakaoAccount(member);
default -> throw new NotSupportedException(ErrorCode.NOT_SUPPORTED_LOGIN_TYPE);
}

jwtWriter.delete(memberId);
member.withdraw();

return "회원 탈퇴가 완료되었습니다.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientException;

import com.dilly.auth.KakaoAccount;
import com.dilly.auth.domain.KakaoAccountReader;
import com.dilly.auth.model.KakaoResource;
import com.dilly.global.exception.InternalServerException;
import com.dilly.global.response.ErrorCode;
import com.dilly.member.Member;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

Expand All @@ -31,6 +37,8 @@
@Slf4j
public class KakaoService {

private final KakaoAccountReader kakaoAccountReader;

@Value("${security.oauth2.provider.kakao.token-uri}")
private String KAKAO_TOKEN_URI;
@Value("${security.oauth2.provider.kakao.user-info-uri}")
Expand Down Expand Up @@ -96,4 +104,28 @@ public String getKakaoAccessToken(String code) {

return access_Token;
}

public void unlinkKakaoAccount(Member member) {
KakaoAccount kakaoAccount = kakaoAccountReader.findByMember(member);

WebClient webClient = WebClient.builder()
.baseUrl(KAKAO_UNLINK_URI)
.defaultHeader(HttpHeaders.AUTHORIZATION, "KakaoAK " + KAKAO_ADMIN_KEY)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.build();

MultiValueMap<String, String> bodyData = new LinkedMultiValueMap<>();
bodyData.add("target_id_type", "user_id");
bodyData.add("target_id", kakaoAccount.getId());

try {
webClient.post()
.body(BodyInserters.fromFormData(bodyData))
.retrieve()
.bodyToMono(String.class)
.block();
} catch (WebClientException e) {
throw new InternalServerException(ErrorCode.KAKAO_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import org.springframework.stereotype.Component;

import com.dilly.auth.KakaoAccount;
import com.dilly.auth.KakaoAccountRepository;
import com.dilly.global.exception.alreadyexist.MemberAlreadyExistException;
import com.dilly.global.exception.entitynotfound.MemberNotFoundException;
import com.dilly.member.Member;

import lombok.RequiredArgsConstructor;

Expand All @@ -18,4 +21,12 @@ public void isKakaoAccountPresent(String id) {
throw new MemberAlreadyExistException();
}
}

public Member getMemberById(String id) {
return kakaoAccountRepository.findById(id).orElseThrow(MemberNotFoundException::new).getMember();
}

public KakaoAccount findByMember(Member member) {
return kakaoAccountRepository.findByMember(member);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package com.dilly.auth.dto.request;

import com.dilly.admin.ProfileImage;
import com.dilly.member.LoginType;
import com.dilly.member.Member;
import com.dilly.member.Provider;

public record SignupRequest(
String loginType,
String provider,
String nickname,
Long profileImg,
Boolean pushNotification,
Boolean marketingAgreement
) {

public Member toEntity(LoginType loginType, ProfileImage profileImage) {
public Member toEntity(Provider provider, ProfileImage profileImage) {
return Member.builder()
.loginType(loginType)
.provider(provider)
.nickname(nickname)
.profileImg(profileImage)
.pushNotification(pushNotification)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,60 @@
package com.dilly.global.exception;

import com.dilly.global.response.ErrorCode;
import com.dilly.global.response.ErrorResponseDto;
import java.sql.SQLException;
import lombok.extern.slf4j.Slf4j;

import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import com.dilly.global.response.ErrorCode;
import com.dilly.global.response.ErrorResponseDto;

import lombok.extern.slf4j.Slf4j;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

// 비즈니스 예외 처리
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<Object> handleBusinessException(BusinessException e) {
log.error(e.toString(), e);
return handleExceptionInternal(e.getErrorCode());
}

// 지원하지 않는 HTTP method를 호출할 경우
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<Object> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.error("HttpRequestMethodNotSupportedException : {}", e.getMessage());
return handleExceptionInternal(ErrorCode.METHOD_NOT_ALLOWED);
}

// 그 밖에 발생하는 모든 예외 처리
@ExceptionHandler(value = {Exception.class, RuntimeException.class, SQLException.class, DataIntegrityViolationException.class})
protected ResponseEntity<Object> handleException(Exception e) {
log.error(e.toString(), e);

return handleExceptionInternal(ErrorCode.INTERNAL_ERROR, e);
}

private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(ErrorResponseDto.from(errorCode));
}

private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, Exception e) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(ErrorResponseDto.of(errorCode, e));
}
// 비즈니스 예외 처리
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<Object> handleBusinessException(BusinessException e) {
log.error(e.toString(), e);
return handleExceptionInternal(e.getErrorCode());
}

// 지원하지 않는 HTTP method를 호출할 경우
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<Object> handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException e) {
log.error("HttpRequestMethodNotSupportedException : {}", e.getMessage());
return handleExceptionInternal(ErrorCode.METHOD_NOT_ALLOWED);
}

// 존재하지 않는 URI에 접근할 경우
@ExceptionHandler(NoResourceFoundException.class)
protected ResponseEntity<Object> handleNoResourceFoundException(NoResourceFoundException e) {
return handleExceptionInternal(ErrorCode.API_NOT_FOUND);
}

// 그 밖에 발생하는 모든 예외 처리
@ExceptionHandler(value = {Exception.class, RuntimeException.class, SQLException.class,
DataIntegrityViolationException.class})
protected ResponseEntity<Object> handleException(Exception e) {
log.error(e.toString(), e);

return handleExceptionInternal(ErrorCode.INTERNAL_ERROR, e);
}

private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(ErrorResponseDto.from(errorCode));
}

private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, Exception e) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(ErrorResponseDto.of(errorCode, e));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum ErrorCode {
INTERNAL_ERROR("SERVER000", HttpStatus.INTERNAL_SERVER_ERROR, "서버에 오류가 발생했습니다."),
METHOD_NOT_ALLOWED("SERVER001", HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP Method 요청입니다."),
KAKAO_SERVER_ERROR("SERVER002", HttpStatus.INTERNAL_SERVER_ERROR, "카카오 서버 연동에 오류가 발생했습니다."),
API_NOT_FOUND("SERVER003", HttpStatus.NOT_FOUND, "요청한 API를 찾을 수 없습니다."),

// Authorization
NOT_SUPPORTED_LOGIN_TYPE("AUTH000", HttpStatus.BAD_REQUEST, "지원하지 않는 로그인 타입입니다."),
Expand All @@ -28,6 +29,7 @@ public enum ErrorCode {
UNSUPPORTED_JWT("AUTH004", HttpStatus.UNAUTHORIZED, "지원하지 않는 JWT 토큰입니다."),
ILLEGAL_JWT("AUTH005", HttpStatus.UNAUTHORIZED, "잘못된 JWT 토큰입니다."),
INVALID_REFRESH_TOKEN("AUTH006", HttpStatus.UNAUTHORIZED, "유효하지 않은 Refresh Token입니다."),
AUTH_INFO_NOT_FOUND("AUTH007", HttpStatus.UNAUTHORIZED, "Security Context에 인증 정보가 없습니다."),

// Not Found
MEMBER_NOT_FOUND("NF000", HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."),
Expand Down
24 changes: 24 additions & 0 deletions packy-api/src/main/java/com/dilly/global/utils/SecurityUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dilly.global.utils;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import com.dilly.global.exception.AuthorizationFailedException;
import com.dilly.global.response.ErrorCode;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SecurityUtil {

public static Long getMemberId() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null || authentication.getName() == null || authentication.getName()
.equals("anonymousUser")) {
throw new AuthorizationFailedException(ErrorCode.AUTH_INFO_NOT_FOUND);
}

return Long.parseLong(authentication.getName());
}
}
Loading

0 comments on commit 40cb556

Please sign in to comment.