시작하기
처음 시작할 때 구글 소셜 로그인 관련된 사이트를 못 찾는 경우가 많다.
구글 클라우드 사이트에 들어간다.
사이트 상단 바를 보면 '콘솔'이라는 문구가 보인다. 클릭.
구글 소셜 로그인을 이용할 때 'API 및 서비스'을 자주 들어갈 것이다.
초기에 'OAuth 동의 화면' 설정과 같은 내용들은 다른 블로그들에 잘 설명되어 있으니 참고하면 된다.
위 링크는 구글 공식 참고 문서
한 번 쭉 읽어보는걸 추천
리디렉션 URI
리디렉션 URI, 리다이렉트 URI...
초기 설정을 하다 보면 리디렉션 URI를 입력하라는 칸이 있다.
처음에는 구글링하면서 막연하게 따라만 하다보니 리디렉션 URI( 카카오 소셜 로그인에서는 Redirect URI ) 가 뭘 의미하는지 이해하지 못한채 블로그들에 적혀있는대로 무작정 따라하기만 했다.
리디렉션 URI는 말 그대로 Google의 인증을 받은 후 다시 연결되는 페이지를 의미한다.
설정으로 입력해 둔 경로로 페이지가 이동되며, 경로 뒤에 GET 방식과 비슷하게 승인 코드가 추가된다.
아래에서 예시로 더 자세하게 설명해보겠다.
OAuth란 ?
현재 사용중인 서비스가, 서비스를 이용하는 사용자의 타 소셜 정보에 접근하기위해 권한을 타 소셜로 부터 위임받는 것.
구글, 네이버, 카카오와 같은 다양한 소셜 플랫폼에 접근하도록 제 3자 클라이언트(우리의 서비스)가 접근 권한을 위임받을 수 있는 표준 프로토콜이다.
이 때 구글, 카카오 등의 소셜 플랫폼이 서비스에게 내 아이디와 비밀번호 계정정보를 그대로 제공하는 것이 아니라, AccessToken의 형태로 발급해준다. 그대로 발급하면 보안상 문제가 발생하니까.
OAuth 와 관련한 역할 및 용어
- Resource Owner:
내 서비스를 사용할 사용자. 이 서비스 사용자들은 구글과 같은 소셜 플랫폼에서 리소스를 가지고 있는 사용자. - Resource Server:
리소스 제공자(구글, 카카오, 네이버)로, 데이터를 보유하고 있는 서버를 의미. - Authorization Server:
Resource Owner를 인증하고, 내 서비스(클라이언트)한테 토큰을 발급해주는 서버. - Client(내가 구현하고 있는 서비스):
내 서비스, 내가 구현하고 있는 애플리케이션을 의미.
Resource Server의 리소스를 이용하고자 하는 서비스.
내가 개발하고 있는 서비스를 클라이언트라고 함.
구글 소셜 로그인 흐름
간단한 흐름은 다음과 같다
- 웹에서 구글 로그인 버튼 클릭
- 구글 계정 선택하는 구글 로그인 페이지로 이동
- 로그인 후 확인 버튼 클릭
- 웹으로 리다이렉션 되고 로그인 성공
구체적인 흐름은 다음과 같다.
- Authorization Code(인가코드)를 받기 위해 구글 로그인창으로 접속한다.
클라이언트(Django)가 웹 브라우저를 통해 사용자를 구글 서버(권한 부여 서버)로 넘겨주고 권한 승인 흐름을 시작하는 부분이다. - 구글 서버(권한 부여 서버)는 사용자를 인증하고 사용자의 동의를 구한다. (구글 로그인은 이러한 과정이 없지만 카카오 로그인을 생각해보면 무엇인가를 동의하는 부분이 있다.)
이때 로그인 시 어떤 정보를 구글 서버로부터 받을 지는 django 서버 내부에서 scope 파라미터로 정해줄 수 있다. (구글 공식 문서 확인). scope를 지정해주면 해당 정보까지 받을 수 있는 접근토큰을 받을 수 있다. - 클라이언트(django)가 인증을 요청하고(-> 계정 버튼을 눌러 로그인 시도) 로그인에 성공 시, 구글 서버는 우리가 지정해준 리디렉션 URI로 접근 토큰을 받을 수 있는 인가 코드를 전송한다.
인가 코드는 브라우저의 URI 쿼리를 통해 전달되는데, 악의적인 javascript 코드가 인증 코드를 가로챌 수 있으므로 짧은 시간에 걸쳐 단 한 번만 허용된다. - URI에서 추출한 인증 코드를 사용해 구글 서버에 접근 토큰(access token)을 요청한다.(POST)
이때 GOOGLE_OAUTH2_CLIENT_ID 와 GOOGLE_OAUTH2_CLIENT_SECRET 을 함께 전송한다. (CLIENT_ID, CLIENT_SECRET) - Access Token을 받은 후 유저 정보를 가지고 올 수 있는 URL로 파라미터에 Access Token을 담아서 GET요청을 보낸다.
자세히 보면 아래와 같다.
사용자는 소셜 로그인 버튼을 클릭한다.
제공된 로그인 페이지에서 로그인을 시도한다.
로그인 성공 시 설정해놓은 리디렉션 URI로 이동되고 Authorization Code(인가 코드)를 발급받는다.
해당 코드는 설정해놓은 리디렉션 URI 뒤에 'code='으로 붙는다.
이렇게 주어진 인가 코드를 이용하여 구글에 AccessToken을 달라고 요청하면 된다.
여기까지가 사용자에게 보여지는 화면들인데 뒷 사정은 조금 복잡할 수 있다.
일단 여기까지 흐름을 이해해보자
Authorization Code(인가 코드) 받기
위 로그인 창으로 연결되게 하는 URL.
구글 소셜 로그인 버튼에 연결해두면 된다.
구글 로그인창을 호출하는 URL은 https://accounts.google.com/o/oauth2/v2/auth 이다.
필수로 추가해야하는 파라미터는 response_type, client_id, redirect_uri, state 총 4개.
공식 문서를 보면 자세하게 설명되어있다.
- client_id: 구글 개발자센터에서 발급받은 Key
- redirect_uri: 구글 개발자센터에서 설정한 리디렉션 URI
- response_type: code로 고정 (인가코드를 통한 로그인 방식)
- scope: 토큰 발급 이후 유저 정보에서 어떤 항목을 조회할 것인가를 띄어쓰기를 구분.
https://accounts.google.com/o/oauth2/v2/auth?client_id="클라이언트ID 입력"&redirect_uri="설정해둔 redirect uri 입력"&response_type=code&scope=email profile
로그인 성공 후 응답받은 URL
'type=google'은 소셜 로그인 플랫폼 별로 개발하기 위해서 내가 설정해놓은 코드(원래는 없음)
http://localhost:5500/signin.html?type=google&code=4%2F0AfJohXmjjibWEnCnAI_HE6N9JY-e2Z0fa7N-mBxRPNVPzluwFYmAWy5Jd_304eoTWbBw&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%1F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=consent
AccessToken 얻기
구글 인증 서버에서 로그인 토큰을 가져온다.
요청 URI는 https://oauth2.googleapis.com/token 이다.
로그인 토큰을 가져오기 위해 아래 정보를 Body에 담아 요청한다.
- code : 클라이언트 페이지에서 얻은 인가 코드를 사용 하기 때문에 "code"로 고정.
- client_id : 구글 개발자센터에서 발급 받은 Client ID
- client_secret : 구글 개발자센터에서 발급 받은 Client Secret
- redirect_uri : 구글 개발자센터에서 등록한 redirect_uri
- grant_type: 'authorization_code' 로 고정 (인가코드를 통한 로그인 방식)
AccessToken을 얻으려고 포스트맨으로 여러번 시도 했는데
{ "error": "invalid_grant", "error_description": "Malformed auth code." } 이런 에러가 떠서 포기하고 vscode로 print 해서 찍어보았다.
{
'access_token': 'ya29.a0AfB_byDraH4Lovc3Z9RbG4czQ3NPJxv233MaIW2ykDzvK3TqnxiSzqau0HSSJ_LFtU9YoT0yGhnI1JdPqhaT-NVMocP4lA4gOPFMI10eCbW1XuxgTVdgclXgF1J2NjdqgUgi2GxUUv-R4eQwD3tSmIuqr9XdbbGlb24vaCgYKAfESARESFQHGX2Mitz-7e2R92i3hpqxBlaaDEg0171',
'expires_in': 3599,
'scope': 'openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
'token_type': 'Bearer',
'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmNDBmMGE4ZWYzZDg4MDk3OGRjODJmMjVjM2VjMzE3YzZhNWI3ODEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0OTIyNTQwNzYzNzctNHZpYWZpdGQwdnFhcjBpbjFmMGFwbDVwbHAyNjdsOGouYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0OTIyNTQwNzYzNzctNHZpYWZpdGQwdnFhcjBpbjFmMGFwbDVwbHAyNjdsOGouYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDk4NTMzMjAxNTU5NjQ3Mjc3MjAiLCJlbWFpbCI6InNodXI5MjMwQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoicjhGSDRIMnBSLTlqb3J5YjRqaTN4ZyIsIm5hbWUiOiJTaCIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJV1Yydk9SMmlhWGo2WDdjSnJNWS1rdVU2X2lnYm10dGpfWDNPalRNcFEzUT1zOTYtYyIsImdpdmVuX25hbWUiOiJTaCIsImxvY2FsZSI6ImtvIiwiaWF0IjoxNzA1NDgzMDYyLCJleHAiOjE3MDU0ODY2NjJ9.R0UNq5DsbNEmRNRb9UCprMDYQax9WmdYFhHymzyX3iWsJOYIY6apJwFfd0UpuydaHs41fctciWqK0AuCT_Y0_PSV3b-WQIWgAaktERBbVXIn9L71oT7yARu_t0Hd_LLc2MJiMlIP6B370M__nqS-ZmiD_9fK4-ZtMLryJlOJPr9nitEcXSZ9WCe8hvPyEJTNMulTZ52RYoJSFi09y30L6vhYJ07wEit8Ia9CDiLLtNI7v2z7C1436g2lrhHLfm9x00B67PzQDcQyjLSONkCgZTF16FrlxEkINPD5fl5ifWmfwTJFuhk9kqbEzPuzkqAi-g0ObFEOultTYzWM3f8Eog'
}
정상적으로 받았을 때 위와 같은 응답이 온다.
- access_token: AccessToken 값
- scope: 조회하고자 하는 사용자 정보
- token_type: 토큰 타입, Bearer로 고정
- expires_in: Access Token 만료시간(초)
유저 정보 가져오기
위 과정에서 발급 받은 Access Token으로 유저 정보를 가져올 수 있다.
요청 URI는 https://www.googleapis.com/oauth2/v1/userinfo
user_info_response.json() 으로 받아온 사용자 정보를 보면 아래와 같다
{
'id': '786853312155964712345',
'email': 'example@gmail.com',
'verified_email': True,
'name': 'example',
'given_name': 'example',
'picture': 'https://lh3.googleusercontent.com/a/CS8ocIdfavewR2iaXj6X7cJrMY-kuU6_igbmttj_X113OjTMpQ3Q=s96-c',
'locale': 'ko'
}
view
@csrf_exempt
@require_POST
def google_login(request):
CLIENT_ID = get_env_variable("GOOGLE_CLIENT_ID")
CLIENT_SECRET = get_env_variable("GOOGLE_CLIENT_SECRET")
# 클라이언트에서 받은 인가 코드
json_data = json.loads(request.body.decode("utf-8"))
authorization_code = json_data.get("code")
# 로그인 토큰을 얻기 위한 요청 설정
token_url = "https://oauth2.googleapis.com/token"
token_payload = {
"code": authorization_code,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": "http://localhost:5500/signin.html?type=google",
"grant_type": "authorization_code",
}
# 로그인 토큰 요청
token_response = requests.post(token_url, data=token_payload)
if token_response.status_code == 200:
access_token = token_response.json().get("access_token")
# 구글 사용자 정보 요청
user_info_url = "https://www.googleapis.com/oauth2/v1/userinfo"
headers = {
"Authorization": f"Bearer {access_token}",
}
user_info_response = requests.get(
user_info_url, headers=headers
)
if user_info_response.status_code == 200:
google_id = user_info_response.json()["id"]
if not google_id:
response_data = {"success": False, "message": "구글 계정을 받아오지 못했습니다."}
return JsonResponse(response_data, status=400)
if not models.Users.objects.filter(
google_id=google_id, user_status=0
).exists():
response_data = {
"success": True,
"message": "not exists",
"data": {"id": google_id},
}
return JsonResponse(response_data, status=200)
# jwt 토큰 프론트로 전달
user = models.Users.objects.get(google_id=google_id)
token = create_token(user.user_id)
response_data = {
"success": True,
"message": "exists",
"data": {"token": token},
}
return JsonResponse(response_data, status=200)
else:
return JsonResponse(
{"success": False, "message": "Failed to fetch user info from Google"},
status=500,
)
else:
return JsonResponse(
{"success": False, "message": "access token을 받아오는데 실패했습니다."},
status=500,
json_dumps_params={"ensure_ascii": False},
)
js
//calllback으로 받은 인가코드 및 아이디
const code = new URL(window.location.href).searchParams.get('code');
const type = new URL(window.location.href).searchParams.get('type');
const selectId = new URL(window.location.href).searchParams.get('id'); // 아이디 찾기에서 넘어온 경우
// 소셜 로그인
const socialLogin = async(type) => {
axios({
url: `http://localhost:8000/account/${type}Login/`,
method: 'post',
data: {code: code},
})
.then(response => {
console.log('성공:', response.data); // 로그에 응답 데이터를 찍습니다.
// 2-1. 로그인 성공 (로그인 후 프롬프트 페이지로 이동)
if (response.data.message == "exists") {
// localStroage에 토큰(Token)을 적재한다.
const accessToken = response.data.data.token;
setWithExpire('accessToken', accessToken, 12*60*60*1000); //12 시간
// index.html로 Routing 한다.
window.location.href = '../index.html';
} else {
// 2-2. 로그인 실패 (모달창 띄우기)
modal.style.display = 'block';
document.getElementById("socialType").value = type;
document.getElementById("socialId").value = response.data.data.id;
document.getElementById("signUpBtn").setAttribute("onclick", `location.href='${signUpURL}?type=${type}&socialId=${response.data.data.id}'`);
}
})
.catch(error => {
console.log(error);
errorLogin.style.display = 'inline-block';
errorLogin.textContent = '로그인에 실패했습니다. 다시 시도해주세요.';
});
}
jwt로 인증하는 방식이라
jwt도 포스팅해서 정리해야겠다.
'Django' 카테고리의 다른 글
아나콘다(Anaconda) 가상환경 만들기 (0) | 2024.01.05 |
---|---|
Model 생성 관련 (0) | 2023.12.20 |
Django Start (0) | 2023.12.20 |
댓글