구글 로그인
참고 링크
전체 흐름
클라이언트 - 'Google 로그인'부터 '인가 코드 전달'까지
서버 - 구글 서버에서 인가코드를 전달받은 후부터 '서버 Access Token 반환'하는 부분 까지
1. Google Cloud 프로젝트 생성
2. 소셜 로그인 시 수집할 개인정보 설정
3. 클라이언트 ID 생성
- 로그인 화면 호출
브라우저에서 구글 로그인 창을 호출하는 URL은 https://accounts/google.com/o/oauth2/v2/auth이고, 여기에 필수 파라미터 4가지를 추가해주어야 합니다.
- client_id : 앞서 발급받은 ClientID
- redirect_url : 앞서 Google Cloud에서 설정해주었던 리다이렉트 url
- response_type : code로 고정(인가 코드를 통한 로그인 방식을 사용할 것이므로)
- scope: 토큰 발급 이후 유저 정보에서 어떤 항목을 조회할 것인지(email, profile 등)
클라이언트에서 리다이렉트해주어야 할 url은 다음과 같은 형태가 됩니다.
const url = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + process.env.VUE_APP_GOOGLE_CLIENT_ID + '&redirect_uri=' + process.env.VUE_APP_GOOGLE_REDIRECT_URL + '&response_type=code' + '&scope=email profile';
브라우저에서 이 url로 접속하니 다음과 같이 소셜 로그인 화면이 뜬다.
- 인가코드 받기
여기서 계정을 선택하면 앞서 설정해준 리다이렉트 url로 리다이렉트됩니다. 이 때 리다이렉트된 url을 살펴보면 쿼리 파라미터로 code 속성을 가지고 있는데, 이 값이 바로 백엔드에 전달해주어야 할 인가코드입니다.
http://localhost:3000/login/oauth2/code/google?code={인가코드}&scope=email+profile+...
클라이언트는 이 인가 코드를 저장해두었다가, 로그인 요청과 함께 서비스 서버에 전달하도록 구성합니다.
Access Token 요청~서버 Access Token 반환(서버)
구현에 앞서 여기서 확실하게 짚고 넘어가야 할 것은, 흐름도에 표현된 Access Token과 서버 Access Token의 차이입니다. Access Token으로 표기한 토큰은 회원가입/로그인 한 사용자의 구글 계정 정보를 가져올 수 있도록 Google에서 발급해준 액세스 토큰입니다.
서버 Access Token은 우리 서버에서 자체적으로 생성한 액세스 토큰으로, 우리 서비스를 이용하기 위해 필요한 사용자 인증 토큰입니다.
간단히 흐름만 살펴보자면, 백엔드에서는 이제 클라이언트가 전달한 인가코드를 가지고 Google OAuth API를 호출하여 Access Token을 요청해야 합니다.
그리고 이 Access Token을 사용해 사용자의 구글 계정 정보를 요청하고, (회원가입이 되어있지 않은 경우) 해당 정보를 기반으로 서비스 DB에 회원(Member) 레코드를 추가합니다.
그리고 서버의 Access Token을 생성해서 클라이언트 측에 반환하기만 하면, 소셜 로그인 동작이 마무리됩니다.
그럼 본격적으로 구현에 들어가봅시다.
먼저 앞서 발급받은 client id, client secret 및 redirect url은 외부에 노출되어선 안될 중요한 정보이므로, 신경써서 관리해주어야 합니다.
옛날에 민감 정보 관리에 대해 고민했던 내용을 정리한 적이 있으니, 필요하다면 참고해주시면 감사하겠습니다.
로그인에 필요한 로직을 수행하는 도메인을 다음과 같이 설계해보았습니다.
구글에 토큰을 요청
- 문제: Spring Boot에서 RestTemplate로 Google API에 요청을 보낼 때, JSON 대신 Form-Encoded 형식을 사용해야 합니다.
- Google API의 /token 엔드포인트는 기본적으로 application/x-www-form-urlencoded 형식을 요구합니다.
- 문제 : 415 Unsupported Media Type -> 서버가 요청 본문(Content-Type) 형식을 처리할 수 없다는 것을 의미
@RequestBody가 JSON 데이터를 요구하는데, 요청이 application/x-www-form-urlencoded로 들어왔기 때문입니다.
{
"status": 415,
"message": "Content-Type 'application/x-www-form-urlencoded;charset=UTF-8' is not supported",
"timestamp": "2025-01-07T14:30:20.9240559+09:00",
"validErrors": null
}
- 주요 원인 : @RequestBody는 기본적으로 JSON을 사용하며, application/x-www-form-urlencoded와 호환되지 않습니다.
- 해결 : application/x-www-form-urlencoded을 받으려면 @RequestBody 대신 @RequestParam을 사용
@RequiredArgsConstructor
@RestController
@RequestMapping("/google")
public class GoogleAuthController {
@PostMapping("/token")
public ResponseEntity<String> getGoogleToken(
// application/x-www-form-urlencoded을 받으려면 @RequestBody 대신 @RequestParam을 사용
@RequestParam("code") String code,
@RequestParam("client_id") String clientId,
@RequestParam("client_secret") String clientSecret,
@RequestParam("redirect_uri") String redirectUri,
@RequestParam("grant_type") String grantType) {
String tokenUrl = "https://oauth2.googleapis.com/token";
// 요청 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 요청 본문 설정 (Form-Encoded)
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("code", code);
body.add("client_id", clientId);
body.add("client_secret", clientSecret);
body.add("redirect_uri", redirectUri);
body.add("grant_type", grantType);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
try {
String response = restTemplate.exchange(tokenUrl, HttpMethod.POST, entity, String.class).getBody();
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Failed to get token: " + e.getMessage());
}
}
}
- 문제 : "error_description": "malformed auth code."
Google 계정 프로필 정보 요청하기(GoogleClient)
다음으로, 응답받은 액세스 토큰을 가지고 사용자의 계정 정보를 조회하는 로직을 구현해봅시다.
요청 url은 https://www.googleapis.com/userinfo/v2/me이며, HTTP 메서드는 GET입니다.
GET https://www.googleapis.com/userinfo/v2/me?access_token=ya29.a0AfB_byDdUDtF6mbOwRnwCRF-qk
GET https://www.googleapis.com/userinfo/v2/me
Authorization: Bearer ya29.a0AfB_byDdUDtF6mbOwRnwCRF-qkv34BVNLS4Dh7AmVhVDkxXepmjiMpgDuPvKsdQHpWT9p_QTB2_ao5rfYg0C-jszCnQTB9TIMeUGEuIETNgKWxn3hIZNUyxF5aDyTpQKVbpz-_BwXeAFtH3XRZHE4BDsTjRukbPvu5ecaCgYKAd0SARMSFQHGX2MiUb3h2ERMghLbiu-Q5NBMfQ0171
여기서 위의 두가지 방법 중 무엇을 사용하든 구글 서버는 같은 응답을 반환합니다. 액세스 토큰을 쿼리스트링으로 노출하는 것보다는 헤더에 숨기는 편이 보안적으로 더 안전하므로 후자의 방식으로 구현했습니다.
응답 데이터의 형식은 다음과 같습니다.
HTTP/1.1 200 OK
{
"id": "116505574185744123267",
"email": "songsy405@gmail.com",
"verified_email": true,
"name": "타아",
"given_name": "아",
"family_name": "타",
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLdvrJklZkJcigN8MaWaa539DitRC-Df0FXCNgNxqsRcw=s96-c",
"locale": "ko"
}
이제 구현 코드를 봅시다.
private String requestGoogleAccountProfile(final String accessToken) {
String url = "https://www.googleapis.com/userinfo/v2/me";
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
try{
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
System.out.println("------------------- profile : " + response.getBody());
return response.getBody();
}catch (Exception e){
throw new RuntimeException("구글 사용자 정보를 가져오는데 실패했습니다." + e.getMessage());
}
}
서비스 로그인 Access Token 발급(JwtTokenGenerator)
다음으로, 이렇게 받은 구글 계정 정보를 토대로 회원 정보 값을 셋팅하여 새로운 회원을 등록하거나(회원가입), 기존에 존재하는 회원 중 정보가 일치하는 회원을 찾아 해당 회원의 로그인 Access Token을 발급해주어야 합니다.
두레 서비스에서는 JWT(Json Web Token)을 발급할 것이기 때문에, JWT에 대해 간략히 알아보도록 하겠습니다.
배포 후
구글 클라우드 설정 변경
잘못된 리디렉션: 이 앱은 게시 상태가 '프로덕션 단계'입니다. URI에서 https://를 스키마로 사용해야 합니다.
=> "리디렉션 URI는 일반 HTTP가 아닌 HTTPS 체계를 사용해야 합니다. 로컬 호스트 URI (localhost IP 주소 URI 포함)는 이 규칙에서 예외입니다." 라고 쓰여져 있다..
https://developers.google.com/identity/protocols/oauth2/web-server?hl=ko#httprest_6
https://developers.google.com/identity/protocols/oauth2?hl=ko
참고로 최초 구글 연동시 refresh_token은 1회 발급되고 만료기간은 6개월이다. 그 이후에는 refresh_token이 만료되기 전까지 access token만 발급된다.
출처: https://1two13.tistory.com/entry/Google-OAuth-20-로그인-구현하기 [희스토리:티스토리]
https://jinudmjournal.tistory.com/161
참고링크
'공부 > Spring' 카테고리의 다른 글
서버사이드 렌더링, 클라이언트 사이드 렌더링 (0) | 2024.11.25 |
---|---|
웹 어플리케이션의 의해 (1) | 2024.11.25 |
@JsonNaming (0) | 2024.11.20 |
@NotFound (0) | 2024.11.20 |
JPA 연관관계 정리 (0) | 2024.07.26 |