전체 보기
🍀

구글 로그인 방법론 및 구현

작성일자
2023/09/09
태그
DIARY_DEVELOP
프로젝트
KDKD
책 종류
1 more property

구글 로그인 방법론

방법 1. 클라가 구글과 소통 다하고, 백은 아무것도 안함

설명
아래 그림과 같이 현재 백엔드 구글 로그인 구현은 구글에 뭔가 따로 요청을 하지 않아요.
클라가 구글에 로그인 요청해서 id토큰과 profile 정보를 받는데, 여기서 profile정보를 서버에 넘겨줘버리는거죠.
플로우를 다르게 나타내보자면 아래 그림과 같아요.
장단점
간단하지만, 클라와 백이 프로필을 바로 주고 받는 거다 보니 보안에 취약하지 않을까 하는 걱정이 됩니다.
코드로 보여드리자면 아래와 같습니다.
// 구글 로그인 public AuthTokenResponse googleLogin(Map<String, Object> data) { // 1. 클라에서 구글로그인 통한 토큰(앱 자체적인 토큰 X)을 받음 // 2. 클라에서 `POST /auth/google` 로 api 요청함. 이때, 클라는 request body에 `Map<String, Object> data` 를 담아 보냄 // 3. 서버는 request body로 들어온 data를 이용해 사용자 정보를 얻음. data는 map 형태로 되어 있고, key값이 profileObj인 value에 사용자 정보가 들어있음 OAuthUserInfo googleUser = new GoogleUser((Map<String, Object>) data.get("profileObj")); /* Map<String, Object> data -> map.put("profileObj", object); data를 Map으로 받아오긴 하지만, Object 형태로 그려보자면 아래와 같음! data = { "profileObj" : { "googleId" : "구글아이디" "email" : "이메일" "name" : "이름" } } */ // 4. DB에 data에서 받아온 정보를 가진 사용자가 있는지 조회 Member findMember = memberRepository.findByLoginId(googleUser.getProvider() + "_" + googleUser.getProviderId()); // 5. DB에 사용자가 없다면, 구글 로그인을 처음 한 사용자이니, DB에 사용자 정보를 저장(회원가입 시켜줌) if (findMember == null) { Member memberRequest = Member.builder() .loginId(googleUser.getProvider() + "_" + googleUser.getProviderId()) .password(bCryptPasswordEncoder.encode("beachcombine")) .email(googleUser.getEmail()) .provider(googleUser.getProvider()) .providerId(googleUser.getProviderId()) .role("ROLE_USER") .build(); findMember = memberRepository.save(memberRequest); } // 6. 처음 온 사용자든, 기존 사용자든 구글 로그인을 시도했으니 로그아웃 전까진 앱을 마음껏 이용할 수 있게 앱의 자체적인 토큰(accessToken과 refreshToken)을 발급해줘야 함. TokenDto tokenDto = jwtUtils.createToken(findMember); refreshTokenService.saveRefreshToken(tokenDto); // 7. refreshToken은 DB에 저장. accessToken은 시큐리티 세션에 저장. // 8. 서버는 refreshToken과 accessToken, 그리고 사용자의 권한(관리자인지, 일반유저인지)을 클라에게 response body에 담아 줌 AuthTokenResponse responseDto = AuthTokenResponse.builder() .accessToken(tokenDto.getAccessToken()) .refreshToken(tokenDto.getRefreshToken()) .role(findMember.getRole()) .build(); return responseDto; }
Java
복사
문제점
프론트엔드가 굳이 서버에게 요청해서 email, username, picture 받아오는 이유 1. 신규 유저인지 기존 유저인지 확인하려면 DB 까지 갔다와야해서 2. 프론트엔드가 구글에 요청해서 email, username, picture 받아온 후 서버에게 로그인/회원가입 요청하면(나 이미 구글로그인 했어! email만 줄테니 로그인 된 것으로 처리해줘!) 서버 입장에서 진짜 구글 로그인 한 게 맞는지 믿을 수 없음 (보안 문제)

방법 2. 클라도 구글과 소통하고, 백도 함

설명
아래 그림과 같음
클라가 구글에 로그인 요청해서 id 토큰 받은 걸 서버에 넘겨줌
서버는 id토큰을 가지고 구글에 검증 요청을 해서 profile(PayLoad) 받음
플로우
장단점
코드 수정 필요. 대신 안전하고 더 보편적인 방법인 거 같음.
코드 - 구현 예시 1 → 서버가 아래 uri로 구글에 요청 보냄
카카오, 네이버는 이런 식으로 구현되어야 해서, 아마 코드 통일하려고 굳이 이렇게 구현하지 않았나 싶음
@Component @RequiredArgsConstructor public class ClientGoogle implements ClientProxy { private final WebClient webClient; // TODO ADMIN 유저 생성 시 getAdminUserData 메소드 생성 필요 @Override public Members getUserData(String accessToken) { GoogleUserResponse googleUserResponse = webClient.get() .uri("https://oauth2.googleapis.com/tokeninfo", builder -> builder.queryParam("id_token", accessToken).build()) // KAKAO와 달리 GOOGLE을 IdToken을 query parameter로 받습니다. 이로 인해 KAKAO와 uri 작성 방식이 상이합니다. .retrieve() .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new TokenValidFailedException("Social Access Token is unauthorized"))) .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new TokenValidFailedException("Internal Server Error"))) .bodyToMono(GoogleUserResponse.class) .block(); return Members.builder() .socialId(googleUserResponse.getSub()) .name(googleUserResponse.getName()) .email(googleUserResponse.getEmail()) .memberProvider(MemberProvider.GOOGLE) .roleType(RoleType.USER) .profileImagePath(googleUserResponse.getPicture()) .build(); } }
Java
복사
코드 - 구현 예시 2 → GoogleIdTokenVerifier 라이브러리 사용
보편적인 방법인지 좀 더 서칭 필요
@Override public AuthDto.GoogleProfileRes userInfoGoogle(AccessTokenReq dto) { HttpTransport transport = new NetHttpTransport(); JsonFactory jsonFactory = JacksonFactory.getDefaultInstance(); GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) .setAudience(Collections.singletonList(GOOGLE_SNS_CLIENT_ID)) //.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3)) .build(); String userId = null; String email = null; try { GoogleIdToken idToken = verifier.verify(dto.getAccessToken()); GoogleIdToken.Payload payload = idToken.getPayload(); userId = payload.getSubject(); email = payload.getEmail(); } catch (GeneralSecurityException e) { log.warn(e.getLocalizedMessage()); } catch (IOException e) { log.warn(e.getLocalizedMessage()); } return GoogleProfileRes.builder().id(userId).email(email).build(); }
Java
복사
기타 참고

구글 로그인 구현

서론
두 번째 방법을 사용하기로 했다.
첫 번째 방법에 적어둔 단점이 너무나 치명적이기 때문이다.
restTemplate 방식을 사용했다.
본론 : 내 코드
AuthController
@PostMapping("/google") public ResponseEntity<AuthLoginResponse> googleLogin(@RequestBody AuthGoogleLoginRequest request) { AuthLoginResponse response = authService.googleLogin(request); return ResponseEntity.ok().body(response); }
Java
복사
AuthService
public AuthLoginResponse googleLogin(AuthGoogleLoginRequest request) { // IdToken으로 구글에서 유저 정보 받아오기 Member googleMember = googleLoginHelper.getUserData(request.getIdToken()); // Member 검증 String loginId = googleMember.getLoginId(); Member member = memberRepository.findByLoginId(loginId); // 최초 로그인이라면 회원가입 시키기 if(member == null) { memberRepository.save(googleMember); } // 토큰 생성 String accessToken = jwtTokenProvider.generateAccessToken(member.getLoginId(), member.getRole()); String refreshToken = jwtTokenProvider.generateRefreshToken(member.getLoginId()); refreshTokenService.saveRefreshToken(refreshToken, member.getLoginId()); AuthLoginResponse response = AuthLoginResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) .role(member.getRole()) .build(); return response; }
Java
복사
GoogleLoginHelper
@Component public class GoogleLoginHelper { public Member getUserData(String idToken) { try { RestTemplate restTemplate = new RestTemplate(); ResponseEntity<GoogleAuthDto> response = restTemplate.getForEntity( "https://oauth2.googleapis.com/tokeninfo?id_token={idToken}", GoogleAuthDto.class, idToken ); GoogleAuthDto googleAuthDto = response.getBody(); return Member.builder() .nickname(googleAuthDto.getName()) .email(googleAuthDto.getEmail()) .oauthProvider("GOOGLE") .loginId(googleAuthDto.getSub()) .password("") // TODO: null도 괜찮다면 제거하기 .role("ROLE_USER") // TODO: 추후 ENUM으로 관리하기 .build(); } catch (HttpClientErrorException e) { throw new CustomException(ErrorCode.UNAUTHORIZED_TOKEN); } catch (Exception e) { throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR); } } }
Java
복사
GoogleAuthDto
@Getter @Builder public class GoogleAuthDto { private String sub; private String name; private String email; private String picture; }
Java
복사
AuthGoogleLoginRequest
@Getter @NoArgsConstructor public class AuthGoogleLoginRequest { private String idToken; }
Java
복사
AuthLogionResponse
@Getter @Builder public class AuthLoginResponse { private String accessToken; private String refreshToken; private String role; }
Java
복사

RestTemplate vs WebClient

하루 정리

TIL 작성하기
DDD 플젝
구글 로그인 구현