[컴][웹] Django 에서 social log in library 사용



아래 내용을 정리한다. 다만 여기선 Django 1.9 를 사용한다.
Tutorial: Adding Facebook/Twitter/Google Authentication to a Django Application - Art & Logic

위의 글에서 python-social-auth 의 사용법을 이야기 해준다.


설치 및 설정

절차

  • login 관련 application 만들기
    • manage.py startapp thirdlogin
    • INSTALLED_APPS 에 추가('thirdlogin')
    • login view 만들기, url 설정
  • python-social-auth 설치 및 설정
    • pip install python-social-auth
    • settings 설정
    • manage.py syncdb

위의 내용중에 설명이 필요한 것들만 아래 세부적인 사항을 적어놓았다.

login view 만들기

from django.template.context_processors import csrf
from django.views.generic import TemplateView

class HomeView(TemplateView):
    template_name = "third-base.html"

    def get(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        context.update(csrf(request))
        context.update({'user': request.user})

        return self.render_to_response(context)

    def get_context_data(self, **kwargs):
        context = super(HomeView, self).get_context_data(**kwargs)

        return context


login view url 설정

url(r'^home/$', HomeView.as_view(), name='home'),


python-social-auth 의 settings 설정

INSTALLED_APPS = (
   ...
   'social.apps.django_app.default',
   ...
)


TEMPLATES = [{
    ...
    'OPTIONS': {
        'debug': DEBUG,
        # see : https://docs.djangoproject.com/en/1.8/ref/templates/upgrading/
        'context_processors': [
            'django.contrib.auth.context_processors.auth',
            'django.template.context_processors.debug',
            'django.template.context_processors.i18n',
            'django.template.context_processors.media',
            'django.template.context_processors.static',
            'django.template.context_processors.tz',
            'django.contrib.messages.context_processors.messages',
            'django.template.context_processors.request',


            'social.apps.django_app.context_processors.backends',
            'social.apps.django_app.context_processors.login_redirect',



        ],
        ...
    },

}]

AUTHENTICATION_BACKENDS = [
   'social.backends.facebook.FacebookOAuth2',
   'social.backends.google.GoogleOAuth2',
   'social.backends.twitter.TwitterOAuth',
   'django.contrib.auth.backends.ModelBackend',
]


SOCIAL_AUTH_FACEBOOK_KEY  = "4891384931804"
SOCIAL_AUTH_FACEBOOK_SECRET  = "348920vdfds715d9fc2ce38154"



url 추가

여기서 auth 는 django 의 기본 logout 을 사용하기 위한 용도이다.
urlpatterns = patterns('',
 ...
 url('', include('social.apps.django_app.urls', namespace='social')),
 url('', include('django.contrib.auth.urls', namespace='auth')),
 ...
)


social.apps.django_app.urls

urls 부분만 조금 수정했다. 내용은 같지만, warning 만 줄인것이다.
social.apps.dango_app.urls.py

from social.apps.django_app import views



urlpatterns = [
    url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth, name='begin'),
    url(r'^complete/(?P<backend>[^/]+){0}$'.format(extra), views.complete, name='complete'),
    url(r'^disconnect/(?P<backend>[^/]+){0}$'.format(extra), views.disconnect, name='disconnect'),
    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+){0}$'
        .format(extra), views.disconnect, name='disconnect_individual'),
]



template

template 에서는 next 에 들어가는 부분이 login 후, 또는 logout 후에 redirect 되는 url 이 된다. next 가 설정되어 있지 않으면 LOGIN_REDIRECT_URL 를 사용한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Third-party Authentication Tutorial{% endblock %}</title>

    <!-- Bootstrap -->
    <link href="/static/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/css/bootstrap-theme.min.css" rel="stylesheet">
    <link href="/static/css/fbposter.css" rel="stylesheet">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
     <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
     <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
   <![endif]-->
</head>
<body>

<div>
    <h1>Third-party authentication demo</h1>

    <p>
        {% if user and not user.is_anonymous %}
            Hello {{ user.get_full_name|default:user.username }}!
            <a href="{% url 'auth:logout' %}?next={{ request.path }}">Logout</a><br>
        {% else %}
            I don’t think we’ve met before.

            <a href="{% url 'social:begin' 'facebook' %}?next={{ request.path }}">Login with Facebook</a>
        {% endif %}
    </p>
</div>

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>




pipeline

이 django social log in library(이하 psa) 에서는 pipeline 개념을 아는 것이 필요하다.

여기서 pipeline 은 function 들의 list 정도로 이해하면 될 것이다. psa 는 login 요청시에 이 pipeline 에 적혀있는 모든 function 들을 수행하게 된다.

그리고 이 function 들은 {}(dict) 나 None 을 return 한다. return 된 값은 이전 function 의 return 값과 merge 해서 다음 pipeline 에 parameter 로 넘어가게 된다.

여기서 pipeline 개념을 좀 더 명확히 알 수 있다.


만약 내가 login 과정중에 어떤 동작을 추가하고 싶다면, 내가 만든 function 을 이 pipeline list 의 원하는 위치에 추가해 주면 된다.

이 pipeline 이 끝나야 로그인이 완료된다. pipeline 중간에 user 가 이미 로그인 됐다는 것은 pipeline 시작시점에 이미 로그인 됐다는 것을 이야기한다.



pipline 에 email 인증 추가



facebook 의 scope 정의

기존에는 FACEBOOK_EXTENDED_PERMISSIONS 을 가지고 한다고 했지만, 변경되었다. 아래글에 따르면


FACEBOOK_AUTH_EXTRA_ARGUMENTS= {'scope': 'user_location,user_about_me,email'}

위의 것도 아니고, 아래 것이 맞다. 2016-01-19

SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
    'fields': 'id,name,email',
}

SCOPE / PROFILE_EXTRA_PARAMS 로 검색하면 facebook.py 에서 찾을 수 있다.

위에서 설정된 param 의 field 는 아래 login flow > complete 에서 self.get_json() 에서 확인 할 수 있다.



login flow


login 버튼을 누를때 아래 url 을 호출하게 한다. next 에는 login 이후에 갈 url 을 적으면 된다.
  • /login/facebook/?next=/thirdauth/home/
그러면 server 에서 아래 oauth url 로 redirect 하라고 응답을 browser 에 준다. 이 때 url 의 redirect_uri 에 /complete/facebook 이 set 된다.
  • https://www.facebook.com/v2.3/dialog/oauth?state=...&redirect_uri=...&client_id=...
그러면 이 redirect url 로 다시 request 를 날리면, facebook 서버에서 응답으로 /complete/facebook 으로 redirect 하라는 응답을 준다.
  • /complete/facebook/?redirect_state=...&code=...&state=...
그러면 이제 server 에서 login 에 대한 작업을 마무리 한다.


user 정보 db 저장

아래 do_complete 에 보면 user model 에 대한 작업이 있다. 이 부분에서 DB의
  • table social_auth_usersocialauth
에 facebook 의 user에 대한 uid 가 저장된다(?) 이때 auth_user 에도 user 정보가 새롭게 추가된다.



auth


social.apps.django_app.urls
    url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth, name='begin'),
social.apps.django_app.views
    def auth(request, backend):
        return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME)
social.actions
    def do_auth(backend, redirect_name='next'):
        # Clean any partial pipeline data
        backend.strategy.clean_partial_pipeline()

        # Save any defined next value into session
        data = backend.strategy.request_data(merge=False)

        
        ...

        # data 에 parameter 값들이 있다.
        if redirect_name in data:
            # Check and sanitize a user-defined GET/POST next field value
            redirect_uri = data[redirect_name]

            # 같은 host 의 redirect_uri 인지 검사
            if backend.setting('SANITIZE_REDIRECTS', True):
                redirect_uri = sanitize_redirect(backend.strategy.request_host(),
                                                 redirect_uri)


            backend.strategy.session_set(
                redirect_name,
                redirect_uri or backend.setting('LOGIN_REDIRECT_URL')
            )
        return backend.start()
social.strategies
class DjangoStrategy(BaseStrategy):
    ...
    def session_set(self, name, value):
        self.session[name] = value
        if hasattr(self.session, 'modified'):
            self.session.modified = True
social.backends.base
class BaseOAuth2(OAuthAuth):
    ...
    def start(self):    #class BaseAuth(object):
        # Clean any partial pipeline info before starting the process
        self.strategy.clean_partial_pipeline()
        if self.uses_redirect():
            # self.auth_url() -> 'https://www.facebook.com/v2.3/dialog/oauth?state=xrCXEWKYc2d5TwJnxanAnePJ7gKIL6xt&redirect_uri=http%3A%2F%2Flocal.com%3A9080%2Fcomplete%2Ffacebook%2F%3Fredirect_state%3DxrCXEWKYc2d5TwJnxanAnePJ7gKIL6xt&client_id='121353'
            return self.strategy.redirect(self.auth_url())
        else:
            return self.strategy.html(self.auth_html())
social.strategies
class DjangoStrategy(BaseStrategy):
    def redirect(self, url):
        return redirect(url)

django.shortcuts
def redirect(to, *args, **kwargs):
    if kwargs.pop('permanent', False):
        redirect_class = HttpResponsePermanentRedirect
    else:
        redirect_class = HttpResponseRedirect

    return redirect_class(resolve_url(to, *args, **kwargs))



complete

social.apps.django_app.urls
url(r'^complete/(?P<backend>[^/]+){0}$'.format(extra), views.complete, name='complete'),


social.apps.django_app.views
@never_cache
@csrf_exempt
@psa('{0}:complete'.format(NAMESPACE))
def complete(request, backend, *args, **kwargs):
    """Authentication complete view"""
    return do_complete(request.backend, _do_login, request.user,
                       redirect_name=REDIRECT_FIELD_NAME, *args, **kwargs)

social.actions
def do_complete(backend, login, user=None, redirect_name='next',
                *args, **kwargs):
    data = backend.strategy.request_data()
    # data = {'code': '...', 'redirect_state': '...', 'state': '...'}

    is_authenticated = user_is_authenticated(user)
    user = is_authenticated and user or None

    partial = partial_pipeline_data(backend, user, *args, **kwargs)
    if partial:
        ...
    else:
        user = backend.complete(user=user, *args, **kwargs)

    ...
    user_model = backend.strategy.storage.user.user_model()
    if user and not isinstance(user, user_model):
        return user

    if is_authenticated:
        ...
    elif user:
        if user_is_active(user):
            ...
    ...
    if backend.setting('SANITIZE_REDIRECTS', True):
        url = sanitize_redirect(backend.strategy.request_host(), url) or \
              backend.setting('LOGIN_REDIRECT_URL')
    return backend.strategy.redirect(url)


social.backends.base
class FacebookOAuth2(BaseAuth2):

    def complete(self, *args, **kwargs):    # class BaseAuth(object):
        return self.auth_complete(*args, **kwargs)

    @handle_http_errors
    def auth_complete(self, *args, **kwargs):
        ...
        state = self.validate_state()
        key, secret = self.get_key_and_secret()
        response = self.request(self.ACCESS_TOKEN_URL, params={
            'client_id': key,
            'redirect_uri': self.get_redirect_uri(state),
            'client_secret': secret,
            'code': self.data['code']
        })
        # API v2.3 returns a JSON, according to the documents linked at issue
        # #592, but it seems that this needs to be enabled(?), otherwise the
        # usual querystring type response is returned.
        try:
            response = response.json()
        except ValueError:
            response = parse_qs(response.text)
        access_token = response['access_token']
        return self.do_auth(access_token, response, *args, **kwargs)




    def request(self, url, method='GET', *args, **kwargs):   # class BaseAuth(object):
        kwargs.setdefault('headers', {})
        
        ...
        #... set kwargs

        try:
            if self.SSL_PROTOCOL:
                session = SSLHttpAdapter.ssl_adapter_session(self.SSL_PROTOCOL)
                response = session.request(method, url, *args, **kwargs)
            else:
                response = request(method, url, *args, **kwargs)
        except ConnectionError as err:
            raise AuthFailed(self, str(err))
        
        response.raise_for_status()
        return response
backends.facebook
    def do_auth(self, access_token, response=None, *args, **kwargs):
        ...
        data = self.user_data(access_token)
        ...
        data['access_token'] = access_token
        ...
        kwargs.update({'backend': self, 'response': data})
        return self.strategy.authenticate(*args, **kwargs)
    

    def authenticate(self, backend, *args, **kwargs):
        kwargs['strategy'] = self
        kwargs['storage'] = self.storage
        kwargs['backend'] = backend
        return authenticate(*args, **kwargs) # from django.contrib.auth import authenticate


    def user_data(self, access_token, *args, **kwargs):
        """Loads user data from service"""
        ...
        return self.get_json(self.USER_DATA_URL, params=params)

    def get_json(self, url, *args, **kwargs):
        return self.request(url, *args, **kwargs).json()



댓글 없음:

댓글 쓰기