반응형
Django 프로젝트 단계별 가이드라인 3
소셜 로그인
소셜 로그인을 위한 패키지는 아래의 패키지들을 사용한다.
- 자세한 사용법은 공식 문서를 참고
- 버전을 지정해 준 이유는 상위 버전에서는 google, apple 로그인 시 라이브러리 충돌이 일어나서 제대로 동작하지 않는다.
- allauth를 마이그레이션 해준후에 django.config.sites를 마이그레이션을 할 경우에도 에러가 발생하니 반드시 같이 진행해야 한다.
pip install dj-rest-auth==4.0.1
pip install django-allauth==0.50.0
pip install djangorestframework-simplejwt
어드민 페이지
구현 코드
더보기
Settings.py
공식 문서를 보고 제대로 따라 한다면 아무런 문제도 없지만 구현 완료 후 추가된 내용들이다.
INSTALLED_APPS = [
"django.contrib.sites",
"rest_framework.authtoken",
"rest_framework_simplejwt",
"dj_rest_auth",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"allauth.socialaccount.providers.kakao",
]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
AUTH_USER_MODEL = "accounts.User"
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "email"
SITE_ID = 1
LOGIN_REDIRECT_URL = "/"
ACCOUNT_LOGOUT_REDIRECT_URL = "/"
accounts/views.py
from django.conf import settings
class Constants:
BASE_URL = getattr(settings, "BASE_URL")
GOOGLE_CALLBACK_URI = f"http://localhost:3000/google"
GOOGLE_CLIENT_ID = getattr(settings, "SOCIAL_AUTH_GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = getattr(settings, "SOCIAL_AUTH_GOOGLE_SECRET")
GOOGLE_SCOPE = " ".join(
[
"https://www.googleapis.com/auth/userinfo.email",
]
)
REST_API_KEY = getattr(settings, "KAKAO_REST_API_KEY")
KAKAO_CALLBACK_URI = f"http://localhost:3000/kakao"
여기서 주의해야 할 점은 CALLBACK_URL을 프론트에서 접글할 수 있는 URL로 지정해주어야 한다.
accounts/social_views/LOGIN_Vivews.py
from django.shortcuts import redirect
from django.http import JsonResponse
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import AllowAny
from allauth.socialaccount.models import SocialAccount
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.kakao import views as kakao_view
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
import requests
from accounts.models import User
from accounts.serializers import TokenResponseSerializer
from rest_framework.response import Response
from accounts.views import Constants
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.types import OpenApiTypes
class KakaoLoginView(APIView):
permission_classes = [AllowAny]
schema = None
def get(self, request):
return redirect(
f"https://kauth.kakao.com/oauth/authorize?client_id={Constants.REST_API_KEY}"
f"&redirect_uri={Constants.KAKAO_CALLBACK_URI}&response_type=code"
)
class KakaoCallbackView(APIView):
permission_classes = [AllowAny]
@extend_schema(
operation_id="카카오 로그인 콜백",
tags=["로그인"],
parameters=[
OpenApiParameter(
name="code",
type=OpenApiTypes.DATE,
location=OpenApiParameter.QUERY,
description="카카오에서 반환한 인증 코드",
),
],
)
def get(self, request):
BASE_URL = Constants.BASE_URL
REST_API_KEY = Constants.REST_API_KEY
KAKAO_CALLBACK_URI = Constants.KAKAO_CALLBACK_URI
code = request.GET.get("code")
token_req = requests.get(
f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={REST_API_KEY}"
f"&redirect_uri={KAKAO_CALLBACK_URI}&code={code}"
)
token_req_json = token_req.json()
error = token_req_json.get("error")
if error is not None:
if token_req_json.get("error") == "invalid_request":
return redirect(f"{BASE_URL}api/v1/accounts/kakao/login")
return JsonResponse(token_req_json)
access_token = token_req_json.get("access_token")
profile_request = requests.get(
"https://kapi.kakao.com/v2/user/me",
headers={"Authorization": f"Bearer {access_token}"},
)
profile_json = profile_request.json()
kakao_account = profile_json.get("kakao_account")
email = kakao_account.get("email")
try:
user = User.objects.get(email=email)
social_user = SocialAccount.objects.get(user=user)
if social_user is None:
return JsonResponse(
{"err_msg": "email exists but not social user"},
status=status.HTTP_400_BAD_REQUEST,
)
if social_user.provider != "kakao":
return JsonResponse(
{"err_msg": "no matching social type"},
status=status.HTTP_400_BAD_REQUEST,
)
data = {"access_token": access_token, "code": code}
accept = requests.post(
f"{BASE_URL}api/v1/accounts/kakao/login/finish/", data=data
)
accept_status = accept.status_code
if accept_status != 200:
return JsonResponse(
{"err_msg": "failed to signin"}, status=accept_status
)
serializer = TokenResponseSerializer(user)
data = serializer.to_representation(serializer)
res = Response(
data,
status=status.HTTP_200_OK,
)
return res
except User.DoesNotExist:
data = {"access_token": access_token, "code": code}
accept = requests.post(
f"{BASE_URL}api/v1/accounts/kakao/login/finish/", data=data
)
accept_status = accept.status_code
if accept_status != 200:
return JsonResponse(
{"err_msg": "failed to signup"}, status=accept_status
)
user = User.objects.get(email=email)
serializer = TokenResponseSerializer(user)
data = serializer.to_representation(serializer)
res = Response(
data,
status=status.HTTP_200_OK,
)
return res
class KakaoLoginToDjango(SocialLoginView):
permission_classes = [AllowAny]
schema = None
adapter_class = kakao_view.KakaoOAuth2Adapter
client_class = OAuth2Client
callback_url = Constants.KAKAO_CALLBACK_URI
구글 역시 비슷한 구조로 이루어져 있으니 읽어보고 조금 수정하면 된다.
accounts/Serializer.py
from rest_framework_simplejwt.tokens import RefreshToken, TokenError
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework import serializers
class TokenResponseSerializer(serializers.Serializer):
access_token = serializers.CharField()
refresh_token = serializers.CharField()
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.token = TokenObtainPairSerializer.get_token(user)
self.user = user
def get_access_token(self):
return str(self.token.access_token)
def get_refresh_token(self):
return str(self.token)
def to_representation(self, instance):
nickname = self.user.nickname
if nickname is None:
message = True
else:
message = False
return {
"message": message,
"token": {
"email": self.user.email,
"access": self.get_access_token(),
"refresh": self.get_refresh_token(),
},
}
카카오토 큰을 이용하여 email을 알아낸 뒤 우리 사이트의 토큰을 생성하는 구문이다.
accounts/urls.py
from django.urls import path
from accounts.social_views.kakao_login import (
KakaoLoginView,
KakaoCallbackView,
KakaoLoginToDjango,
)
from accounts.social_views.google_login import (
GoogleLoginView,
GoogleCallbackView,
GoogleLoginToDjango,
)
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path("kakao/login/", KakaoLoginView.as_view(), name="kakao_login"),
path("kakao/callback/", KakaoCallbackView.as_view(), name="kakao_callback"),
path(
"kakao/login/finish/",
KakaoLoginToDjango.as_view(),
name="kakao_login_to_django",
),
path("google/login/", GoogleLoginView.as_view(), name="google_login"),
path("google/callback/", GoogleCallbackView.as_view(), name="google_callback"),
path(
"google/login/finish/",
GoogleLoginToDjango.as_view(),
name="google_login_to_django",
),
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
반응형