이전 내용
[Django tutorial] 사용자 인증 - 소셜 로그인 (1)
소셜 로그인 로컬에서 회원가입과 로그인은 사용자에게 불편함을 가져다주기도 합니다. 사용자의 정보를 안전하게 관리하고 있더라도 사용자는 그것을 알 수 없기에 자기 자신의 정보를 서비
jongseoung.tistory.com
1. 네이버 로그인 믹스인 구현
NaverLoginMixin는 장고에서 제공하는 클래스가 아니라 직접 구현해야 합니다.
user앱에 oauth라는 패키지를 생성하고 그 안에 providers라는 패키지를 생성합니다. providers패키지에 naver.py라는 파일을 생성 후 아래와 같이 코드를 추가합니다.
# user/oauth/providers/naver.py
from django.conf import settings
from django.contrib.auth import login
class NaverLoginMixin:
naver_client = NaverClient()
def login_with_naver(self, state, code):
# 인증토근 발급
is_success, token_infos = self.naver_client.get_access_token(state, code)
if not is_success:
return False, '{} [{}]'.format(token_infos.get('error_desc'), token_infos.get('error'))
access_token = token_infos.get('access_token')
refresh_token = token_infos.get('refresh_token')
expires_in = token_infos.get('expires_in')
token_type = token_infos.get('token_type')
# 네이버 프로필 얻기
is_success, profiles = self.get_naver_profile(access_token, token_type)
if not is_success:
return False, profiles
# 사용자 생성 또는 업데이트
user, created = self.model.objects.get_or_create(email=profiles.get('email'))
if created: # 사용자 생성할 경우
user.set_password(None)
user.name = profiles.get('name')
user.is_active = True
user.save()
# 로그인
login(self.request, user, 'user.oauth.backends.NaverBackend') # NaverBackend 를 통한 인증 시도
# 세션데이터 추가
self.set_session(access_token=access_token, refresh_token=refresh_token, expires_in=expires_in, token_type=token_type)
return True, user
def get_naver_profile(self, access_token, token_type):
is_success, profiles = self.naver_client.get_profile(access_token, token_type)
if not is_success:
return False, profiles
for profile in self.required_profiles:
if profile not in profiles:
return False, '{}은 필수정보입니다. 정보제공에 동의해주세요.'.format(profile)
return True, profiles
NaverLoginMixin에서 네이버의 api를 구현한 네이버 클라이언트를 naver_client클래스 변수로 추가했습니다. 네이버 인증 토큰 발급과 프로필 정보를 가져오는 두 가지의 기능을 제공합니다.
login_with_naver메서드는 naver_client로부터 token_infos 객체를 전달하는데 token_infors객체는 아래와 같은 키를 가지는 딕셔너리 객체입니다.
- error - 에러코드
- error_description - 에러메시지
- access_token - 인증 토큰
- refresh_token - 인증 토큰 재발급 토큰
- expires_in - 인증 토큰 만료기한
- token_type - 인증 토큰 사용하는 api 호출 시 인증 방식
만일 인증 토큰을 받아오는데 실패했다면 에러메시지와 함께 함수를 바로 종료합니다.
인증토큰이 정상적으로 발급되었다면 회원가입을 위해 이메일과 사용자의 이름을 받아야 하는데, 네이버에서 profile api도 제공해 주기 때문에 이것을 이용하면 됩니다. get_naver_profile 메서드는 api를 통해 받아온 프로필 정보를 검증하는 역할을 합니다. 프로필 정보는 사용자가 제공한 항목에 선택한 결과 값들과 사용자의 id 값만 전달되는데 만일 이메일이나 이름을 선택하지 않을 경우 에러 메시지를 반환합니다.
프로필정보까지 정상적으로 받아오면 사용자 모델에서 get_or_create메서드를 통해 동일한 이메일의 사용자가 있는지 확인 후 없으면 새로 수정합니다. 소셜로그인은 가입과 로그인을 동시에 제공하는 것이 좋습니다. 이미 가입되어 있는 사용자라면 회원정보만 수정하면 되고, 가입되어 있지 않은 케이스라면 새로운 회원정보를 생성해서 가입시켜 줍니다. 소셜로그인은 로컬 비밀번호가 필요 없기 때문에 새로운 사용자 데이터가 추가되는 경우라면 set_password(None) 메서드를 통해 랜덤 한 비밀번호를 생성해서 저장합니다. 이미 소셜로그인을 통해서 이메일에 대한 인증도 되어 있으니 is_active값도 활성화시켜 주고 저장을 하면 가입이 완료됩니다. 만일 이미 가입되어 있던 사용자라면 이메일과 비밀번호로도 로그인이 가능하고 네이버 소셜로그인으로 로그인이 가능합니다.
가입된 이후에 로그인 처리까지 해줘야 합니다. 로그인은 auth프레임 워크의 login함수를 이용합니다. login 함수는 사용자 데이터와 로그인 처리를 해줄 인증백엔드의 경로가 필요합니다. 기본 인증 모듈인 'django.contrib.auth.backends.Modelbackend'는 username(email)과 비밀번호를 이용해서 인증처리를 하는데 소셜 로그인은 비밀번호를 전달받을 수가 없습니다. 어쩔 수 없이 소셜 로그인을 위한 인증 백엔드를 추가로 구현해줘야 합니다.
소셜로그인의 마지막은 세션 정보에 인증 토큰 정보를 추가하는 것입니다. 현재는 인증토큰이 필요 없겠지만 네이버 api를 이용한 기능을 제공할 경우도 있습니다. 이때 사용자의 인증토큰이 있어야만 사용자의 권한으로 네이버 서비스 api기능을 제공할 수 있는데 매번 재 로그인을 할 수 없으니 인증토큰과 그 외 정보들을 세션에 저장합니다. 인증토큰 재발급토큰도 함께 저장해야 인증토큰이 만료가 되더라도 재발급 토큰으로 다시 인증 토큰을 갱신할 수 있습니다. 만일 재발급 토큰도 만료가 되었거나 문제가 있어서 인증토큰을 갱신할 수 없다면 로그아웃 처리해 줍니다.
2. 네이버 클라이언트 구현
네이버 api를 호출하는 모듈이 NaverLoginMixin에서 필요합니다. 네이버 api는 인증과 관련된 부분과 서비스 api를 통칭하지만 현재 인증과 관련된 api만 구현할 것입니다. 그래서 네이버 로그인 미그린과 동일한 패키지에 구현해도 되지만 나중에 네이버 서비스 api도 구현하게 된다면 이것을 외부로 분리시키는 것이 좋습니다.
네이버의 api를 호출할 때 request라이브러리를 사용하여 호출하도록 했습니다. requsets는 파이썬의 표준 http 클라이언트보다 사영하기 간편하고 직관적입니다.
우선 requests 라이브러리를 먼저 설치해야 합니다.
pip install requests
NaverLoginMixin이 정의된 ouath/providers/naver.py에 NaverClient라는 클래스를 추가하고 아래와 같이 정의합니다.
# user/oauth/providers/naver.py
import requests
class NaverClient:
client_id = settings.NAVER_CLIENT_ID
secret_key = settings.NAVER_SECRET_KEY
grant_type = 'authorization_code'
auth_url = 'https://nid.naver.com/oauth2.0/token'
profile_url = 'https://openapi.naver.com/v1/nid/me'
__instance = None
def __new__(cls, *args, **kwargs):
if not isinstance(cls.__instance, cls):
cls.__instance = super().__new__(cls, *args, **kwargs)
return cls.__instance
def get_access_token(self, state, code):
res = requests.get(self.auth_url, params={'client_id': self.client_id, 'client_secret': self.secret_key,
'grant_type': self.grant_type, 'state': state, 'code': code})
return res.ok, res.json()
def get_profile(self, access_token, token_type='Bearer'):
res = requests.get(self.profile_url, headers={'Authorization': '{} {}'.format(token_type, access_token)}).json()
if res.get('resultcode') != '00':
return False, res.get('message')
else:
return True, res.get('response')
requests 모듈의 사용법은 get, post, put, delete 등의 함수들이 구현되어 있고, 각각의 함수명과 동일한 http 메서드로 요청을 합니다. 첫 번째 위치 인자는 url이고 그 외 피라미터는 keyword인자로 전달합니다. get_profile 메서드에서 headers라는 피라미터가 사용되는데 http 헤더의 값을 딕셔너리 형태로 전달합니다. Authorization헤더를 token_type(bearer)와 인증토큰을 조합한 값을 추가했습니다. 각 함수의 반환 데이터는 json메서드를 통해 본문의 내용을 딕셔너리 형태로 반환해 줄 수 도 있습니다.
singleton이라는 패턴이 들어가 있는데 첫 번째 생성자 호출 때만 객체만 생성시키고 이후 생성자 호출부터는 먼저 생성된 객체를 공유하게 하는 방식입니다. NaverClient 클래스를 NaverLoginMixin 뿐만 아니라 다른 클래스에서도 공유하며 사용할 수 있습니다. NaverClient객체는 인스턴스 변수가 없기 때문에 하나의 객체를 서로 공유하더라도 문제가 발생하지 않습니다. 이렇게 인스턴스 변수가 존재하지 않으나 여러 클래스에서 유틸리티처럼 사용하는 클래스의 경우 싱글턴 패턴을 많이 사용합니다. 객체를 생성하는 비용이 줄어 서버의 가용성을 높이는 좋은 패턴입니다.
3. 인증백엔드 구현
NaverLoginMixin에서 로그인할 때 인증백엔드를 'user.oauth.backends.NaverBackend'로 전달했습니다. 인증 백엔드의 경로대로 oauth패키지에 backends.py 파일을 추가하고 아래의 클래스를 생성해 줍니다.
# user/oauth/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import AnonymousUser
UserModel = get_user_model()
class NaverBackend(ModelBackend):
def authenticate(self, request, username=None,**kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
pass
else:
if self.user_can_authenticate(user):
return user
NaverBackend 백엔드는 기본인증백엔드를 상속받아 대부분의 기능들을 그대로 사용합니다. authenticate 메서드에서 비밀번호를 비교하여 인증하는 부분이 있는데 이 부분을 삭제하고 소셜로그인으로 email만 비교하도록 하였습니다. 만일 email이 사용자 테이블에 존재하지 않는다면 None를 반환해 주면 됩니다. 함수에서 아무것도 반환하지 않으면 None를 리턴하므로 사용자데이터 검색에 실패할 경우 아무것도 하지 않도록 설정하였습니다.
user_can_authenticate메서드는 사용자 데이터의 is_active가 True인지 확인하는 기능을 제공합니다. 비밀번호와 관계가 없으니 이것을 확인하는 것으로 인증백엔드의 인증테스트를 종료합니다.
소셜로그인은 이미 네이버에게 인증을 위임했기 때문에 인증백엔드에서 추가로 인증할 것이 별로 없습니다. 다만 사용자 모델의 정이가 이전 예제와 다르게 무언가 추가로 인증해야 할 필드들이 생겼을 경우에만 해당 필드를 이용해서 추가 인증을 하면 됩니다.
4. 인증백엔드 설정
인증백엔드는 NaverLoginMixin에서 사용을 하지만 이것은 로그인을 시도할 때 어떤 백엔드를 사용할지에 대한 설정입니다. 이후 로그인된 상태에서 또 다른 요청을 할 때장고는 세션의 정보를 확인하여 로그인된 사용자가 맞는지 맞다면 어떤 사용자인지를 식별하는데 장고의 기본값인 기본 인증 백엔드를 통해 식별처리를 실행합니다. 소셜로그인으로 로그인 사용자를 위해 설정파일의 AUTHENTICATION_BACKENDS변수에 NaverBacked를 추가합니다. AUTHENTICATION_BACKENDS는 설정의 세션의 사용자 정보를 식별할 때 사용될 백엔드를 리스트로 설정하여 실제 사용자 정보를 식별할 때 리스트의 순서대로 백엔드에 인증을 시도하고, 인증이 되면 해당 인증된 사용자 정보를 넘겨주고, 인증에 실패할 경우 리스트의 다음 백엔드를 위임하게 됩니다. 모든 백엔드에서 인증에 실패할 경우 인증되지 않은 사용자로 처리하는 것이다.
# djangotutorial/settings.py
# 생략
NAVER_CLIENT_ID = 'your client id'
NAVER_SECRET_KEY = 'your secret key'
AUTHENTICATION_BACKENDS = [
'user.oauth.backends.NaverBackend', # 네이버 인증백엔드
'django.contrib.auth.backends.ModelBackend'
]
# 생략
5. next query 파라미터 처리
로그인하지 않고 새 글 작성 시 url이 아래와 같이 next가 뜨는 것이 아니라 작성이 된 되면 bbs/view.py의 부분을 아래와 같이 수정해 주면 된다.

from django.contrib.auth.mixins import LoginRequiredMixin
...
class ArticleCreateUpdateView(LoginRequiredMixin, TemplateView):
login_url = settings.LOGIN_URL
template_name = 'article_update.html'
queryset = Article.objects.all()
pk_url_kwargs = 'article_id'
...
\현재 소셜로그인을 성공할 경우 무조건 settings.LOGIN_REDIRECT_URL로 이동하는데 next query 파라미터가 있는 경우 소셜 로그인 이후 해당 url로 이동하도록 수정하겠습니다.
우선 자바스크립트의 redirect_url을 통해 next query파라미터를 전달하겠습니다.
// user/static/user/js/social_login.js
function buildQuery(params) {
return Object.keys(params).map(function (key) {return key + '=' + encodeURIComponent(params[key])}).join('&')
}
function buildUrl(baseUrl, queries) {
return baseUrl + '?' + buildQuery(queries)
}
function naverLogin() {
params = {
response_type: 'code',
client_id:'nfenn0pzKTlihOzu_h8S',
redirect_uri: location.origin + '/user/login/social/naver/callback/' + location.search,
state: document.querySelector('[name=csrfmiddlewaretoken]').value
}
url = buildUrl('https://nid.naver.com/oauth2.0/authorize', params)
location.replace(url)
}
query피라미터가 있을 경우 redirect_url에도 그대로 추가하도록 했습니다. 다음으로는 callbackview에서 next query 피라미터를 읽고 소셜로그인이 성공했을 경우 해당 url로 이동하도록 수정합니다.
# user/views.py
# 생략
class SocialLoginCallbackView(NaverLoginMixin, View):
success_url = settings.LOGIN_REDIRECT_URL
failure_url = settings.LOGIN_URL
required_profiles = ['email', 'name']
model = get_user_model()
def get(self, request, *args, **kwargs):
provider = kwargs.get('provider')
success_url = request.GET.get('next', self.success_url)
if provider == 'naver':
csrf_token = request.GET.get('state')
code = request.GET.get('code')
if not _compare_salted_tokens(csrf_token, request.COOKIES.get('csrftoken')):
messages.error(request, '잘못된 경로로 로그인하셨습니다.', extra_tags='danger')
return HttpResponseRedirect(self.failure_url)
is_success, error = self.login_with_naver(csrf_token, code)
if not is_success:
messages.error(request, error, extra_tags='danger')
return HttpResponseRedirect(success_url if is_success else self.failure_url)
return HttpResponseRedirect(self.failure_url)
def set_session(self, **kwargs):
for key, value in kwargs.items():
self.request.session[key] = value
간단하게 success_url = request.GET.get('next', self.success_url)로 지역변수 success_url을 정의했습니다. 소셜로그인이 성공한 이후에도 success_url 지역변수를 이용해 이동하도록 변경하였습니다.
이제 로그아웃된 상태에서 새 글 작성버튼을 클릭하면 로그인 화면으로 이동됩니다.
6. urlpatterns 추가 등록
urlpatterns에 SocialloginCallView를 등록하면 소셜로그인 기능이 완료됩니다.
from django.contrib import admin
from django.urls import path
# 작성한 핸들러를 사용할 수 있게 가져옵니다.
from bbs.views import hello, ArticleListView, ArticleCreateUpdateView, ArticleDetailView
from user.views import UserRegistrationView, UserLoginView, UserVerificationView, ResendVerifyEmailView, SocialLoginCallbackView
from django.contrib.auth.views import LogoutView # 로그아웃
urlpatterns = [
path('hello/<to>', hello), # 'hello/'라는 경로로 접근했을 때 hello 핸들러가 호출되도록 추가합니다.
path('admin/', admin.site.urls),
path('article/', ArticleListView.as_view()),
path('article/create/', ArticleCreateUpdateView.as_view()),
path('article/<article_id>/', ArticleDetailView.as_view()),
path('article/<article_id>/update/', ArticleCreateUpdateView.as_view()),
path('user/create/', UserRegistrationView.as_view()), # 회원가입
path('user/<pk>/verify/<token>/', UserVerificationView.as_view()), # 인증
path('user/resend_verify_email/', ResendVerifyEmailView.as_view()), # 이메일 재 인증
path('user/login/', UserLoginView.as_view()), # 로그인
path('user/logout/', LogoutView.as_view()), # 로그아웃
path('user/login/social/<provider>/callback/', SocialLoginCallbackView.as_view()), # 소셜로그인
]
굳이 하나의 클래스에 구현하지 않고 네이버 콜백 클래스로 변형해도 된다.
회원가입 화면에 소셜 로그인 추가
회원가입 템플릿을 조금 수정하여 회원가입할 때 소셜로그인을 통한 회원가입을 해주겠습니다.
<!-- user/templates/registration_form.html -->
{% extends 'base.html' %}
{% load static%}
{% load i18n %}
{% block title %}<title>회원 가입</title>{% endblock %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/user.css' %}">
{% endblock css %}
{% block content %}
<div class="panel panel-default registration">
<div class="panel-heading">
가입하기
</div>
<div class="panel-body">
<form action="." method="post">
{% csrf_token %}
{% include 'form_field.html' with form=form %}
<div class="form-actions">
<button class="btn btn-primary btn-large" type="submit">가입하기</button>
</div>
</form>
</div>
</div>
{% include 'social_login_panel.html' %}
{% endblock content %}
include 템플릿 태그 하나로 소셜로그인을 추가해 주었습니다. 소셜로그인이라고 표시되는 것이 보기 싫으면 인자로 include템플릿태그에 with 키워드로 패널이름을 넘겨주면 된다. 방법은 아래와 같이 진행하면 된다.
우선 social_login_panel.html 템플릿에서 panel_name이라는 변수로 소셜로그인이라는 텍스트를 대체한다
<!-- user/templates/social_login_panel.html -->
{% load static %}
{% static 'img/kakao_login.png' as kakao_button %}
{% static 'img/kakao_login_ov.png' as kakao_button_hover %}
{% static 'img/naver_login_green.png' as naver_button %}
{% static 'img/naver_login_white.png' as naver_button_hover %}
<div class="panel panel-default user-panel">
<div class="panel-heading">
{{ panel_name }}
</div>
<div class="panel-body text-center">
<div class="pull-left">
<a href="#" onclick="kakaoLogin()">
<img src="{{ kakao_button }}"
onmouseover="this.src='{{ kakao_button_hover }}'"
onmouseleave="this.src='{{ kakao_button }}'"height="34">
</a>
</div>
<div class="pull-right">
<a href="#" onclick="naverLogin()">
<img src="{{ naver_button }}"
onmouseover="this.src='{{ naver_button_hover }}'"
onmouseleave="this.src='{{ naver_button }}'" height="34">
</a>
</div>
</div>
</div>
<script src="{% static 'js/social_login.js' %}"></script>
login_form.html과 registration_form.html의 include템플릿태그를 수정합니다.
<!-- user/templates/login_form.html -->
<!-- 생략 -->
{% include 'social_login_panel.html' with panel_name='간편 로그인' %}
<!-- 생략 -->
<!-- user/templates/registration_form.html -->
<!-- 생략 -->
{% include 'social_login_panel.html' with panel_name='간편 회원가입' %}
<!-- 생략 -->

위처럼 출력이 되었다면 정상적으로 된 것입니다.
소셜로그인 앱 분리
소셜 로그인 기능은 user앱과는 별도의 앱으로 분리해서 사용해도 됩니다. 여기에서는 user 앱 내부에 소셜 로그인 기능을 내장하도록 하였습니다. 소셜로그인 기능을 분리하고 싶다면 static 파일들과 뷰 그리고 oauth 패키지를 따로 앱으로 분리하면 됩니다.
- 설정파일에 AUTHENTICATION_BACKENDS, NAVER_CLIENT_ID, NAVER_SCRET_KEY설정
- urlpatterns에 CallbackView 등록
- 템플릿 생성 및 naverLogin() 호출, 템플릿은 플젝트에 맞게 생성, 네이버 로그인의 onclick속성을 naverLogin으로 설정
앱을 분리하는 것은 좋은 방법이지만 분리된 앱을 사용할 때 문제가 되는 점이 없는지 확인해 보고 문제가 발생하지 않도록 처리를 해줘야 합니다.
'Django > DRF' 카테고리의 다른 글
[Project] 웹 포트폴리오 제작 - 기본 설정 및 모델 , Admin 만들기 (0) | 2023.01.31 |
---|---|
[Django tutorial] 사용자 인증 (0) | 2023.01.30 |
[Django tutorial] 사용자 인증 - 소셜 로그인 (1) (0) | 2023.01.21 |
[Django tutorial] 파일 리팩토링 (1) | 2023.01.20 |
[Django tutorial] 사용자 인증 - 이메일 인증, 로그아웃 (0) | 2023.01.17 |
이전 내용
[Django tutorial] 사용자 인증 - 소셜 로그인 (1)
소셜 로그인 로컬에서 회원가입과 로그인은 사용자에게 불편함을 가져다주기도 합니다. 사용자의 정보를 안전하게 관리하고 있더라도 사용자는 그것을 알 수 없기에 자기 자신의 정보를 서비
jongseoung.tistory.com
1. 네이버 로그인 믹스인 구현
NaverLoginMixin는 장고에서 제공하는 클래스가 아니라 직접 구현해야 합니다.
user앱에 oauth라는 패키지를 생성하고 그 안에 providers라는 패키지를 생성합니다. providers패키지에 naver.py라는 파일을 생성 후 아래와 같이 코드를 추가합니다.
# user/oauth/providers/naver.py
from django.conf import settings
from django.contrib.auth import login
class NaverLoginMixin:
naver_client = NaverClient()
def login_with_naver(self, state, code):
# 인증토근 발급
is_success, token_infos = self.naver_client.get_access_token(state, code)
if not is_success:
return False, '{} [{}]'.format(token_infos.get('error_desc'), token_infos.get('error'))
access_token = token_infos.get('access_token')
refresh_token = token_infos.get('refresh_token')
expires_in = token_infos.get('expires_in')
token_type = token_infos.get('token_type')
# 네이버 프로필 얻기
is_success, profiles = self.get_naver_profile(access_token, token_type)
if not is_success:
return False, profiles
# 사용자 생성 또는 업데이트
user, created = self.model.objects.get_or_create(email=profiles.get('email'))
if created: # 사용자 생성할 경우
user.set_password(None)
user.name = profiles.get('name')
user.is_active = True
user.save()
# 로그인
login(self.request, user, 'user.oauth.backends.NaverBackend') # NaverBackend 를 통한 인증 시도
# 세션데이터 추가
self.set_session(access_token=access_token, refresh_token=refresh_token, expires_in=expires_in, token_type=token_type)
return True, user
def get_naver_profile(self, access_token, token_type):
is_success, profiles = self.naver_client.get_profile(access_token, token_type)
if not is_success:
return False, profiles
for profile in self.required_profiles:
if profile not in profiles:
return False, '{}은 필수정보입니다. 정보제공에 동의해주세요.'.format(profile)
return True, profiles
NaverLoginMixin에서 네이버의 api를 구현한 네이버 클라이언트를 naver_client클래스 변수로 추가했습니다. 네이버 인증 토큰 발급과 프로필 정보를 가져오는 두 가지의 기능을 제공합니다.
login_with_naver메서드는 naver_client로부터 token_infos 객체를 전달하는데 token_infors객체는 아래와 같은 키를 가지는 딕셔너리 객체입니다.
- error - 에러코드
- error_description - 에러메시지
- access_token - 인증 토큰
- refresh_token - 인증 토큰 재발급 토큰
- expires_in - 인증 토큰 만료기한
- token_type - 인증 토큰 사용하는 api 호출 시 인증 방식
만일 인증 토큰을 받아오는데 실패했다면 에러메시지와 함께 함수를 바로 종료합니다.
인증토큰이 정상적으로 발급되었다면 회원가입을 위해 이메일과 사용자의 이름을 받아야 하는데, 네이버에서 profile api도 제공해 주기 때문에 이것을 이용하면 됩니다. get_naver_profile 메서드는 api를 통해 받아온 프로필 정보를 검증하는 역할을 합니다. 프로필 정보는 사용자가 제공한 항목에 선택한 결과 값들과 사용자의 id 값만 전달되는데 만일 이메일이나 이름을 선택하지 않을 경우 에러 메시지를 반환합니다.
프로필정보까지 정상적으로 받아오면 사용자 모델에서 get_or_create메서드를 통해 동일한 이메일의 사용자가 있는지 확인 후 없으면 새로 수정합니다. 소셜로그인은 가입과 로그인을 동시에 제공하는 것이 좋습니다. 이미 가입되어 있는 사용자라면 회원정보만 수정하면 되고, 가입되어 있지 않은 케이스라면 새로운 회원정보를 생성해서 가입시켜 줍니다. 소셜로그인은 로컬 비밀번호가 필요 없기 때문에 새로운 사용자 데이터가 추가되는 경우라면 set_password(None) 메서드를 통해 랜덤 한 비밀번호를 생성해서 저장합니다. 이미 소셜로그인을 통해서 이메일에 대한 인증도 되어 있으니 is_active값도 활성화시켜 주고 저장을 하면 가입이 완료됩니다. 만일 이미 가입되어 있던 사용자라면 이메일과 비밀번호로도 로그인이 가능하고 네이버 소셜로그인으로 로그인이 가능합니다.
가입된 이후에 로그인 처리까지 해줘야 합니다. 로그인은 auth프레임 워크의 login함수를 이용합니다. login 함수는 사용자 데이터와 로그인 처리를 해줄 인증백엔드의 경로가 필요합니다. 기본 인증 모듈인 'django.contrib.auth.backends.Modelbackend'는 username(email)과 비밀번호를 이용해서 인증처리를 하는데 소셜 로그인은 비밀번호를 전달받을 수가 없습니다. 어쩔 수 없이 소셜 로그인을 위한 인증 백엔드를 추가로 구현해줘야 합니다.
소셜로그인의 마지막은 세션 정보에 인증 토큰 정보를 추가하는 것입니다. 현재는 인증토큰이 필요 없겠지만 네이버 api를 이용한 기능을 제공할 경우도 있습니다. 이때 사용자의 인증토큰이 있어야만 사용자의 권한으로 네이버 서비스 api기능을 제공할 수 있는데 매번 재 로그인을 할 수 없으니 인증토큰과 그 외 정보들을 세션에 저장합니다. 인증토큰 재발급토큰도 함께 저장해야 인증토큰이 만료가 되더라도 재발급 토큰으로 다시 인증 토큰을 갱신할 수 있습니다. 만일 재발급 토큰도 만료가 되었거나 문제가 있어서 인증토큰을 갱신할 수 없다면 로그아웃 처리해 줍니다.
2. 네이버 클라이언트 구현
네이버 api를 호출하는 모듈이 NaverLoginMixin에서 필요합니다. 네이버 api는 인증과 관련된 부분과 서비스 api를 통칭하지만 현재 인증과 관련된 api만 구현할 것입니다. 그래서 네이버 로그인 미그린과 동일한 패키지에 구현해도 되지만 나중에 네이버 서비스 api도 구현하게 된다면 이것을 외부로 분리시키는 것이 좋습니다.
네이버의 api를 호출할 때 request라이브러리를 사용하여 호출하도록 했습니다. requsets는 파이썬의 표준 http 클라이언트보다 사영하기 간편하고 직관적입니다.
우선 requests 라이브러리를 먼저 설치해야 합니다.
pip install requests
NaverLoginMixin이 정의된 ouath/providers/naver.py에 NaverClient라는 클래스를 추가하고 아래와 같이 정의합니다.
# user/oauth/providers/naver.py
import requests
class NaverClient:
client_id = settings.NAVER_CLIENT_ID
secret_key = settings.NAVER_SECRET_KEY
grant_type = 'authorization_code'
auth_url = 'https://nid.naver.com/oauth2.0/token'
profile_url = 'https://openapi.naver.com/v1/nid/me'
__instance = None
def __new__(cls, *args, **kwargs):
if not isinstance(cls.__instance, cls):
cls.__instance = super().__new__(cls, *args, **kwargs)
return cls.__instance
def get_access_token(self, state, code):
res = requests.get(self.auth_url, params={'client_id': self.client_id, 'client_secret': self.secret_key,
'grant_type': self.grant_type, 'state': state, 'code': code})
return res.ok, res.json()
def get_profile(self, access_token, token_type='Bearer'):
res = requests.get(self.profile_url, headers={'Authorization': '{} {}'.format(token_type, access_token)}).json()
if res.get('resultcode') != '00':
return False, res.get('message')
else:
return True, res.get('response')
requests 모듈의 사용법은 get, post, put, delete 등의 함수들이 구현되어 있고, 각각의 함수명과 동일한 http 메서드로 요청을 합니다. 첫 번째 위치 인자는 url이고 그 외 피라미터는 keyword인자로 전달합니다. get_profile 메서드에서 headers라는 피라미터가 사용되는데 http 헤더의 값을 딕셔너리 형태로 전달합니다. Authorization헤더를 token_type(bearer)와 인증토큰을 조합한 값을 추가했습니다. 각 함수의 반환 데이터는 json메서드를 통해 본문의 내용을 딕셔너리 형태로 반환해 줄 수 도 있습니다.
singleton이라는 패턴이 들어가 있는데 첫 번째 생성자 호출 때만 객체만 생성시키고 이후 생성자 호출부터는 먼저 생성된 객체를 공유하게 하는 방식입니다. NaverClient 클래스를 NaverLoginMixin 뿐만 아니라 다른 클래스에서도 공유하며 사용할 수 있습니다. NaverClient객체는 인스턴스 변수가 없기 때문에 하나의 객체를 서로 공유하더라도 문제가 발생하지 않습니다. 이렇게 인스턴스 변수가 존재하지 않으나 여러 클래스에서 유틸리티처럼 사용하는 클래스의 경우 싱글턴 패턴을 많이 사용합니다. 객체를 생성하는 비용이 줄어 서버의 가용성을 높이는 좋은 패턴입니다.
3. 인증백엔드 구현
NaverLoginMixin에서 로그인할 때 인증백엔드를 'user.oauth.backends.NaverBackend'로 전달했습니다. 인증 백엔드의 경로대로 oauth패키지에 backends.py 파일을 추가하고 아래의 클래스를 생성해 줍니다.
# user/oauth/backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import AnonymousUser
UserModel = get_user_model()
class NaverBackend(ModelBackend):
def authenticate(self, request, username=None,**kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
pass
else:
if self.user_can_authenticate(user):
return user
NaverBackend 백엔드는 기본인증백엔드를 상속받아 대부분의 기능들을 그대로 사용합니다. authenticate 메서드에서 비밀번호를 비교하여 인증하는 부분이 있는데 이 부분을 삭제하고 소셜로그인으로 email만 비교하도록 하였습니다. 만일 email이 사용자 테이블에 존재하지 않는다면 None를 반환해 주면 됩니다. 함수에서 아무것도 반환하지 않으면 None를 리턴하므로 사용자데이터 검색에 실패할 경우 아무것도 하지 않도록 설정하였습니다.
user_can_authenticate메서드는 사용자 데이터의 is_active가 True인지 확인하는 기능을 제공합니다. 비밀번호와 관계가 없으니 이것을 확인하는 것으로 인증백엔드의 인증테스트를 종료합니다.
소셜로그인은 이미 네이버에게 인증을 위임했기 때문에 인증백엔드에서 추가로 인증할 것이 별로 없습니다. 다만 사용자 모델의 정이가 이전 예제와 다르게 무언가 추가로 인증해야 할 필드들이 생겼을 경우에만 해당 필드를 이용해서 추가 인증을 하면 됩니다.
4. 인증백엔드 설정
인증백엔드는 NaverLoginMixin에서 사용을 하지만 이것은 로그인을 시도할 때 어떤 백엔드를 사용할지에 대한 설정입니다. 이후 로그인된 상태에서 또 다른 요청을 할 때장고는 세션의 정보를 확인하여 로그인된 사용자가 맞는지 맞다면 어떤 사용자인지를 식별하는데 장고의 기본값인 기본 인증 백엔드를 통해 식별처리를 실행합니다. 소셜로그인으로 로그인 사용자를 위해 설정파일의 AUTHENTICATION_BACKENDS변수에 NaverBacked를 추가합니다. AUTHENTICATION_BACKENDS는 설정의 세션의 사용자 정보를 식별할 때 사용될 백엔드를 리스트로 설정하여 실제 사용자 정보를 식별할 때 리스트의 순서대로 백엔드에 인증을 시도하고, 인증이 되면 해당 인증된 사용자 정보를 넘겨주고, 인증에 실패할 경우 리스트의 다음 백엔드를 위임하게 됩니다. 모든 백엔드에서 인증에 실패할 경우 인증되지 않은 사용자로 처리하는 것이다.
# djangotutorial/settings.py
# 생략
NAVER_CLIENT_ID = 'your client id'
NAVER_SECRET_KEY = 'your secret key'
AUTHENTICATION_BACKENDS = [
'user.oauth.backends.NaverBackend', # 네이버 인증백엔드
'django.contrib.auth.backends.ModelBackend'
]
# 생략
5. next query 파라미터 처리
로그인하지 않고 새 글 작성 시 url이 아래와 같이 next가 뜨는 것이 아니라 작성이 된 되면 bbs/view.py의 부분을 아래와 같이 수정해 주면 된다.

from django.contrib.auth.mixins import LoginRequiredMixin
...
class ArticleCreateUpdateView(LoginRequiredMixin, TemplateView):
login_url = settings.LOGIN_URL
template_name = 'article_update.html'
queryset = Article.objects.all()
pk_url_kwargs = 'article_id'
...
\현재 소셜로그인을 성공할 경우 무조건 settings.LOGIN_REDIRECT_URL로 이동하는데 next query 파라미터가 있는 경우 소셜 로그인 이후 해당 url로 이동하도록 수정하겠습니다.
우선 자바스크립트의 redirect_url을 통해 next query파라미터를 전달하겠습니다.
// user/static/user/js/social_login.js
function buildQuery(params) {
return Object.keys(params).map(function (key) {return key + '=' + encodeURIComponent(params[key])}).join('&')
}
function buildUrl(baseUrl, queries) {
return baseUrl + '?' + buildQuery(queries)
}
function naverLogin() {
params = {
response_type: 'code',
client_id:'nfenn0pzKTlihOzu_h8S',
redirect_uri: location.origin + '/user/login/social/naver/callback/' + location.search,
state: document.querySelector('[name=csrfmiddlewaretoken]').value
}
url = buildUrl('https://nid.naver.com/oauth2.0/authorize', params)
location.replace(url)
}
query피라미터가 있을 경우 redirect_url에도 그대로 추가하도록 했습니다. 다음으로는 callbackview에서 next query 피라미터를 읽고 소셜로그인이 성공했을 경우 해당 url로 이동하도록 수정합니다.
# user/views.py
# 생략
class SocialLoginCallbackView(NaverLoginMixin, View):
success_url = settings.LOGIN_REDIRECT_URL
failure_url = settings.LOGIN_URL
required_profiles = ['email', 'name']
model = get_user_model()
def get(self, request, *args, **kwargs):
provider = kwargs.get('provider')
success_url = request.GET.get('next', self.success_url)
if provider == 'naver':
csrf_token = request.GET.get('state')
code = request.GET.get('code')
if not _compare_salted_tokens(csrf_token, request.COOKIES.get('csrftoken')):
messages.error(request, '잘못된 경로로 로그인하셨습니다.', extra_tags='danger')
return HttpResponseRedirect(self.failure_url)
is_success, error = self.login_with_naver(csrf_token, code)
if not is_success:
messages.error(request, error, extra_tags='danger')
return HttpResponseRedirect(success_url if is_success else self.failure_url)
return HttpResponseRedirect(self.failure_url)
def set_session(self, **kwargs):
for key, value in kwargs.items():
self.request.session[key] = value
간단하게 success_url = request.GET.get('next', self.success_url)로 지역변수 success_url을 정의했습니다. 소셜로그인이 성공한 이후에도 success_url 지역변수를 이용해 이동하도록 변경하였습니다.
이제 로그아웃된 상태에서 새 글 작성버튼을 클릭하면 로그인 화면으로 이동됩니다.
6. urlpatterns 추가 등록
urlpatterns에 SocialloginCallView를 등록하면 소셜로그인 기능이 완료됩니다.
from django.contrib import admin
from django.urls import path
# 작성한 핸들러를 사용할 수 있게 가져옵니다.
from bbs.views import hello, ArticleListView, ArticleCreateUpdateView, ArticleDetailView
from user.views import UserRegistrationView, UserLoginView, UserVerificationView, ResendVerifyEmailView, SocialLoginCallbackView
from django.contrib.auth.views import LogoutView # 로그아웃
urlpatterns = [
path('hello/<to>', hello), # 'hello/'라는 경로로 접근했을 때 hello 핸들러가 호출되도록 추가합니다.
path('admin/', admin.site.urls),
path('article/', ArticleListView.as_view()),
path('article/create/', ArticleCreateUpdateView.as_view()),
path('article/<article_id>/', ArticleDetailView.as_view()),
path('article/<article_id>/update/', ArticleCreateUpdateView.as_view()),
path('user/create/', UserRegistrationView.as_view()), # 회원가입
path('user/<pk>/verify/<token>/', UserVerificationView.as_view()), # 인증
path('user/resend_verify_email/', ResendVerifyEmailView.as_view()), # 이메일 재 인증
path('user/login/', UserLoginView.as_view()), # 로그인
path('user/logout/', LogoutView.as_view()), # 로그아웃
path('user/login/social/<provider>/callback/', SocialLoginCallbackView.as_view()), # 소셜로그인
]
굳이 하나의 클래스에 구현하지 않고 네이버 콜백 클래스로 변형해도 된다.
회원가입 화면에 소셜 로그인 추가
회원가입 템플릿을 조금 수정하여 회원가입할 때 소셜로그인을 통한 회원가입을 해주겠습니다.
<!-- user/templates/registration_form.html -->
{% extends 'base.html' %}
{% load static%}
{% load i18n %}
{% block title %}<title>회원 가입</title>{% endblock %}
{% block css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/user.css' %}">
{% endblock css %}
{% block content %}
<div class="panel panel-default registration">
<div class="panel-heading">
가입하기
</div>
<div class="panel-body">
<form action="." method="post">
{% csrf_token %}
{% include 'form_field.html' with form=form %}
<div class="form-actions">
<button class="btn btn-primary btn-large" type="submit">가입하기</button>
</div>
</form>
</div>
</div>
{% include 'social_login_panel.html' %}
{% endblock content %}
include 템플릿 태그 하나로 소셜로그인을 추가해 주었습니다. 소셜로그인이라고 표시되는 것이 보기 싫으면 인자로 include템플릿태그에 with 키워드로 패널이름을 넘겨주면 된다. 방법은 아래와 같이 진행하면 된다.
우선 social_login_panel.html 템플릿에서 panel_name이라는 변수로 소셜로그인이라는 텍스트를 대체한다
<!-- user/templates/social_login_panel.html -->
{% load static %}
{% static 'img/kakao_login.png' as kakao_button %}
{% static 'img/kakao_login_ov.png' as kakao_button_hover %}
{% static 'img/naver_login_green.png' as naver_button %}
{% static 'img/naver_login_white.png' as naver_button_hover %}
<div class="panel panel-default user-panel">
<div class="panel-heading">
{{ panel_name }}
</div>
<div class="panel-body text-center">
<div class="pull-left">
<a href="#" onclick="kakaoLogin()">
<img src="{{ kakao_button }}"
onmouseover="this.src='{{ kakao_button_hover }}'"
onmouseleave="this.src='{{ kakao_button }}'"height="34">
</a>
</div>
<div class="pull-right">
<a href="#" onclick="naverLogin()">
<img src="{{ naver_button }}"
onmouseover="this.src='{{ naver_button_hover }}'"
onmouseleave="this.src='{{ naver_button }}'" height="34">
</a>
</div>
</div>
</div>
<script src="{% static 'js/social_login.js' %}"></script>
login_form.html과 registration_form.html의 include템플릿태그를 수정합니다.
<!-- user/templates/login_form.html -->
<!-- 생략 -->
{% include 'social_login_panel.html' with panel_name='간편 로그인' %}
<!-- 생략 -->
<!-- user/templates/registration_form.html -->
<!-- 생략 -->
{% include 'social_login_panel.html' with panel_name='간편 회원가입' %}
<!-- 생략 -->

위처럼 출력이 되었다면 정상적으로 된 것입니다.
소셜로그인 앱 분리
소셜 로그인 기능은 user앱과는 별도의 앱으로 분리해서 사용해도 됩니다. 여기에서는 user 앱 내부에 소셜 로그인 기능을 내장하도록 하였습니다. 소셜로그인 기능을 분리하고 싶다면 static 파일들과 뷰 그리고 oauth 패키지를 따로 앱으로 분리하면 됩니다.
- 설정파일에 AUTHENTICATION_BACKENDS, NAVER_CLIENT_ID, NAVER_SCRET_KEY설정
- urlpatterns에 CallbackView 등록
- 템플릿 생성 및 naverLogin() 호출, 템플릿은 플젝트에 맞게 생성, 네이버 로그인의 onclick속성을 naverLogin으로 설정
앱을 분리하는 것은 좋은 방법이지만 분리된 앱을 사용할 때 문제가 되는 점이 없는지 확인해 보고 문제가 발생하지 않도록 처리를 해줘야 합니다.
'Django > DRF' 카테고리의 다른 글
[Project] 웹 포트폴리오 제작 - 기본 설정 및 모델 , Admin 만들기 (0) | 2023.01.31 |
---|---|
[Django tutorial] 사용자 인증 (0) | 2023.01.30 |
[Django tutorial] 사용자 인증 - 소셜 로그인 (1) (0) | 2023.01.21 |
[Django tutorial] 파일 리팩토링 (1) | 2023.01.20 |
[Django tutorial] 사용자 인증 - 이메일 인증, 로그아웃 (0) | 2023.01.17 |