[2020] Django에 소셜 로그인 기능 추가하기: Naver, Google, KakaoTalk

snoop2head·2020년 4월 21일
4

django

목록 보기
2/3
post-thumbnail
post-custom-banner

django-allauth vs django-social-auth and others

Regarding libraries that support django's social login functionality, there's a medium post written in 2017: Django-Allauth vs. Django Social Auth vs. Python-Social-Auth.

Long story short, developer should choose allauth for its 1) frequent maintanence, 2) support for python 3.x and 3) upto-date documentation.

KakaoTalk

django allauth에서는 KakaoTalk 로그인이 옛날 거라서 작동 안 함.

"""
users/views.py
"""

class LoginView(FormView):

    """ Login View """

    # Using inherited FormView class instead of LoginView: https://ccbv.co.uk/projects/Django/3.0/django.views.generic.edit/FormView/
    template_name = "users/login.html"
    form_class = forms.LoginForm
    success_url = reverse_lazy("core:home")

    def form_valid(self, form):
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user is not None:
            login(
                self.request, user, backend="django.contrib.auth.backends.ModelBackend"
            )
        return super().form_valid(form)

# Logout function: https://docs.djangoproject.com/en/3.0/topics/auth/default/#how-to-log-a-user-out
# LogoutView class: https://docs.djangoproject.com/en/3.0/topics/auth/default/#django.contrib.auth.views.LogoutView
def log_out(request):
    logout(request)
    return redirect(reverse("core:home"))


class SignUpView(FormView):
    # Using inherited FormView class: https://ccbv.co.uk/projects/Django/3.0/django.views.generic.edit/FormView/
    template_name = "users/signup.html"
    form_class = forms.SignUpForm
    success_url = reverse_lazy("core:home")

    # to see where "form" came from, CMD + Click on FormView inherited class
    def form_valid(self, form):
        form.save()
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user is not None:
            login(
                self.request, user, backend="django.contrib.auth.backends.ModelBackend"
            )
        # getting function from models.py users app, verify user by sending randomly genearated string
        user.verify_email()
        return super().form_valid(form)

# https://developers.kakao.com/docs/restapi/user-management
def kakao_login(request):
    app_rest_api_key = os.environ.get("KAKAO_REST_API_KEY")
    redirect_uri = main_domain + "users/login/kakao/callback"
    return redirect(
        f"https://kauth.kakao.com/oauth/authorize?client_id={app_rest_api_key}&redirect_uri={redirect_uri}&response_type=code"
    )


class KakaoException(Exception):
    pass

# https://developers.kakao.com/docs/restapi/user-management
def kakao_callback(request):
    try:
        app_rest_api_key = os.environ.get("KAKAO_REST_API_KEY")
        redirect_uri = main_domain + "users/login/kakao/callback"
        user_token = request.GET.get("code")
        # post request
        token_request = requests.get(
            f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={app_rest_api_key}&redirect_uri={redirect_uri}&code={user_token}"
        )

        token_response_json = token_request.json()
        error = token_response_json.get("error", None)
        # if there is an error from token_request
        if error is not None:
            raise KakaoException()
        access_token = token_response_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()
        # print(profile_json)
        # parsing profile json
        kakao_account = profile_json.get("kakao_account")
        email = kakao_account.get("email", None)
        if email is None:
            raise KakaoException()
        profile = kakao_account.get("profile")
        nickname = profile.get("nickname")
        profile_image_url = profile.get("profile_image_url")
        try:
            user_in_db = models.User.objects.get(email=email)
            if user_in_db.register_login_method != models.User.REGISTER_LOGIN_KAKAO:
                raise KakaoException()
            else:
                login(
                    request,
                    user_in_db,
                    backend="django.contrib.auth.backends.ModelBackend",
                )
        except models.User.DoesNotExist:
            new_user_to_db = models.User.objects.create(
                username=email,
                email=email,
                first_name=nickname,
                register_login_method=models.User.REGISTER_LOGIN_KAKAO,
                email_confirmed=True,
            )
            # https://docs.djangoproject.com/en/3.0/ref/contrib/auth/#django.contrib.auth.models.User.set_unusable_password
            new_user_to_db.set_unusable_password()
            new_user_to_db.save()
            # after user is saved to db, login the user
            login(
                request,
                new_user_to_db,
                backend="django.contrib.auth.backends.ModelBackend",
            )
        return redirect(reverse("core:home"))
    except KakaoException:
        return redirect(reverse("users:login"))

If you don't write the following part:

backend="django.contrib.auth.backends.ModelBackend"

then it will clash with django-allauth with the following error message.

ValueError: You have multiple authentication backends configured and therefore must provide the backend argument or set the backend attribute on the user.

Google: django-allauth

"""config/settings.py"""
# Specify the context processors as follows:
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                # Already defined Django-related contexts here

                # `allauth` needs this from django
                'django.template.context_processors.request',
            ],
        },
    },
]

AUTHENTICATION_BACKENDS = (
    # Needed to login by username in Django admin, regardless of `allauth`
    'django.contrib.auth.backends.ModelBackend',

    # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
)

INSTALLED_APPS = (
    # The following apps are required:
    'django.contrib.auth',
    'django.contrib.messages',
    'django.contrib.sites',
		
	  # Required apps for allauth:
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
	
  
  	# selecting google and naver
    'allauth.socialaccount.providers.google',
  	'allauth.socialaccount.providers.naver',
)

# site_id is primary key for queryset item in the site application.
# if add a site instead of replacing for the given examples.com, then you should use 2.
SITE_ID = 1
"""config/urls.py"""
urlpatterns = [
    ...
    path("accounts/", include("allauth.urls")),
    ...
]
python ./manage.py migrate

위의 과정만 똑같이 하면 되지만, Callback URL을 알아내는 과정이 좀 번거롭긴 하다. Naver Help에서 Callback URL을 알아내는 방법이 나와있다.

Facebook (극혐)

SSL 인증서를 갖고 HTTPS로 연결을 해야 하는데, 아직도 에러가 난다. 무엇보다 2018년 Facebook Development Console 템플릿과 현재 Facebook Development Console 템플릿이 달라서, 아직 잘 모르겠다. 현재 어플리케이션 등록만 한 상태.

Screen Shot 2020-04-21 at 9.34.04 AM

Django Template

{% load socialaccount %}
{% providers_media_js %}

<div class="flex flex-col w-full">
    <!-- receiving from django-allauth -->    
    <a href="{% provider_login_url "naver" %}" class="w-full border font-medium text-white mb-2 border-green-500 text-center rounded-sm py-5 bg-green-500">
        <i class="xi-naver-square xi-x mr-1"></i> 네이버로 계속하기
    </a> 

    <!-- receiving from users/urls.py -->    
    <a href="{% url 'users:kakao-login' %}" class="w-full border font-medium text-yellow-800 mb-2 border-yellow-400 text-center rounded-sm py-5 bg-yellow-400">
        <i class="xi-kakaotalk xi-x mr-1"></i> 카카오로 계속하기
    </a>     
    
  	<!-- receiving from django-allauth -->    
    <a href="{% provider_login_url "google" %}" class="w-full border font-medium text-white mb-2 border-red-500 text-center rounded-sm py-5 bg-red-500">
        <i class="fab fa-google mr-2"></i> 구글로 계속하기
    </a>
    
</div>
profile
break, compose, display
post-custom-banner

2개의 댓글

comment-user-thumbnail
2020년 6월 17일

kakao_callback 함수 속 models.User.REGISTER_LOGIN_KAKAO 이 User 모델 클래스 안에 있는 함수 같은데 맞나요? 코드가 없어서 헷갈리네요..ㅠ

1개의 답글