Django를 백엔드로 이용해서 소셜로그인을 구현하려고 하는데 내가 못 찾는 건지 모르겠지만 Djnago를 백으로만 사용하여 구현한 글이 생각보다 참고할 글이 없고 이렇게 저렇게 시행착오가 많았다.
지금 와서 생각해 보면 당연한 이야기이고 조금만 생각해 보면 결과를 도출해 낼 수 있었는데 왜 더 빠르게 생각해내지 못했는지..
우선 소셜로그인의 전반적인 과정을 이해하는게 중요하다고 생각한다. 아래의 그림은 내가 전반적인 과정을 이해하는데 가장 큰 도움이 된 그림이다.
그럼 백단에서 해야 할 일은 무엇인가?
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 |