구글 로그인
참고 링크
스프링 부트 구글 소셜 로그인 구현하기
해당 글은 아마란스💐가 작성했습니다.이번에 두레 서비스의 소셜 로그인 기능을 맡게 되어, 기능을 구현한 과정을 정리해보았습니다.구글 소셜 로그인을 구현한 다른 문서들을 참고하며 이해
velog.io
전체 흐름
클라이언트 - '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."
[Spring/SpringBoog] 구글 로그인 "error_description": "malformed auth code." 문제 해결
어..? 분명 됐었는데....?
velog.io
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에 대해 간략히 알아보도록 하겠습니다.
OAuth2 인증 흐름:
- 클라이언트(React 앱)에서 사용자가 Google 로그인 버튼을 클릭합니다. 이 때 /oauth2/authorization/google URL로 리디렉션됩니다.
- http://localhost:4000/oauth2/authorization/google (Spring Security에서 설정한 엔드포인트)에서 Google OAuth2 로그인이 시작됩니다.
- 사용자가 Google 계정에 로그인하고 동의를 하면, Google이 인증 코드를 리디렉션 URL로 전달합니다. 예를 들어, http://localhost:4000/login/oauth2/code/google로 인증 코드가 전달됩니다.
- Spring Security는 이 인증 코드를 사용하여 Google OAuth2 서버와 통신하고, 최종적으로 사용자의 정보를 가져옵니다.
- 그런 다음, loadUser() 메서드에서 이 정보를 사용하여 사용자를 처리하고 애플리케이션에 로그인된 상태로 만듭니다.
3. loadUser() 메서드의 역할
loadUser() 메서드는 Google에서 반환된 사용자 정보를 처리하는 로직입니다. 이 메서드는 Spring Security에서 OAuth2 인증 과정에서 호출됩니다.
- OAuth2UserRequest userRequest: Google에서 받은 사용자 정보와 관련된 요청을 처리합니다. 이 객체는 클라이언트의 등록된 정보와 Google 서버에서 반환된 정보를 포함합니다.
- super.loadUser(userRequest): 이 부분은 상위 클래스인 DefaultOAuth2UserService의 메서드를 호출하여 실제로 Google로부터 사용자 정보를 로드하는 부분입니다.
- OAuthAttributes.of(registrationId, originAttributes): Google에서 가져온 사용자 정보를 OAuthAttributes로 변환합니다. 여기서 registrationId는 google로 설정되어 Google OAuth 인증을 처리하는데 필요한 속성입니다.
- getOrSave(attributes): 이 메서드는 사용자가 이미 존재하는지 확인하고, 존재하지 않으면 새로운 사용자 정보를 저장합니다.
- OAuth2AuthenticationException: 만약 사용자가 존재하지 않으면 예외를 던져서 인증을 실패시킵니다.
배포 후
구글 클라우드 설정 변경
잘못된 리디렉션: 이 앱은 게시 상태가 '프로덕션 단계'입니다. URI에서 https://를 스키마로 사용해야 합니다.
=> "리디렉션 URI는 일반 HTTP가 아닌 HTTPS 체계를 사용해야 합니다. 로컬 호스트 URI (localhost IP 주소 URI 포함)는 이 규칙에서 예외입니다." 라고 쓰여져 있다..
https://developers.google.com/identity/protocols/oauth2/web-server?hl=ko#httprest_6
웹 서버 애플리케이션용 OAuth 2.0 사용 | Authorization | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 웹 서버 애플리케이션용 OAuth 2.0 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이
developers.google.com
진짜 배포! 후
위에서
잘못된 리디렉션이라는 에러가 발생했었는데, 배포 후에는 잘됐다.
근데 localhost로 했을 때는 이렇게 리디렉션을 알아서 자동으로! 해주었다..
http://localhost:4000/login/oauth2/code/google
근데 진짜 배포하고 나서 문제가 발생했다.
리디렉션이 이런 식으로 가는 것이다!!!!
http://배포도메인:4000/login/oauth2/code/google
local일 때는 승인된 리디렌션 URL에 등록만 해주면 되었다.
하지만 실제 배포하고 나서는... 이렇게 따로 지정해두어야 내가 원하는 리디렉션 URI로 간다....
spring.security.oauth2.client.registration.google.redirect-uri=https://배포한도메인r:4000/login/oauth2/code/google
https://okky.kr/questions/486534
OAuth시 redirect url의 역할 질문. | OKKY Q&A
진행중인 프로젝트에 소셜로그인을 적용하려고 하고있습니다. 구글에 앱을 등록하려고 하니 redirect url을 등록해야하더군요. 제가 여기저기 검색을 해본 결과 구글에서 저의 앱에 code(access token
okky.kr
https://developers.google.com/identity/protocols/oauth2?hl=ko
OAuth 2.0을 사용하여 Google API에 액세스하기 | Authorization | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 의견 보내기 OAuth 2.0을 사용하여 Google API에 액세스하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.
developers.google.com
참고로 최초 구글 연동시 refresh_token은 1회 발급되고 만료기간은 6개월이다. 그 이후에는 refresh_token이 만료되기 전까지 access token만 발급된다.
출처: https://1two13.tistory.com/entry/Google-OAuth-20-로그인-구현하기 [희스토리:티스토리]
https://jinudmjournal.tistory.com/161
[구글 로그인] "error_description": "Malformed auth code." 문제 해결
문제 상황 400 Bad Request: "{ "error": "invalid_grant", "error_description": "Malformed auth code."}" 구글 로그인 구현 중에 위와 같은 에러가 발생했습니다. 구글링을 열심히 해봤지만 해당 문구와 관련된 자료가
jinudmjournal.tistory.com
[Spring/SpringBoog] 구글 로그인 "error_description": "malformed auth code." 문제 해결
어..? 분명 됐었는데....?
velog.io
참고링크
'공부 > Spring' 카테고리의 다른 글
서버사이드 렌더링, 클라이언트 사이드 렌더링 (0) | 2024.11.25 |
---|---|
웹 어플리케이션의 의해 (1) | 2024.11.25 |
@JsonNaming (0) | 2024.11.20 |
@NotFound (0) | 2024.11.20 |
JPA 연관관계 정리 (0) | 2024.07.26 |