Django/DRF

Django 프로젝트 단계별 가이드라인 3

Jong_seoung 2023. 12. 3. 15:41
반응형

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
 

DRF 구글 소셜로그인 TypeError: string indices must be integers

dj-rest-auth와 django-allauth를 사용하여 소셜 로그인 및 회원가입을 구현하는데 카카오와 깃허브는 정상적으로 잘 되는데 구글의 경우는 에러가 발생하는 상황이 일어났다. 에러코드 문제가 되는 부

jongseoung.tistory.com

 

어드민 페이지

어드민 페이지에서 설정해 줘야하는 내용

 

구현 코드

더보기

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"),
]

 

반응형