Django JWT를 이용한 소셜로그인 (백엔드)

2023. 10. 11. 11:45· Django/DRF
목차
  1. 전체 코드
  2. 코드 해석
  3. reference
반응형

Django를 백엔드로 이용해서 소셜로그인을 구현하려고 하는데 내가 못 찾는 건지 모르겠지만 Djnago를 백으로만 사용하여 구현한 글이 생각보다 참고할 글이 없고 이렇게 저렇게 시행착오가 많았다.

 

지금 와서 생각해 보면 당연한 이야기이고 조금만 생각해 보면 결과를 도출해 낼 수 있었는데 왜 더 빠르게 생각해내지 못했는지..

 

우선 소셜로그인의 전반적인 과정을 이해하는게 중요하다고 생각한다. 아래의 그림은 내가 전반적인 과정을 이해하는데 가장 큰 도움이 된 그림이다.

https://data-jj.tistory.com/53

그럼 백단에서 해야 할 일은 무엇인가?

1. 프론트에서 보내는 인가코드를 받아서 Redirect url과 함께 카카오로 전송한다.

2. 카카오에서 보낸 토큰을 받아서 우리 서버의 JWT 토큰을 생성하고 로그인시켜 준다.

이렇게 정리하고 보니 더 간단한 일 같은데 왜 그렇게 헤맸을까..

 

전체 코드

views.py

from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response


class Constants:
    BASE_URL = getattr(settings, "BASE_URL")
    
    REST_API_KEY = getattr(settings, "KAKAO_REST_API_KEY")
    KAKAO_CALLBACK_URI = f"http://localhost:3000/kakao"

 

kakaologin.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
from drf_yasg.utils import swagger_auto_schema
import requests
from user.models import User
from user.serializers import TokenResponseSerializer
from rest_framework.response import Response
from user.views import Constants
from drf_yasg import openapi


class KakaoLoginView(APIView):
    permission_classes = [AllowAny]
    schema = None

    @swagger_auto_schema(operation_id="카카오 로그인")
    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]

    @swagger_auto_schema(
        operation_id="카카오 로그인 콜백",
        manual_parameters=[
            openapi.Parameter(
                'code',
                in_=openapi.IN_QUERY,
                description='카카오에서 반환한 인증 코드',
                type=openapi.TYPE_STRING,
                required=True,
            ),
        ],
    )
    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")
        """
            Access Token Request
        """
        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/user/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/user/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/user/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

 

urls.py

from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
from user.social_views.kakao_login import (
    KakaoLoginView,
    KakaoCallbackView,
    KakaoLoginToDjango,
)

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("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
]

 

 

코드 해석

views.py

from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response


class Constants:
    BASE_URL = getattr(settings, "BASE_URL")
    
    REST_API_KEY = getattr(settings, "KAKAO_REST_API_KEY")
    KAKAO_CALLBACK_URI = f"http://localhost:3000/kakao"

BASE_ULR의 경우는 홈페이지의 URL을 사용하면 된다. 위에서는 settings로 이동해서. env안에 담겨 있고 서버의 경우는 서버 환경 파일에 들어 있다.

REST_API_KEY의 경우는 이전에 사용했던 카카오 REST_API_KEY를 사용하면 된다

KAKAO_CALLBACK_URI 이 부분이 중요한 부분이다. Redirect uri가 되는 값인데 프론트에서 접근이 가능하도록 설정해 주어야 한다. 왜냐하면 프론트단에서 Redirect uri에서 인가코드를 받아서 백엔드로 넘기기 때문이다.

 

kakaologin.py

class KakaoLoginView(APIView):
    permission_classes = [AllowAny]
    schema = None

    @swagger_auto_schema(operation_id="카카오 로그인")
    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"
        )

이 부분의 경우는 인가코드를 받아오고 redirect url로 넘겨주는 부분인데.. django만 이용해서 할 때는 rediect uri를 callback url로 연결해서 자동으로 인가코드가 넘어가도록 하였지만 인가코드는 프론트에서 전달해 주기 때문에 사실상 작성하지 않아도 되는 코드이다.

 

나는 로컬에서도 인가코드를 받아서 테스트해보고 싶어서 작성하였고 프론트 분들이 헷갈리면 안 되기 때문에 schena = None로 해서 스웨거에서는 표시 안되도록 하였다.

 

과거에 아무 생각 없이 인가코드를 받아오고 백에서 백으로 리다이렉트 하도록 구현하였는데 이 부분 때문에 프론트분들과 이야기도 많이했고 제일 이해하기 어려웠던 부분이였다.  '나는 잘되는데 왜 안된다는 거지?' 라는 느낌이였다.

하지만 위처럼 구현하면 프론트에서 접근할 수 있는 방법이 없었고 스웨거에서도 redirect로 넘어가기 때문에 에러가 발생하였다.

 

class KakaoCallbackView(APIView):
    permission_classes = [AllowAny]

    @swagger_auto_schema(
        operation_id="카카오 로그인 콜백",
        manual_parameters=[
            openapi.Parameter(
                'code',
                in_=openapi.IN_QUERY,
                description='카카오에서 반환한 인증 코드',
                type=openapi.TYPE_STRING,
                required=True,
            ),
        ],
    )
    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")
        """
            Access Token Request
        """
        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/user/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/user/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/user/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

이 부분이 사실상 메인이다. 코드가 길어서 어려워 보이는데 하나하나 천천히 읽어보면 그렇게 어려운 코드도 아니고 이해하기 어려운 내용도 없는 부분이다.

대략적인 진행 방향은 아래와 같다.

  • 카카오에 인가코드와 리다이렉트 uri를 보내서 토큰을 받아온다.
  • 토큰을 이용하여 카카오에 유저정보(email)를 요청한다.
  • 이메일을 이용하여 우리 서버에 계정이 있는지 없는지를 판별한다
  • 만약 계정이 있다면 서버의 액세스토큰과 리프래시 토큰을 리턴해준다.
  • 계정이 없다면 서버에 회원가입하고 액세스토큰과 리프래시 토큰을 리턴해준다.

토큰 발급은 다른 게시물에 있다.

 

class KakaoLoginToDjango(SocialLoginView):
    permission_classes = [AllowAny]
    schema = None

    adapter_class = kakao_view.KakaoOAuth2Adapter
    client_class = OAuth2Client
    callback_url = Constants.KAKAO_CALLBACK_URI

SocialLoginView 클래스를 상속받아 사용된 클래스이다. SocialLoginView 클래스는 소셜로그인 관련 기능을 확장하고 커스터마이징 할 수 있다. 이 클래스는 사용자가 카카오 계정으로 로그인할 때의 동작을 정의하는 클래스로 카카오와의 OAuth2 연동과 관련된 설정들이 있다

 

 

reference

 

[OAuth] Spring Boot + React + OAuth2.0 이용한 네이버, 카카오 로그인

들어가기 전 토이 프로젝트를 진행하면서 OAuth를 이용한 소셜 로그인을 구현해보았습니다. 프론트는 React를 이용하였고 백엔드는 Spring Boot를 이용하였습니다. 네이버, 카카오 로그인에 대한 코

hoestory.tistory.com

 

 

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

SPA(react.js), Mobile App을 DRF(Django-Rest-Framework)와 연동하여 진행하는 프로젝트의 일환으로 소셜 로그인을 구현해 보았다.

medium.com

 

반응형
저작자표시 (새창열림)

'Django > DRF' 카테고리의 다른 글

DRF Paginator를 이용한 페이지네이션  (0) 2023.10.11
DRF 유저 정보로 JWT 토큰 발급  (0) 2023.10.11
DRF JWT를 이용하여 유저 정보 GET/POST  (0) 2023.10.11
DRF Image DB 저장, 스웨거 적용  (0) 2023.10.10
Django 인기글 구현  (1) 2023.09.26
  1. 전체 코드
  2. 코드 해석
  3. reference
'Django/DRF' 카테고리의 다른 글
  • DRF Paginator를 이용한 페이지네이션
  • DRF 유저 정보로 JWT 토큰 발급
  • DRF JWT를 이용하여 유저 정보 GET/POST
  • DRF Image DB 저장, 스웨거 적용
Jong_seoung
Jong_seoung
기록하자, 머리는 생각하는 곳이지 저장장치가 아니다.
반응형
Jong_seoung
Today_developStory
Jong_seoung
전체
오늘
어제

블로그 메뉴

  • Home
  • Git Hub
  • 분류 전체보기 (351)
    • Theory (16)
    • Java (3)
      • 알고리즘 (2)
      • 문법 (0)
    • Spring (7)
      • 스프링 입문 (6)
      • PickTalk (0)
      • 에러처리 (1)
    • Python (80)
      • 알고리즘 - 이론 (17)
      • 알고리즘 - 내장함수, 라이브러리 등등 (3)
      • 알고리즘 - 백준 (53)
      • 나도코딩 정리 (2)
      • 기타 (5)
    • Django (159)
      • DRF (105)
      • 인프라 (46)
      • DataBases (2)
      • API Docs (6)
    • FrontEnd (22)
      • htmx (2)
      • React (8)
      • 자바스크립트 (12)
    • GIT (16)
    • 기타 (8)
      • 정리 (2)
      • Flutter (1)
      • 마이크로프로세서 - ATmega128 (2)
      • 개발환경 세팅 (3)
    • 자격증 (37)
      • 정보처리기사 (19)
      • SQLD자격증 (18)

인기 글

최근 글

태그

  • alarm
  • BFS
  • CSRF
  • Django
  • django channels
  • django sse
  • django tutorial
  • django 배포
  • django 스웨거 적용
  • Django 이미지 저장

최근 댓글

hELLO · Designed By 정상우.v4.3.0
Jong_seoung
Django JWT를 이용한 소셜로그인 (백엔드)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.