Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: google login 기능 #282

Merged
merged 3 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ public class AuthController {
@Operation(summary = "로그인 API", description = "카카오 로그인 페이지로 리다이렉트되어 카카오 로그인을 수행할 수 있도록 안내")
@PostMapping("/sign-in")
public ApplicationResponse<LoginRes> 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 블랙리스트 등록")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.gongjakso.server.domain.member.enumerate;

public enum LoginType {
GENERAL, KAKAO
GENERAL, KAKAO , GOOGLE
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -46,17 +74,33 @@ 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 void signOut(String token, Member member) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String , String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gongjakso.server.global.security.google.dto;

import lombok.Builder;

@Builder
public record GoogleProfile(
String sub,
String name,
String given_name,
String family_name,
String picture,
String email,
boolean email_verified,
String locale
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gongjakso.server.global.security.google.dto;

import jakarta.validation.constraints.Null;
import lombok.Builder;

@Builder
public record GoogleToken(
String access_token,
@Null
String refresh_token,
int expires_in,
String scope,
String token_type,
String id_token
) {
}
Loading