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

Edit. 5월 25일

Chanjong Park
Chan’s Programming Diary
19 min readFeb 16, 2021

--

이번에는 Kakao, Github 소셜 로그인 실습을 해볼 예정이다. 초기 세팅과 Google 로그인은 이전 포스트에 있으며, 기존 구글 로그인의 흐름과 거의 똑같기 때문에, 자세한 설명은 생략하고 진행하며, 이전 포스트에서 확인이 가능하다.

KaKao

카카오는 여기에서 키를 발급받을 수 있다. 로그인을 진행하고 상단 위에 내 어플리케이션에 들어가서, 애플리케이션을 추가한다. 그리고 들어가보면, 다양한 키들이 있는데, 우리는 그 중에 REST API 키를 사용한다.

REST API 키를 secrets.json에 입력 및 admin 사이트에서 Social Application에 등록해준다.

그리고 왼쪽 메뉴바에 카카오 로그인에 들어가서, redirect uri를 설정해준다.

URI와 URL은 차이점이 있다. 혹시 모르니 짚고 넘어가자면, URL가 URI의 상위 호환의 개념이고, URI는 하나의 Endpoint라고 생각하면 된다. 자세한 건 아래 블로그에 잘 나와있다.

이제 admin사이트에서 등록하기만 하면 된다.

client_id가 필수이기 때문에 api key를 client id에 적어준다.

이제 카카오 REST API 문서에 들어가서 필요한 주소들을 확인해준다.

urls.py

from django.urls import path
from accounts import views
urlpatterns = [
path('accounts/kakao/login/', views.kakao_login, name='kakao_login'),
path('accounts/kakao/callback/', views.kakao_callback, name='kakao_callback'),
path('accounts/kakao/login/finish/', views.KakaoLogin.as_view(), name='kakao_login_todjango'),

]

views.py

from django.conf import settings
from accounts.models import User
from allauth.socialaccount.models import SocialAccount
from django.conf import settings
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.google import views as google_view
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from django.http import JsonResponse
import requests
from rest_framework import status
from json.decoder import JSONDecodeError
def kakao_login(request):
rest_api_key = getattr(settings, 'KAKAO_REST_API_KEY')
return redirect(
f"https://kauth.kakao.com/oauth/authorize?client_id={rest_api_key}&redirect_uri={KAKAO_CALLBACK_URI}&response_type=code"
)
def kakao_callback(request):
rest_api_key = getattr(settings, 'KAKAO_REST_API_KEY')
code = request.GET.get("code")
redirect_uri = KAKAO_CALLBACK_URI
"""
Access Token Request
"""

token_req = requests.get(
f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={rest_api_key}&redirect_uri={redirect_uri}&code={code}")
token_req_json = token_req.json()
error = token_req_json.get("error")
if error is not None:
raise JSONDecodeError(error)
access_token = token_req_json.get("access_token")
"""
Email Request
"""

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')
"""
kakao_account에서 이메일 외에
카카오톡 프로필 이미지, 배경 이미지 url 가져올 수 있음
print(kakao_account) 참고
"""

# print(kakao_account)
email = kakao_account.get('email')
"""
Signup or Signin Request
"""

try:
user = User.objects.get(email=email)
# 기존에 가입된 유저의 Provider가 kakao가 아니면 에러 발생, 맞으면 로그인
# 다른 SNS로 가입된 유저

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)
# 기존에 Google로 가입된 유저
data = {'access_token': access_token, 'code': code}
accept = requests.post(
f"{BASE_URL}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)
accept_json = accept.json()
accept_json.pop('user', None)
return JsonResponse(accept_json)
except User.DoesNotExist:
# 기존에 가입된 유저가 없으면 새로 가입
data = {'access_token': access_token, 'code': code}
accept = requests.post(
f"{BASE_URL}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의 pk, email, first name, last name과 Access Token, Refresh token 가져옴
accept_json = accept.json()
accept_json.pop('user', None)
return JsonResponse(accept_json)
class KakaoLogin(SocialLoginView):
adapter_class = kakao_view.KakaoOAuth2Adapter
client_class = OAuth2Client
callback_url = KAKAO_CALLBACK_URI

기본적인 흐름은 전 포스트에 구글과 같이 때문에, 한번 더 언급하지 않고 다른 점만 살펴보겠다.

카카오 계정에 대한 정보(프로필 이미지, 배경 이미지, 이름, 이메일 등)

사용자가 개인정보 제공 동의 창에서 동의한 내용을 바탕으로 정보를 가져올 수 있다.

https://kapi.kakao.com/v2/user/me 의 Header에 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')
"""
kakao_account에서 이메일 외에
카카오톡 프로필 이미지, 배경 이미지 url 가져올 수 있음
print(kakao_account) 참고
"""

# print(kakao_account)
email = kakao_account.get('email')

Github

Github는 여기에서 프로젝트를 생성하고 키를 발급받을 수 있다.

Homepage URL은 http://localhost:8000로 지정하고, callback URL로 마찬가지로 http://localhost:8000/accounts/github/callback/ 까지 입력을 다 하고 나면 프로젝트가 생성되고, Client Id와 Client secrets를 볼 수 있다.

secrets.json에 입력 후, admin 페이지에서 직접 등록해준다.

urls.py

from django.urls import path
from accounts import views
urlpatterns = [
path('github/login/', views.github_login, name='github_login'),
path('github/callback/', views.github_callback, name='github_callback'),
path('github/login/finish/', views.GithubLogin.as_view(), name='github_login_todjango'),
]

views.py

from allauth.socialaccount.providers import github
from django.shortcuts import redirect
from django.conf import settings
from rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.github import views as github_view
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
import requests
from django.http import JsonResponse
BASE_URL = 'http://localhost:8000/'
GITHUB_CALLBACK_URI = BASE_URL + 'accounts/github/callback/'
def github_login(request):
client_id = getattr(settings, 'SOCIAL_AUTH_GITHUB_KEY')
return redirect(
f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={GITHUB_CALLBACK_URI}"
)
def github_callback(request):
client_id = getattr(settings, 'SOCIAL_AUTH_GITHUB_CLIENT_ID')
client_secret = getattr(settings, 'SOCIAL_AUTH_GITHUB_SECRET')
code = request.GET.get('code')
"""
Access Token Request
"""

token_req = requests.post(
f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}&accept=&json&redirect_uri={GITHUB_CALLBACK_URI}&response_type=code", headers={'Accept': 'application/json'})
token_req_json = token_req.json()
error = token_req_json.get("error")
if error is not None:
raise JSONDecodeError(error)
access_token = token_req_json.get('access_token')
"""
Email Request
"""

user_req = requests.get(f"https://api.github.com/user",
headers={"Authorization": f"Bearer {access_token}"})
user_json = user_req.json()
error = user_json.get("error")
if error is not None:
raise JSONDecodeError(error)
# print(user_json)
email = user_json.get("email")
"""
Signup or Signin Request
"""

try:
user = User.objects.get(email=email)
# 기존에 가입된 유저의 Provider가 github가 아니면 에러 발생, 맞으면 로그인
# 다른 SNS로 가입된 유저

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 != 'github':
return JsonResponse({'err_msg': 'no matching social type'}, status=status.HTTP_400_BAD_REQUEST)
# 기존에 github로 가입된 유저
data = {'access_token': access_token, 'code': code}
accept = requests.post(
f"{BASE_URL}accounts/github/login/finish/", data=data)
accept_status = accept.status_code
if accept_status != 200:
return JsonResponse({'err_msg': 'failed to signin'}, status=accept_status)
accept_json = accept.json()
accept_json.pop('user', None)
return JsonResponse(accept_json)
except User.DoesNotExist:
# 기존에 가입된 유저가 없으면 새로 가입
data = {'access_token': access_token, 'code': code}
accept = requests.post(
f"{BASE_URL}accounts/github/login/finish/", data=data)
accept_status = accept.status_code
if accept_status != 200:
return JsonResponse({'err_msg': 'failed to signup'}, status=accept_status)
# user의 pk, email, first name, last name과 Access Token, Refresh token 가져옴
accept_json = accept.json()
accept_json.pop('user', None)
return JsonResponse(accept_json)
class GithubLogin(SocialLoginView):
"""
If it's not working
You need to customize GitHubOAuth2Adapter
use header instead of params
-------------------
def complete_login(self, request, app, token, **kwargs):
params = {'access_token': token.token}
TOdef complete_login(self, request, app, token, **kwargs):
headers = {'Authorization': 'Bearer {0}'.format(token.token)}
-------------------
"""

adapter_class = github_view.GitHubOAuth2Adapter
callback_url = GITHUB_CALLBACK_URI
client_class = OAuth2Client

GithubLogin에서 주석처리 해논 것은 지금은 fix되었지만 혹시나 다른 버전을 다운받았을 때 나처럼 삽질하지 않았으면 하는 마음에 적었다. allauth의 GitHubOAuth2Adapter에서, 변수 이름이 원래 params 였는데, headers로 바꿔줘야 잘 작동했었다. 이거 때문에 print로 다 찍어가며 찾았던 기억이 있다.

깃허브 계정에 대한 정보(팔로워 수, 배경사진 등)

깃허브에서는 요청 값으로 팔로워 수, 이름, 이메일, 배경사진 등을 재공한다.자세한건 user_json 변수를 직접 print해서 찍어서 확인하면 된다.

    """
Email Request
"""

user_req = requests.get(f"https://api.github.com/user",
headers={"Authorization": f"Bearer {access_token}"})
user_json = user_req.json()
error = user_json.get("error")
if error is not None:
raise JSONDecodeError(error)
# print(user_json)
email = user_json.get("email")

ps) Facebook 로그인은 SSL 인증서 문제, 즉 http, https 업데이트 때문에 이 라이브러리로는 사용할 수 없다.

전체 코드는 아래 Repository에서 참고할 수 있다(Star주세요..)

참고

https://code4human.tistory.com/84 -DRF 환경에서 소셜 로그인(kakao) 구현하기

https://code4human.tistory.com/83 -Django REST framework(DRF) 로그인 구현 기록

https://velog.io/@snoop2head/2020-django-social-login-naver-google-kakao -Django에 소셜 로그인 기능 추가하기: Naver, Google, KaKaoTalk

--

--