[Django] 구글 소셜로그인

윤찬효·2023년 6월 12일
0

장고 프로젝트 중 소셜 로그인을 구현해보았다.
토큰 형식의 로그인 방식을 시도하다가 토큰을 받아오는 건 실패하여 현재 진행된 사항까지만 작성 예정이다.


준비하기

소셜로그인 연동 코드 작성 전 구글의 client_idsecret_key가 필요하여 준비 과정이 필요하다.

Google Cloud에 접속하여 새로운 프로젝트를 생성하면 된다.

화면에서 빨간색 부분을 클릭하면 프로젝트를 생성, 선택할 수 있는 창이 나오며 주황색 네모칸을 눌러 프로젝트를 만든다.

그 후 상단에 "+ 사용자 인증 정보 만들기" 👉 "OAuth 클라이언트 ID" 클릭

애플리케이션 유형은 "웹 애플리케이션"으로 선택

이름은 원하는대로 지정하면 되고, 승인된 자바스크립트 원본에 "http://127.0.0.1:800" URI를 추가하면 된다.

승인된 리디렉션 URI에는 구글 로그인 이후 콜백이 되는 URI를 작성하면 되는데
"http://127.0.0.1:8000/users/google/callback/"으로 api를 진행할 예정이라 미리 작성해두었다.
( 아래 작성된 urls.py 확인시 조금 더 명확하게 알 수 있다. )

위의 화면에 오른편에 클라이언트 ID와 클라이언트 보안 비밀번호는 .env에 넣어두어야 하기에 메모해둬야 한다.


코드 작성

준비를 마쳤으니 코드를 작성해야 한다.

1. 패키지 설치

poetry add django-allauth=0.51.0
poetry add dj-rest-auth

allauth 패키지의 경우 0.51 버전으로 받아야 정상적으로 진행이 가능하다.

pip install django-allauth=0.51.0
pip install dj-rest-auth

poetry가 아닌 pip를 사용할 경우 위의 명령어를 입력한다.

2. 프로젝트 세팅하기

# config/settings.py

INSTALLED_APPS = [
	...
    "rest_framework",
    "rest_framework_simplejwt",
    ...
    # 생성한 앱
    "users",
	...
    # 추가할 내용들
    "django.contrib.sites",
    "rest_framework.authtoken",
    "dj_rest_auth",
    "dj_rest_auth.registration",
    "allauth",
    "allauth.account",
    "allauth.socialaccount",
    "allauth.socialaccount.providers.google", # 구글 소셜
]

# 커스텀 유저 모델 사용
AUTH_USER_MODEL = "users.User"

# 사용할 사이트 갯수
SITE_ID = 1

REST_USE_JWT = True

ACCOUNT_USER_MODEL_USERNAME_FIELD = "username" # 유저네임필드 이름
ACCOUNT_EMAIL_REQUIRED = True # 이메일 사용여부
ACCOUNT_USERNAME_REQUIRED = True # 유저네임 사용여부
ACCOUNT_AUTHENTICATION_METHOD = "email" # 인증 필드 메소드

settings.py에 위의 내용을 입력한다.

# config/urls.py
...
urlpatterns = [
    path("admin/", admin.site.urls),
    path("users/", include("users.urls")),
    path("users/", include("allauth.urls")),
]
...

urls.py에 위의 내용을 입력한다.

모두 완료한 뒤에 makemigrations, migrate를 해준다.

3. admin 페이지 세팅

모두 완료되면 py manage.py runserver를 해준다.
( 그 전에 createsuperuser 안했으면 해주기 ! )

SITES 👉 sites에 접속한 뒤 위의 화면처럼 수정하고 저장한다ㅣ

그 뒤에 Social applications에서 만들어준다.

위의 화면처럼 입력한다.
아까 미리 준비한 구글 client id와 secret key를 입력한다.


소셜 로그인 api

urls.py
# users/urls.py

urlpatterns = [
    ...
    # 구글 소셜로그인
    path("google/login/", social.google_login, name="google_login"),
    path("google/callback/", social.google_callback, name="google_callback"),
    path(
        "google/login/finish/",
        social.GoogleLogin.as_view(),
        name="google_login_todjango",
    ),
    ...
]

urls.py를 연결해준다. 해당 url 중 google_callback의 경우 구글의 정보를 가지고 리다이렉션이 되는 주소이므로 잘 기억해두자.

social.py

views.py에 작성해도 되지만 기능을 나누기 위해 social.py 파일을 만들어 소셜 로그인 코드를 작성했다.

전체코드

from django.shortcuts import redirect
import os
from json import JSONDecodeError
from django.http import JsonResponse
import requests
from rest_framework import status
from allauth.socialaccount.models import SocialAccount
from users.models import User
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.socialaccount.providers.google import views as google_view
from rest_framework_simplejwt.tokens import AccessToken, RefreshToken
from rest_framework.response import Response
from dj_rest_auth.registration.serializers import SocialLoginSerializer

state = os.environ.get("STATE")
BASE_URL = "http://127.0.0.1:8000/"
GOOGLE_CALLBACK_URI = BASE_URL + "users/google/callback/"


# 구글 로그인
def google_login(request):
    scope = "https://www.googleapis.com/auth/userinfo.email"
    client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
    return redirect(
        f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&response_type=code&redirect_uri={GOOGLE_CALLBACK_URI}&scope={scope}"
    )


def google_callback(request):
    client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
    client_secret = os.environ.get("SOCIAL_AUTH_GOOGLE_SECRET")
    code = request.GET.get("code")

    token_req = requests.post(
        f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={GOOGLE_CALLBACK_URI}&state={state}"
    )

    token_req_json = token_req.json()
    error = token_req_json.get("error")

    if error is not None:
        raise JSONDecodeError(error)

    access_token = token_req_json.get("access_token")
    email_req = requests.get(
        f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}"
    )
    email_req_status = email_req.status_code

    if email_req_status != 200:
        return JsonResponse(
            {"message": "이메일을 가져오지 못했습니다."}, status=status.HTTP_400_BAD_REQUEST
        )

    email_req_json = email_req.json()
    print(email_req_json)
    email = email_req_json.get("email")

    try:
        user = User.objects.get(email=email)

        social_user = SocialAccount.objects.get(user=user)

        if social_user.provider != "google":
            return JsonResponse(
                {"message": "소셜이 일치하지 않습니다."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        data = {"access_token": access_token, "code": code}
        accept = requests.post(f"{BASE_URL}users/google/login/finish/", data=data)
        accept_status = accept.status_code

        if accept_status != 200:
            return JsonResponse({"message": "구글로그인에 실패했습니다."}, status=accept_status)

        user, created = User.objects.get_or_create(email=email)
        access_token = AccessToken.for_user(user)
        refresh_token = RefreshToken.for_user(user)

        # accept_json = accept.json()
        # accept_json.pop("user", None)
        return Response(
            {"refresh": str(refresh_token), "access": str(access_token)},
            status=status.HTTP_200_OK,
        )

    except User.DoesNotExist:
        data = {"access_token": access_token, "code": code}
        accept = requests.post(f"{BASE_URL}users/google/login/finish/", data=data)
        accept_status = accept.status_code

        if accept_status != 200:
            return JsonResponse({"message": "구글 회원가입에 실패했습니다."}, status=accept_status)

        user, created = User.objects.get_or_create(email=email)
        access_token = AccessToken.for_user(user)
        refresh_token = RefreshToken.for_user(user)
        # accept_json = accept.json()
        # accept_json.pop("user", None)

        return Response(
            {"refresh": str(refresh_token), "access": str(access_token)},
            status=status.HTTP_201_CREATED,
        )

    except SocialAccount.DoesNotExist:
        return JsonResponse(
            {"message": "소셜로그인 유저가 아닙니다."},
            status=status.HTTP_400_BAD_REQUEST,
        )


class GoogleLogin(SocialLoginView):
    adapter_class = google_view.GoogleOAuth2Adapter
    callback_url = GOOGLE_CALLBACK_URI
    client_class = OAuth2Client
    serializer_class = SocialLoginSerializer

코드별 설명

06/13 추가 작성

구글로그인

def google_login(request):
    scope = "https://www.googleapis.com/auth/userinfo.email"
    client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
    return redirect(
        f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}&response_type=code&redirect_uri={GOOGLE_CALLBACK_URI}&scope={scope}"
    )

구글 로그인으로 스코프의 정보에 대한 코드를 받아온다.
코드를 받아온 다음 "콜백URI/?가져온코드"로 이동한다.

( 코드는 매번 변경된다. )

구글 콜백

def google_callback(request):
   client_id = os.environ.get("SOCIAL_AUTH_GOOGLE_CLIENT_ID")
   client_secret = os.environ.get("SOCIAL_AUTH_GOOGLE_SECRET")
   code = request.GET.get("code")

   token_req = requests.post(
       f"https://oauth2.googleapis.com/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={GOOGLE_CALLBACK_URI}&state={state}"
   )

   token_req_json = token_req.json()
   error = token_req_json.get("error")

   if error is not None:
       raise JSONDecodeError(error)

   access_token = token_req_json.get("access_token")
   email_req = requests.get(
       f"https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token}"
   )
   email_req_status = email_req.status_code

   if email_req_status != 200:
       return JsonResponse(
           {"message": "이메일을 가져오지 못했습니다."}, status=status.HTTP_400_BAD_REQUEST
       )

   email_req_json = email_req.json()
   email = email_req_json.get("email")

받아온 code 정보로 구글에 access_token을 요청한다. ( 코드에서 token_req부분 )

만약 access_token을 받아오는 과정에서 에러가 난다면 에러를 리턴한다.
받은 access_token을 가지고 구글에 사용자 정보를 받아온다.

 try:
       user = User.objects.get(email=email)

       social_user = SocialAccount.objects.get(user=user)

       if social_user.provider != "google":
           return JsonResponse(
               {"message": "소셜이 일치하지 않습니다."},
               status=status.HTTP_400_BAD_REQUEST,
           )

       data = {"access_token": access_token, "code": code}
       accept = requests.post(f"{BASE_URL}users/google/login/finish/", data=data)
       accept_status = accept.status_code

       if accept_status != 200:
           return JsonResponse({"message": "구글로그인에 실패했습니다."}, status=accept_status)

       user, created = User.objects.get_or_create(email=email)
       access_token = AccessToken.for_user(user)
       refresh_token = RefreshToken.for_user(user)

       # accept_json = accept.json()
       # accept_json.pop("user", None)
       return Response(
           {"refresh": str(refresh_token), "access": str(access_token)},
           status=status.HTTP_200_OK,
       )

   except User.DoesNotExist:
       data = {"access_token": access_token, "code": code}
       accept = requests.post(f"{BASE_URL}users/google/login/finish/", data=data)
       accept_status = accept.status_code

       if accept_status != 200:
           return JsonResponse({"message": "구글 회원가입에 실패했습니다."}, status=accept_status)

       user, created = User.objects.get_or_create(email=email)
       access_token = AccessToken.for_user(user)
       refresh_token = RefreshToken.for_user(user)
       # accept_json = accept.json()
       # accept_json.pop("user", None)

       return Response(
           {"refresh": str(refresh_token), "access": str(access_token)},
           status=status.HTTP_201_CREATED,
       )

   except SocialAccount.DoesNotExist:
       return JsonResponse(
           {"message": "소셜로그인 유저가 아닙니다."},
           status=status.HTTP_400_BAD_REQUEST,
       )

try, except 구문을 통해서 로그인, 회원가입을 시도한다.
email을 통해 user와 social_user를 받아오고 일치여부를 확인한다.

class GoogleLogin(SocialLoginView):
   adapter_class = google_view.GoogleOAuth2Adapter
   callback_url = GOOGLE_CALLBACK_URI
   client_class = OAuth2Client

구글로그인으로 allauth 패키지를 통한 구글 소셜로그인을 완료한다.
( return 값은 key를 받아온다. )

0개의 댓글