구글 로그인 방법론
방법 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
복사
◦
참고 → 우리 코드 & Springboot-JWT-React-OAuth2.0-Eazy
방법 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 플젝
구글 로그인 구현