[DRF] 구글 소셜로그인 (JWT)

김재연·2022년 7월 18일
3

Watti

목록 보기
2/10
post-thumbnail

DRF + jwt + 소셜로그인

1. 초기 환경 세팅

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

dj-rest-auth는 업데이트가 중단된 django-rest-auth 대신 사용하는 패키지로, 회원가입과 로그인, 소셜로그인 기능을 제공해준다. 추가적으로 비밀번호 찾기/리셋, 회원가입 시 이메일 인증 등의 유저 관련 기능들을 커버한다고 한다. django-allauth는 rest-auth가 의존하는 라이브러리인데, 소셜로그인을 쓰기 위해서 꼭 쓰는가보다.

2. settings.py

# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # site 설정도!
    'django.contrib.sites',

    # 생성한 앱
    'user',

    # 설치한 라이브러리
    'rest_framework',
    'rest_framework.authtoken',
    'rest_framework_simplejwt',
    'dj_rest_auth',
    'dj_rest_auth.registration',

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
]

# 사이트는 1개만 사용할 것이라고 명시
SITE_ID = 1

AUTH_USER_MODEL = 'user.User'

REST_USE_JWT = True

ACCOUNT_USER_MODEL_USERNAME_FIELD = None # username 필드 사용 x
ACCOUNT_EMAIL_REQUIRED = True            # email 필드 사용 o
ACCOUNT_USERNAME_REQUIRED = False        # username 필드 사용 x
ACCOUNT_AUTHENTICATION_METHOD = 'email'

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

설치한/필요한 라이브러리들을 INSTALLED_APPS에 등록해주고, allauth.socialaccount.providers.소셜로그인제공업체 에 google 외에도 소셜로그인을 제공하는 업체인 카카오, 네이버 등을 추가할 수 있다.

그리고 기존의 username 필드가 있던 User 모델에서 email만 사용하도록 커스터마이징했기 때문에 ACCOUNT_@@ 관련 설정들을 조금 해줘야 한다.

❗그리고 나중에 dj_rest_auth.registration.views.SocialLoginView을 쓰려면 REST_USE_JWT = True 도 꼭 추가해줘야 한다.❗

3. 라이브러리 url 매핑 & 커스텀유저 만들기

# urls.py (project)

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/user/', include('allauth.urls')),
    path('api/user/', include('user.urls')),
]

settings.py에서 추가한 라이브러리들과 커스텀 유저 모델을 만든 앱에 url을 매핑한다.

커스텀 유저 만드는 건 이전 포스팅 참고

4. site & social application 설정 (admin)

admin 페이지에서 site 설정을 example.com에서 localhost:8000로 바꿔주고

social application을 등록한다.

5. 구글 client id & secret key 발급받기

그리고 여기서 client id와 secret key를 발급받아야 하는데, 자세한 과정은 아래 레퍼런스로 달아둔 다른 포스팅 참고. 간단하게만 요약하자면 새 프로젝트를 만들어서 OAuth 동의화면에서 앱 기본 정보(앱이름, 개발자이메일 등)를 입력하고, 사용자 인증 정보에 들어가서 callback URI를 지정해준다. 주의할 점은

승인된 리디렉션 URI에 /도 무시하면 안된다.

암튼 이렇게 받아온 client id와 secret key를 admin의 social application 해당 필드에 넣어주고, env 파일에도 넣어준다. (참고: 환경변수 숨기기)

# .env
SOCIAL_AUTH_GOOGLE_CLIENT_ID = "973...중략...com"
SOCIAL_AUTH_GOOGLE_SECRET = "GOC...중략...ZhZ"
STATE = "vyv...중략...2dj"

state에는 그냥 아무 랜덤 문자열이나 넣었다. (용도가 뭐지)

6. urls.py

# urls.py

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

url들을 등록해주고

7. views.py

함수들을 작성한다.

1) 변수 설정

# views.py
import os

# 구글 소셜로그인 변수 설정
state = os.environ.get("STATE")
BASE_URL = 'http://localhost:8000/'
GOOGLE_CALLBACK_URI = BASE_URL + 'api/user/google/callback/'

❗ 아까 승인된 리디렉션 URI를 /api/user/google/callback/로 썼으니 여기서도 무조건 api/user/google/callback/로 똑같이 써줘야 한다. / 빼먹으면 오류 난다 ❗

2) 구글 로그인

# views.py
from django.shortcuts import redirect
import os

# 구글 로그인
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}")

이 url로 들어가면 구글 로그인 창이 뜨고, 알맞은 아이디와 비밀번호를 입력하면 callback URI로 코드값이 들어간다.

3) ⭐access token & 이메일 요청 -> 회원가입/로그인 & jwt 발급⭐

# views.py
from json import JSONDecodeError
from django.http import JsonResponse
import requests
import os
from rest_framework import status
from .models import *
from allauth.socialaccount.models import SocialAccount

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')

    # 1. 받은 코드로 구글에 access token 요청
    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}")
    
    ### 1-1. json으로 변환 & 에러 부분 파싱
    token_req_json = token_req.json()
    error = token_req_json.get("error")

    ### 1-2. 에러 발생 시 종료
    if error is not None:
        raise JSONDecodeError(error)

    ### 1-3. 성공 시 access_token 가져오기
    access_token = token_req_json.get('access_token')

    #################################################################

    # 2. 가져온 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

    ### 2-1. 에러 발생 시 400 에러 반환
    if email_req_status != 200:
        return JsonResponse({'err_msg': 'failed to get email'}, status=status.HTTP_400_BAD_REQUEST)
    
    ### 2-2. 성공 시 이메일 가져오기
    email_req_json = email_req.json()
    email = email_req_json.get('email')

    # return JsonResponse({'access': access_token, 'email':email})

    #################################################################

    # 3. 전달받은 이메일, access_token, code를 바탕으로 회원가입/로그인
    try:
        # 전달받은 이메일로 등록된 유저가 있는지 탐색
        user = User.objects.get(email=email)

        # FK로 연결되어 있는 socialaccount 테이블에서 해당 이메일의 유저가 있는지 확인
        social_user = SocialAccount.objects.get(user=user)

        # 있는데 구글계정이 아니어도 에러
        if social_user.provider != 'google':
            return JsonResponse({'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST)

        # 이미 Google로 제대로 가입된 유저 => 로그인 & 해당 우저의 jwt 발급
        data = {'access_token': access_token, 'code': code}
        accept = requests.post(f"{BASE_URL}api/user/google/login/finish/", data=data)
        accept_status = accept.status_code

        # 뭔가 중간에 문제가 생기면 에러
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status)

        accept_json = accept.json()
        accept_json.pop('user', None)
        return JsonResponse(accept_json)

    except User.DoesNotExist:
        # 전달받은 이메일로 기존에 가입된 유저가 아예 없으면 => 새로 회원가입 & 해당 유저의 jwt 발급
        data = {'access_token': access_token, 'code': code}
        accept = requests.post(f"{BASE_URL}api/user/google/login/finish/", data=data)
        accept_status = accept.status_code

        # 뭔가 중간에 문제가 생기면 에러
        if accept_status != 200:
            return JsonResponse({'err_msg': 'failed to signup'}, status=accept_status)

        accept_json = accept.json()
        accept_json.pop('user', None)
        return JsonResponse(accept_json)
        
	except SocialAccount.DoesNotExist:
    	# User는 있는데 SocialAccount가 없을 때 (=일반회원으로 가입된 이메일일때)
        return JsonResponse({'err_msg': 'email exists but not social user'}, status=status.HTTP_400_BAD_REQUEST)

받은 code로 구글에 access token을 요청하고, 응답받은 access token으로 로그인된 사용자의 이메일 값을 구글에 요청한다. 성공적으로 이메일값을 받았으면 해당 이메일과 access token, code를 바탕으로 회원가입과 로그인을 진행한다. jwt 토큰도 성공적으로 나오는데, 이 코드의 어느 부분에서 발급되고 있는건지는 잘 모르겠다.

🐞 2022-07-25 오류발견
일반회원으로 가입된 회원과 동일한 이메일로 소셜로그인을 하려고 할 때, SocialAccount에서 DoesNotExist에러가 발생했다. 그래서 기존 코드에서 social_user가 없을때 에러 처리를 했던 코드를 except로 빼줬다.

4) 구글 소셜로그인뷰

# views.py
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

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

8. 테스트

127.0.0.1:8000/api/user/google/login으로 접속하면

이 페이지로 리다이렉션된다. 로그인할 계정을 선택하면

이렇게 jwt 토큰이 발급된다.

디비를 확인해보면

user 테이블에도 있고

socialaccount 테이블에도 있다.

여기서 발급된 access token을 가지고도

Authentication이 잘 된다. 근데 쿠키에 저장은 못했다.


Reference

Django-Rest-Framework(DRF)로 소셜 로그인 API 구현해보기(Google, KaKao, Github)

profile
일기장같은 공부기록📝

2개의 댓글

comment-user-thumbnail
2022년 12월 14일

안녕하세요 이 글을 보고 소셜로그인에 정말 많은 도움이 됐습니다.
제가 커스텀유저에 nickname필드를 추가했는데 소셜로그인할 때 nickname필드에 이메일을 저장하려고 여러가지 시도를 해봤는데 다 실패해서 혹시 어느 부분을 수정해야하는지 알수있을까요?

답글 달기
comment-user-thumbnail
2023년 3월 14일

안녕하세요 코딩 배우고 있는 학생인데 혹시 궁금한거 개인적으로 여쭤봐도 될까요...?

답글 달기