본문 바로가기

BackEnd/DRF

DRF - 소셜 로그인 -2(Naver Login)

반응형


지난 1편에서는 Callback 응답 정보를 올바르게 받아오는 것까지 작성을 했었습니다. 이번 2편에서는 NaverCallbackAPIView 를 수정해서 회원가입, 로그인까지 가능하도록 만들어보겠습니다.

목차

1. views.py
2. NaverCallbackAPIView
3. usls.py


1. views.py

지난 1편에서 NaverCallbackAPIView 를 만들고 실행까지 시켰었습니다. 출력이 try 문을 통해 "성공"이 출력 됐었는데, 확인을 못하신 분들은 다시 한번 실행해서 확인해보시기 바랍니다.

이번 2편에서는 전체 view.py 파일과 urls.py 파일을 수정합니다. 먼저 전체 수정된 파일들을 확인하신 후 각 파트별로 어떻게 되어 있는지 설명하도록 하겠습니다. *NaverLoginAPIView 부분은 1편에서 확인하시기 바랍니다.

구현된 전체 View

# SocialLoginProject/user/API/views.py

# settings.py 에서 설정한 MAIN_DOMAIN 등을 불러오기 위해 import 함
from json import JSONDecodeError

from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import redirect

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from rest_framework.response import Response

import requests

from user.models import User

from allauth.socialaccount.providers.naver import views as naver_views
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView

# main domain(http://127.0.0.1:8000)
main_domain = settings.MAIN_DOMAIN


# DRF의 APIView를 상속받아 View를 구성
class NaverLoginAPIView(APIView):
    # 로그인을 위한 창은 누구든 접속이 가능해야 하기 때문에 permission을 AllowAny로 설정
    permission_classes = (AllowAny,)
    
    def get(self, request, *args, **kwargs):
        client_id = settings.NAVER_CLIENT_ID
        response_type = "code"
        # Naver에서 설정했던 callback url을 입력해주어야 한다.
        # 아래의 전체 값은 http://127.0.0.1:8000/user/naver/callback 이 된다.
        uri = main_domain + "/user/naver/callback"
        state = settings.STATE
        # Naver Document 에서 확인했던 요청 url
        url = "https://nid.naver.com/oauth2.0/authorize"
        
        # Document에 나와있는 요소들을 담아서 요청한다.
        return redirect(
            f'{url}?response_type={response_type}&client_id={client_id}&redirect_uri={uri}&state={state}'
        )


class NaverCallbackAPIView(APIView):
    permission_classes = (AllowAny,)
    
    def get(self, request, *args, **kwargs):
        try:
            # Naver Login Parameters
            grant_type = 'authorization_code'
            client_id = settings.NAVER_CLIENT_ID
            client_secret = settings.NAVER_CLIENT_SECRET
            code = request.GET.get('code')
            state = request.GET.get('state')

            parameters = f"grant_type={grant_type}&client_id={client_id}&client_secret={client_secret}&code={code}&state={state}"

            # token request
            token_request = requests.get(
                f"https://nid.naver.com/oauth2.0/token?{parameters}"
            )

            token_response_json = token_request.json()
            error = token_response_json.get("error", None)

            if error is not None:
                raise JSONDecodeError(error)

            access_token = token_response_json.get("access_token")

            # User info get request
            user_info_request = requests.get(
                "https://openapi.naver.com/v1/nid/me",
                headers={"Authorization": f"Bearer {access_token}"},
            )

            # User 정보를 가지고 오는 요청이 잘못된 경우
            if user_info_request.status_code != 200:
                return JsonResponse({"error": "failed to get email."}, status=status.HTTP_400_BAD_REQUEST)

            user_info = user_info_request.json().get("response")
            email = user_info["email"]

            # User 의 email 을 받아오지 못한 경우
            if email is None:
                return JsonResponse({
                    "error": "Can't Get Email Information from Naver"
                }, status=status.HTTP_400_BAD_REQUEST)

            try:
                user = User.objects.get(email=email)
                data = {'access_token': access_token, 'code': code}
                # accept 에는 token 값이 json 형태로 들어온다({"key"}:"token value")
                # 여기서 오는 key 값은 authtoken_token에 저장된다.
                accept = requests.post(
                    f"{main_domain}/user/naver/login/success", data=data
                )
                # 만약 token 요청이 제대로 이루어지지 않으면 오류처리
                if accept.status_code != 200:
                    return JsonResponse({"error": "Failed to Signin."}, status=accept.status_code)
                return Response(accept.json(), status=status.HTTP_200_OK)

            except User.DoesNotExist:
                data = {'access_token': access_token, 'code': code}
                accept = requests.post(
                    f"{main_domain}/user/naver/login/success", data=data
                )
                # token 발급
                return Response(accept.json(), status=status.HTTP_200_OK)
                
        except:
            return JsonResponse({
                "error": "error",
            }, status=status.HTTP_404_NOT_FOUND)
            
   
 class NaverToDjangoLoginView(SocialLoginView):
    adapter_class = naver_views.NaverOAuth2Adapter
    client_class = OAuth2Client

 


2. NaverCallbackAPIView 

앞서 만들었던 NaverLoginAPIView를 통해 redirect로 NaverCallbackAPIView가 실행되는 것을 알 수 있었습니다. 다시 한번 Naver에서 제공해주는 Document를 확인해보면서 어떻게 View를 구성하는지 자세히 살펴보도록 하겠습니다.

(1) 접근 토큰 발급 요청

Naver 개발자센터

Callback으로 전달받은 정보를 이용해서 접근 토큰을 발급받을 수 있습니다. 접근 토큰은 사용자가 인증을 완료했다는 것을 보장할 수 있는 인증정보입니다.

1편에서 만들었던 NaverLoginAPIView 에서 요청했던 것과 크게 다른 부분이 없습니다. URL에 grant_type, client_id 등을 포함시켜 요청을 보내면 코드가 발급됩니다. NaverCallBackAPIView의 접근 토큰 발급 요청 부분을 자세히 살펴보겠습니다.

# Naver Login Parameters
grant_type = 'authorization_code'
client_id = settings.NAVER_CLIENT_ID
client_secret = settings.NAVER_CLIENT_SECRET
code = request.GET.get('code')
state = request.GET.get('state')

grant_type 은 Document에 나와있는 대로 발급 요청을 할 경우 'authorization_code'를 입력하도록 되어 있습니다. client_id와 client_secret은 앞서 secrets.json 파일에 옮겨놓았던 것들을 불러와서 할당합니다. code의 경우 아래 사진에서와 같이 Callback으로 전달받은 code, state 값을 각각 code, state에 넣어줍니다.

이 응답 정보는 1편을 진행해 받은 응답입니다.

그리고 이 값들을 parameters 라는 변수에 url 파라미터 규칙에 맞게 할당합니다.

parameters = f"grant_type={grant_type}&client_id={client_id}&client_secret={client_secret}&code={code}&state={state}"

이제 요청할 url에 파라미터를 담아 요청해줍니다.

# token request
token_request = requests.get(
    f"https://nid.naver.com/oauth2.0/token?{parameters}"
)

이렇게 요청을 보내면 반환된 값이 token_request 에 담기게 됩니다. JSON 형태로 받은 응답을 Python에서 확인할 수 있게 변환하고, Error 코드가 왔는지 확인합니다. error 코드가 없으면 반환된 "access_token"을 access_token 변수에 할당하는 과정을 아래와 같이 진행합니다.

token_response_json = token_request.json()
error = token_response_json.get("error", None)

if error is not None:
    raise JSONDecodeError(error)

access_token = token_response_json.get("access_token")

아래 표는 "https://nid.naver.com/oauth2.0/token?" 주소로 요청했을 때 받을 수 있는 정보들입니다.

(2) 접근 토큰을 이용해 프로필 API 호출

(1)번의 과정을 통해 접근 토큰을 발급 받았습니다. 이번에는 이를 통해 프로필 API를 호출하고, 사용자의 정보를 받아오도록 하겠습니다. 먼저 아래 Document를 살펴보도록 하겠습니다.

Document에 따르면 요청할 주소는 "https://openapi.naver.com/v1/nid/me"이고 요청 변수는 사용자 정보를 담고있는 "Access token(접근 토큰)" 입니다. 접근 토큰은 위에 (1)번에서 받았었습니다. 그걸 파라미터로 넣어서 요청 주소로 보내주면 됩니다. 아래 코드와 같이 말이죠.

 

 # User info get request
user_info_request = requests.get(
    "https://openapi.naver.com/v1/nid/me",
    headers={"Authorization": f"Bearer {access_token}"},
)

그러면 "Access token"에 맞는 사용자의 정보가 user_info_request에 담기게 됩니다. 

# User 정보를 가지고 오는 요청이 잘못된 경우
if user_info_request.status_code != 200:
    return JsonResponse({"error": "failed to get email."}, status=status.HTTP_400_BAD_REQUEST)

위 코드는 만약 요청 과정에서 access_token 이 잘못되었거나 응답을 제대로 받지 못하는 경우 나타나는 오류를 설정해주었습니다.

user_info = user_info_request.json().get("response")
email = user_info["email"]

아래에 있는 출력 결과를 보면 user에 대한 정보는 "response" 영역 아래에 있는 것을 알 수 있습니다. 이번에 저희가 가져올 부분은 "email" 등 사용자의 정보이기 때문에 "response" 영역만 따로 user_info 라는 변수에 할당합니다. 그리고 그 안에 있는 "email"을 불러옵니다.

Document 를 확인하시면 reponse를 통해 더 많은 정보를 얻을 수 있습니다. 확인해보시기 바랍니다. 또한 이 부분에서 받아오는 정보들은 권한 동의하에 진행되는 부분입니다. 추가로 받고 싶은 정보가 있다면 Naver 개발자센터 에서 설정해주시면 됩니다. 다만 지금 allauth 와 dj_rest_auth를 사용 중에 있기 때문에 해당 내용을 DB에 저장하는데에는 문제가 조금 있습니다. 이 부분은 adapter 와 serializer 를 구현하면 해결됩니다. adapter와 serializer를 만드는 부분은 추후에 업데이트 하도록 하겠습니다.

# User 의 email 을 받아오지 못한 경우
if email is None:
    return JsonResponse({
        "error": "Can't Get Email Information from Naver"
    }, status=status.HTTP_400_BAD_REQUEST)

만약 "email"을 불러오지 못하면 error가 나타나도록 설정해주었습니다.

아래 코드부터는 사용자가 이미 가입한 사용자인지, 새로운 사용자인지에 따라 로그인 처리를 할지, 회원가입으로 처리를 할지를 결정합니다. try 구문에 User.DoesNotExist로 Exception이 발생하면 회원가입 절차를, Exception이 발생하지 않으면 로그인으로 처리합니다.

try:
    user = User.objects.get(email=email)
    data = {'access_token': access_token, 'code': code}
    # accept 에는 token 값이 json 형태로 들어온다({"key"}:"token value")
    # 여기서 오는 key 값은 authtoken_token에 저장된다.
    accept = requests.post(
        f"{main_domain}/user/naver/login/success", data=data
    )
    # 만약 token 요청이 제대로 이루어지지 않으면 오류처리
    if accept.status_code != 200:
        return JsonResponse({"error": "Failed to Signin."}, status=accept.status_code)
    return Response(accept.json(), status=status.HTTP_200_OK)

먼저 동일한 email을 사용하는 사용자가 있는 경우에, access_token과 code를 보냅니다.  

f"{main_domain}/user/naver/login/success"

이 부분의 주소는 추후에 dj_rest_auth 의 SocialLoginView를 상속받는 NaverToDjangoLoginView를 실행시키는 URL 입니다. 여기서 adapter_class 와 client_class 를 설정해주는데, 이 부분은 뒤에서 설명하도록 하겠습니다.

NaverToDjangoLoginView가 실행되고, 성공적으로 로그인이 완료되면 다음과 같은 정보가 accept를 통해 돌아오게 됩니다. 여기서 access_token 과 refresh_token 은 제가 설정한 simplejwt 입니다.

    except User.DoesNotExist:
        data = {'access_token': access_token, 'code': code}
        accept = requests.post(
            f"{main_domain}/user/naver/login/success", data=data
        )
        # token 발급
        return Response(accept.json(), status=status.HTTP_200_OK)
        # return JsonResponse(accept.json())
except:
    return JsonResponse({
        "error": "error",
    }, status=status.HTTP_404_NOT_FOUND)

위 코드는 사용자가 존재하지 않는 경우 회원가입 절차를 나타냈습니다. 위의 부분과 다를게 없어보이지만 여기에서 몇 가지 코드를 구현해 회원가입 절차를 따로 추가할 수 있습니다.

class NaverToDjangoLoginView(SocialLoginView):
    adapter_class = naver_views.NaverOAuth2Adapter
    client_class = OAuth2Client

dj_rest_auth 메뉴얼에 따라 login 정보를 저장하기 위해 SocialLoginView를 상속받아 Naver Adapter에 추가해줍니다. 이  view에서는 serializer, callbackurl 등을 custom 해서 사용할 수 있습니다. 자세한 내용은 아래 홈페이지에서 확인하시기 바랍니다.

https://dj-rest-auth.readthedocs.io/en/latest/

 

Welcome to dj-rest-auth’s documentation! — dj-rest-auth 2.2.4 documentation

© Copyright 2020, @iMerica Revision b2d5f0e0.

dj-rest-auth.readthedocs.io


3. urls.py

# SocialLoginProject/user/API/urls.py

from django.urls import path, include
from user.API.views import (
    NaverLoginAPIView, NaverCallbackAPIView, NaverToDjangoLoginView
    )

urlpatterns = [
    path('naver/login', NaverLoginAPIView.as_view()),
    path('naver/callback', NaverCallbackAPIView.as_view()),
    path('naver/login/success', NaverToDjangoLoginView.as_view()),
]

여기까지 DRF에서의 Naver Login 과정을 마치도록 하겠습니다.

전체 코드는 아래에서 확인해주시기 바랍니다. 카카오 로그인도 구현되어 있습니다. 참고해서 활용하시기 바랍니다 ~

https://github.com/SudonNoh/SocialLoginApp-Naver

 

GitHub - SudonNoh/SocialLoginApp-Naver: DRF를 이용한 Naver Social Login

DRF를 이용한 Naver Social Login. Contribute to SudonNoh/SocialLoginApp-Naver development by creating an account on GitHub.

github.com

https://github.com/SudonNoh/SocialLoginApp-KaKao

 

GitHub - SudonNoh/SocialLoginApp-KaKao: DRF를 이용한 Kakao Social Login

DRF를 이용한 Kakao Social Login. Contribute to SudonNoh/SocialLoginApp-KaKao development by creating an account on GitHub.

github.com

 

반응형