From 59171306f6ebdf3e124264a67b506d987eec1ae6 Mon Sep 17 00:00:00 2001 From: sycuuui <102959791+sycuuui@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:55:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?#281=20feat:=20google=20login=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/AuthController.java | 5 +- .../domain/member/enumerate/LoginType.java | 2 +- .../domain/member/service/AuthService.java | 60 ++++++++++-- .../global/security/google/GoogleClient.java | 96 +++++++++++++++++++ .../security/google/GoogleUserService.java | 18 ++++ .../security/google/dto/GoogleProfile.java | 13 +++ .../security/google/dto/GoogleToken.java | 14 +++ 7 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java create mode 100644 src/main/java/com/gongjakso/server/global/security/google/GoogleUserService.java create mode 100644 src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java create mode 100644 src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java diff --git a/src/main/java/com/gongjakso/server/domain/member/controller/AuthController.java b/src/main/java/com/gongjakso/server/domain/member/controller/AuthController.java index b806920b..4d745065 100644 --- a/src/main/java/com/gongjakso/server/domain/member/controller/AuthController.java +++ b/src/main/java/com/gongjakso/server/domain/member/controller/AuthController.java @@ -24,8 +24,9 @@ public class AuthController { @Operation(summary = "로그인 API", description = "카카오 로그인 페이지로 리다이렉트되어 카카오 로그인을 수행할 수 있도록 안내") @PostMapping("/sign-in") public ApplicationResponse signIn(@RequestParam(name = "code") String code, - @RequestParam(name = "redirect-uri") String redirectUri) { - return ApplicationResponse.ok(authService.signIn(code, redirectUri)); + @RequestParam(name = "redirect-uri") String redirectUri, + @RequestParam(name = "type") String type) { + return ApplicationResponse.ok(authService.signIn(code, redirectUri,type)); } @Operation(summary = "로그아웃 API", description = "로그아웃된 JWT 블랙리스트 등록") 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..7cf730d1 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 } 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..f14cfc64 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,9 +2,13 @@ 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; @@ -21,13 +25,37 @@ public class AuthService { private final KakaoClient kakaoClient; + private final GoogleClient googleClient; private final RedisClient redisClient; private final TokenProvider tokenProvider; private final MemberRepository memberRepository; @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); + } + + 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); @@ -46,17 +74,33 @@ public LoginRes signIn(String code, String redirectUri) { .loginType("KAKAO") .build(); - member = memberRepository.save(newMember); + return memberRepository.save(newMember); } + return null; + } - 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 null; } public void signOut(String token, Member member) { diff --git a/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java b/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java new file mode 100644 index 00000000..79c4cfb7 --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java @@ -0,0 +1,96 @@ +package com.gongjakso.server.global.security.google; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.gongjakso.server.global.security.google.dto.GoogleProfile; +import com.gongjakso.server.global.security.google.dto.GoogleToken; +import lombok.RequiredArgsConstructor; +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; + +@Component +@RequiredArgsConstructor +public class GoogleClient { + @Value("${spring.security.oauth2.client.registration.google.client-id}") + private String googleClientId; + + @Value("${spring.security.oauth2.client.registration.google.client-secret}") + private String googleClientSecret; + + @Value("${spring.security.oauth2.client.registration.google.authorization-grant-type}") + private String googleGrantType; + + @Value("${spring.security.oauth2.client.provider.google.token-uri}") + private String googleTokenUri; + + @Value("${spring.security.oauth2.client.provider.google.user-info-uri}") + private String googleUserInfoUri; + + /** + * + * @param code - + * @return - + */ + public GoogleToken getGoogleAccessToken(String code, String redirectUri) { + // 요청 보낼 객체 기본 생성 + WebClient webClient = WebClient.create(googleTokenUri); + + //요청 본문 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", googleGrantType); + params.add("client_id", googleClientId); + params.add("redirect_uri", redirectUri); + params.add("code", code); + params.add("client_secret", googleClientSecret); + + // 요청 보내기 및 응답 수신 + String response = webClient.post() + .uri(googleTokenUri) + .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(); + System.out.println(objectMapper); + GoogleToken googleToken; + try { + googleToken = objectMapper.readValue(response, GoogleToken.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return googleToken; + } + + public GoogleProfile getMemberInfo(GoogleToken googleToken) { + // 요청 기본 객체 생성 + WebClient webClient = WebClient.create(googleUserInfoUri); + + // 요청 보내서 응답 받기 + String response = webClient.post() + .uri(googleUserInfoUri) + .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") + .header("Authorization", "Bearer " + googleToken.access_token()) + .retrieve() + .bodyToMono(String.class) + .block(); + + // 수신된 응답 Mapping + ObjectMapper objectMapper = new ObjectMapper(); + GoogleProfile googleProfile; + try { + googleProfile = objectMapper.readValue(response, GoogleProfile.class); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + return googleProfile; + } +} diff --git a/src/main/java/com/gongjakso/server/global/security/google/GoogleUserService.java b/src/main/java/com/gongjakso/server/global/security/google/GoogleUserService.java new file mode 100644 index 00000000..75cb02e9 --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/google/GoogleUserService.java @@ -0,0 +1,18 @@ +package com.gongjakso.server.global.security.google; + +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 GoogleUserService extends DefaultOAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { + return null; + } +} diff --git a/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java new file mode 100644 index 00000000..944db5eb --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java @@ -0,0 +1,13 @@ +package com.gongjakso.server.global.security.google.dto; + +public record GoogleProfile( + String sub, + String name, + String given_name, + String family_name, + String picture, + String email, + boolean email_verified, + String locale +) { +} diff --git a/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java new file mode 100644 index 00000000..1155dd05 --- /dev/null +++ b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java @@ -0,0 +1,14 @@ +package com.gongjakso.server.global.security.google.dto; + +import jakarta.validation.constraints.Null; + +public record GoogleToken( + String access_token, + @Null + String refresh_token, + int expires_in, + String scope, + String token_type, + String id_token +) { +} From 65dd4b8db3c6ebd3086547ca713bc3584f405c7e Mon Sep 17 00:00:00 2001 From: sycuuui <102959791+sycuuui@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:31:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?#281=20feat:=20google=20dto=20builder?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=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 --- .../gongjakso/server/domain/member/service/AuthService.java | 4 ++-- .../server/global/security/google/dto/GoogleProfile.java | 3 +++ .../server/global/security/google/dto/GoogleToken.java | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) 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 f14cfc64..25926c19 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 @@ -76,7 +76,7 @@ public Member kakaoMember(String code, String redirectUri) { return memberRepository.save(newMember); } - return null; + return member; } public Member googleMember(String code, String redirectUri) { @@ -100,7 +100,7 @@ public Member googleMember(String code, String redirectUri) { return memberRepository.save(newMember); } - return null; + return member; } public void signOut(String token, Member member) { diff --git a/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java index 944db5eb..ea28609d 100644 --- a/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java +++ b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleProfile.java @@ -1,5 +1,8 @@ package com.gongjakso.server.global.security.google.dto; +import lombok.Builder; + +@Builder public record GoogleProfile( String sub, String name, diff --git a/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java index 1155dd05..3d09ea6c 100644 --- a/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java +++ b/src/main/java/com/gongjakso/server/global/security/google/dto/GoogleToken.java @@ -1,7 +1,9 @@ package com.gongjakso.server.global.security.google.dto; import jakarta.validation.constraints.Null; +import lombok.Builder; +@Builder public record GoogleToken( String access_token, @Null From 2f8b39473943fb07ea3b4631526a53e4d9e85f38 Mon Sep 17 00:00:00 2001 From: sycuuui <102959791+sycuuui@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:13:00 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=EA=B3=BC=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=ED=99=98=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/AuthService.java | 18 ++++----- .../global/security/google/GoogleClient.java | 38 +++++++++++++++---- 2 files changed, 40 insertions(+), 16 deletions(-) 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 25926c19..2491b9d2 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 @@ -35,15 +35,15 @@ 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); - } + Member member = switch (loginType) { + case KAKAO -> kakaoMember(code, redirectUri); + case GOOGLE -> googleMember(code, redirectUri); - if(loginType.equals(LoginType.GOOGLE)) { - member = googleMember(code, redirectUri); - } + default -> null; + }; + + assert member != null; TokenDto tokenDto = tokenProvider.createToken(member); @@ -55,7 +55,7 @@ public LoginRes signIn(String code, String redirectUri, String type) { return LoginRes.of(member, tokenDto); } - public Member kakaoMember(String code, String redirectUri) { + private Member kakaoMember(String code, String redirectUri) { // 카카오로 액세스 토큰 요청하기 KakaoToken kakaoAccessToken = kakaoClient.getKakaoAccessToken(code, redirectUri); @@ -79,7 +79,7 @@ public Member kakaoMember(String code, String redirectUri) { return member; } - public Member googleMember(String code, String redirectUri) { + private Member googleMember(String code, String redirectUri) { // 구글로 액세스 토큰 요청하기 GoogleToken googleAccessToken = googleClient.getGoogleAccessToken(code, redirectUri); diff --git a/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java b/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java index 79c4cfb7..d458779c 100644 --- a/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java +++ b/src/main/java/com/gongjakso/server/global/security/google/GoogleClient.java @@ -14,19 +14,31 @@ @Component @RequiredArgsConstructor public class GoogleClient { - @Value("${spring.security.oauth2.client.registration.google.client-id}") - private String googleClientId; + @Value("${spring.security.oauth2.client.registration.google-local.redirect-uri}") + private String localGoogleRedirectUri; - @Value("${spring.security.oauth2.client.registration.google.client-secret}") - private String googleClientSecret; + @Value("${spring.security.oauth2.client.registration.google-local.client-id}") + private String localGoogleClientId; - @Value("${spring.security.oauth2.client.registration.google.authorization-grant-type}") + @Value("${spring.security.oauth2.client.registration.google-local.client-secret}") + private String localGoogleClientSecret; + + @Value("${spring.security.oauth2.client.registration.google-dev.redirect-uri}") + private String devGoogleRedirectUri; + + @Value("${spring.security.oauth2.client.registration.google-dev.client-id}") + private String devGoogleClientId; + + @Value("${spring.security.oauth2.client.registration.google-dev.client-secret}") + private String devGoogleClientSecret; + + @Value("${spring.security.oauth2.client.registration.google-dev.authorization-grant-type}") private String googleGrantType; - @Value("${spring.security.oauth2.client.provider.google.token-uri}") + @Value("${spring.security.oauth2.client.provider.google-dev.token-uri}") private String googleTokenUri; - @Value("${spring.security.oauth2.client.provider.google.user-info-uri}") + @Value("${spring.security.oauth2.client.provider.google-dev.user-info-uri}") private String googleUserInfoUri; /** @@ -35,6 +47,18 @@ public class GoogleClient { * @return - */ public GoogleToken getGoogleAccessToken(String code, String redirectUri) { + String googleClientId = null; + String googleClientSecret = null; + if(redirectUri.equals(devGoogleRedirectUri)){ + googleClientId = devGoogleClientId; + googleClientSecret = devGoogleClientSecret; + } + + if(redirectUri.equals(localGoogleRedirectUri)) { + googleClientId = localGoogleClientId; + googleClientSecret = localGoogleClientSecret; + } + // 요청 보낼 객체 기본 생성 WebClient webClient = WebClient.create(googleTokenUri);