DRF + jwt + 소셜로그인
pip install dj-rest-auth
pip install django-allauth
dj-rest-auth는 업데이트가 중단된 django-rest-auth 대신 사용하는 패키지로, 회원가입과 로그인, 소셜로그인 기능을 제공해준다. 추가적으로 비밀번호 찾기/리셋, 회원가입 시 이메일 인증 등의 유저 관련 기능들을 커버한다고 한다. django-allauth는 rest-auth가 의존하는 라이브러리인데, 소셜로그인을 쓰기 위해서 꼭 쓰는가보다.
# 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
도 꼭 추가해줘야 한다.❗
# 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을 매핑한다.
admin 페이지에서 site 설정을 example.com
에서 localhost:8000
로 바꿔주고
social application을 등록한다.
그리고 여기서 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에는 그냥 아무 랜덤 문자열이나 넣었다. (용도가 뭐지)
# 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들을 등록해주고
함수들을 작성한다.
# 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/
로 똑같이 써줘야 한다. /
빼먹으면 오류 난다 ❗
# 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로 코드값이 들어간다.
# 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
로 빼줬다.
# 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
127.0.0.1:8000/api/user/google/login
으로 접속하면
이 페이지로 리다이렉션된다. 로그인할 계정을 선택하면
이렇게 jwt 토큰이 발급된다.
디비를 확인해보면
user 테이블에도 있고
socialaccount 테이블에도 있다.
여기서 발급된 access token을 가지고도
Authentication이 잘 된다. 근데 쿠키에 저장은 못했다.
Django-Rest-Framework(DRF)로 소셜 로그인 API 구현해보기(Google, KaKao, Github)
안녕하세요 이 글을 보고 소셜로그인에 정말 많은 도움이 됐습니다.
제가 커스텀유저에 nickname필드를 추가했는데 소셜로그인할 때 nickname필드에 이메일을 저장하려고 여러가지 시도를 해봤는데 다 실패해서 혹시 어느 부분을 수정해야하는지 알수있을까요?