From 8546fccc3246f4a6e6d5818799e942b415ed0775 Mon Sep 17 00:00:00 2001 From: yumzen Date: Tue, 5 Nov 2024 17:01:43 +0900 Subject: [PATCH] =?UTF-8?q?#283:=20Naver=20login=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/enumerate/LoginType.java | 2 +- .../domain/member/service/AuthService.java | 94 ++++++++++++++-- .../global/security/naver/NaverClient.java | 101 ++++++++++++++++++ .../security/naver/NaverUserService.java | 18 ++++ .../security/naver/dto/NaverProfile.java | 25 +++++ .../global/security/naver/dto/NaverToken.java | 16 +++ 6 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/gongjakso/server/global/security/naver/NaverClient.java create mode 100644 src/main/java/com/gongjakso/server/global/security/naver/NaverUserService.java create mode 100644 src/main/java/com/gongjakso/server/global/security/naver/dto/NaverProfile.java create mode 100644 src/main/java/com/gongjakso/server/global/security/naver/dto/NaverToken.java diff --git a/src/main/java/com/gongjakso/server/domain/member/enumerate/LoginType.java b/src/main/java/com/gongjakso/server/domain/member/enumerate/LoginType.java index 83361275..7c781a88 100644 --- a/src/main/java/com/gongjakso/server/domain/member/enumerate/LoginType.java +++ b/src/main/java/com/gongjakso/server/domain/member/enumerate/LoginType.java @@ -1,5 +1,5 @@ package com.gongjakso.server.domain.member.enumerate; public enum LoginType { - GENERAL, KAKAO + GENERAL, KAKAO, GOOGLE, NAVER } diff --git a/src/main/java/com/gongjakso/server/domain/member/service/AuthService.java b/src/main/java/com/gongjakso/server/domain/member/service/AuthService.java index 8504f9c0..92c7eac4 100644 --- a/src/main/java/com/gongjakso/server/domain/member/service/AuthService.java +++ b/src/main/java/com/gongjakso/server/domain/member/service/AuthService.java @@ -2,14 +2,21 @@ import com.gongjakso.server.domain.member.dto.LoginRes; import com.gongjakso.server.domain.member.entity.Member; +import com.gongjakso.server.domain.member.enumerate.LoginType; 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.google.GoogleClient; +import com.gongjakso.server.global.security.google.dto.GoogleProfile; +import com.gongjakso.server.global.security.google.dto.GoogleToken; 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.security.naver.NaverClient; +import com.gongjakso.server.global.security.naver.dto.NaverProfile; +import com.gongjakso.server.global.security.naver.dto.NaverToken; import com.gongjakso.server.global.util.redis.RedisClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -24,20 +31,47 @@ public class AuthService { private final RedisClient redisClient; private final TokenProvider tokenProvider; private final MemberRepository memberRepository; + private final GoogleClient googleClient; + private final NaverClient naverClient; @Transactional - public LoginRes signIn(String code, String redirectUri) { + public LoginRes signIn(String code, String redirectUri, String type) { // Business Logic + String loginTypeUpper = type.toUpperCase(); + LoginType loginType = LoginType.valueOf(loginTypeUpper); + Member member = null; + + if (loginType.equals(LoginType.KAKAO)) { + member = kakaoMember(code, redirectUri); + } + + if (loginType.equals(LoginType.GOOGLE)) { + member = googleMember(code, redirectUri); + } + + if(loginType.equals(LoginType.NAVER)){ + member = naverMember(code, redirectUri); + } + + 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 Member kakaoMember(String code, String redirectUri) { // 카카오로 액세스 토큰 요청하기 KakaoToken kakaoAccessToken = kakaoClient.getKakaoAccessToken(code, redirectUri); // 카카오톡에 있는 사용자 정보 반환 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()) @@ -46,17 +80,57 @@ public LoginRes signIn(String code, String redirectUri) { .loginType("KAKAO") .build(); - member = memberRepository.save(newMember); + return memberRepository.save(newMember); } + return member; + } - TokenDto tokenDto = tokenProvider.createToken(member); + public Member googleMember(String code, String redirectUri) { + // 구글로 액세스 토큰 요청하기 + GoogleToken googleAccessToken = googleClient.getGoogleAccessToken(code, redirectUri); - // Redis에 RefreshToken 저장 - // TODO: timeout 관련되어 constant가 아닌 tokenProvider 내의 메소드로 관리할 수 있도록 수정 필요 - redisClient.setValue(member.getEmail(), tokenDto.refreshToken(), 30 * 24 * 60 * 60 * 1000L); + // 구글에 있는 사용자 정보 반환 + GoogleProfile googleProfile = googleClient.getMemberInfo(googleAccessToken); - // Response - return LoginRes.of(member, tokenDto); + // 반환된 정보의 이메일 기반으로 사용자 테이블에서 계정 정보 조회 진행 + // 이메일 존재 시 로그인 , 존재하지 않을 경우 회원가입 진행 + Member member = memberRepository.findMemberByEmailAndDeletedAtIsNull(googleProfile.email()).orElse(null); + + if(member == null) { + Member newMember = Member.builder() + .email(googleProfile.email()) + .name(googleProfile.name()) + .memberType("GENERAL") + .loginType("GOOGLE") + .build(); + + return memberRepository.save(newMember); + } + return member; + } + + public Member naverMember(String code, String redirectUri){ + // 네이버로 액세스 토큰 요청하기 + NaverToken naverAccessToken = naverClient.getNaverAccessToken(code, redirectUri); + + // 네이버에 있는 사용자 정보 반환 + NaverProfile naverProfile = naverClient.getMemberInfo(naverAccessToken); + + // 반환된 정보의 이메일 기반으로 사용자 테이블에서 계정 정보 조회 진행 + // 이메일 존재 시 로그인 , 존재하지 않을 경우 회원가입 진행 + Member member = memberRepository.findMemberByEmailAndDeletedAtIsNull(naverProfile.response().email()).orElse(null); + + if(member == null) { + Member newMember = Member.builder() + .email(naverProfile.response().email()) + .name(naverProfile.response().name()) + .memberType("GENERAL") + .loginType("NAVER") + .build(); + + return memberRepository.save(newMember); + } + return member; } public void signOut(String token, Member member) { diff --git a/src/main/java/com/gongjakso/server/global/security/naver/NaverClient.java b/src/main/java/com/gongjakso/server/global/security/naver/NaverClient.java new file mode 100644 index 00000000..9aed2e61 --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/naver/NaverClient.java @@ -0,0 +1,101 @@ +package com.gongjakso.server.global.security.naver; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.gongjakso.server.global.security.naver.dto.NaverProfile; +import com.gongjakso.server.global.security.naver.dto.NaverToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +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; + +@Slf4j +@Component +@RequiredArgsConstructor +public class NaverClient { + + @Value("${spring.security.oauth2.client.provider.naver.token-uri}") + private String naverTokenUri; + + @Value("${spring.security.oauth2.client.registration.naver.client-id}") + private String naverClientId; + + @Value("${spring.security.oauth2.client.registration.naver.authorization-grant-type}") + private String naverGrantType; + + @Value("${spring.security.oauth2.client.registration.naver.client-secret}") + private String naverClientSecret; + + @Value("${spring.security.oauth2.client.provider.naver.user-info-uri}") + private String naverUserInfoUri; + + @Value("${spring.security.oauth2.client.provider.naver.authorization-uri}") + private String naverAuthorizationUri; + + /** + * 네이버 서버에 인가코드 기반으로 사용자의 토큰 정보를 조회하는 메소드 + * @param code - 네이버에서 발급해준 인가 코드 + * @return - 네이버에서 반환한 응답 토큰 객체 + */ + public NaverToken getNaverAccessToken(String code, String redirectUri) { + // 요청 보낼 객체 기본 생성 + WebClient webClient = WebClient.create(naverTokenUri); + + //요청 본문 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", naverGrantType); + params.add("client_id", naverClientId); + params.add("redirect_uri", redirectUri); + params.add("code", code); + params.add("client_secret", naverClientSecret); + + // 요청 보내기 및 응답 수신 + String response = webClient.post() + .uri(naverTokenUri) + .header("Content-type", "application/x-www-form-urlencoded") + .body(BodyInserters.fromFormData(params)) + .retrieve() // 데이터 받는 방식, 스프링에서는 exchange는 메모리 누수 가능성 때문에 retrieve 권장 + .bodyToMono(String.class) // (Mono는 단일 데이터, Flux는 복수 데이터) + .block();// 비동기 방식의 데이터 수신 + + // 수신된 응답 Mapping + ObjectMapper objectMapper = new ObjectMapper(); + NaverToken naverToken; + try { + naverToken = objectMapper.readValue(response, NaverToken.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return naverToken; + } + + public NaverProfile getMemberInfo(NaverToken naverToken) { + // 요청 기본 객체 생성 + WebClient webClient = WebClient.create(naverUserInfoUri); + + // 요청 보내서 응답 받기 + String response = webClient.post() + .uri(naverUserInfoUri) + .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") + .header("Authorization", "Bearer " + naverToken.access_token()) + .retrieve() + .bodyToMono(String.class) + .block(); + + // 수신된 응답 Mapping + ObjectMapper objectMapper = new ObjectMapper(); + NaverProfile naverProfile; + try { + naverProfile = objectMapper.readValue(response, NaverProfile.class); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + return naverProfile; + } +} diff --git a/src/main/java/com/gongjakso/server/global/security/naver/NaverUserService.java b/src/main/java/com/gongjakso/server/global/security/naver/NaverUserService.java new file mode 100644 index 00000000..b3a36fc1 --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/naver/NaverUserService.java @@ -0,0 +1,18 @@ +package com.gongjakso.server.global.security.naver; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class NaverUserService extends DefaultOAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { + return null; + } +} diff --git a/src/main/java/com/gongjakso/server/global/security/naver/dto/NaverProfile.java b/src/main/java/com/gongjakso/server/global/security/naver/dto/NaverProfile.java new file mode 100644 index 00000000..6836e1fb --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/naver/dto/NaverProfile.java @@ -0,0 +1,25 @@ +package com.gongjakso.server.global.security.naver.dto; + +import lombok.Builder; + +@Builder +public record NaverProfile( + String resultcode, + String message, + NaverProfileResponse response +) { + public record NaverProfileResponse( + String id, + String name, + String nickname, + String profile_image, + String email, + String age, + String birthday, + String gender, + String mobile, + String mobile_e164, + String birthyear + ) { + } +} diff --git a/src/main/java/com/gongjakso/server/global/security/naver/dto/NaverToken.java b/src/main/java/com/gongjakso/server/global/security/naver/dto/NaverToken.java new file mode 100644 index 00000000..cd79d557 --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/naver/dto/NaverToken.java @@ -0,0 +1,16 @@ +package com.gongjakso.server.global.security.naver.dto; + +import jakarta.validation.constraints.Null; +import lombok.Builder; + +@Builder +public record NaverToken( + String access_token, + @Null + String refresh_token, + int expires_in, + String token_type, + String error, + String error_description +) { +} \ No newline at end of file