[Django] (Source) login_required 데코레이터 구현 (feat. next 쿼리 생성)

azzurri21·2022년 1월 22일
0

Django

목록 보기
5/7

서론

점프투장고 3-07 모델 변경을 보면서 login_required 데코레이터를 처음 접했다. 뷰 호출 시 로그인 상태가 아니면 먼저 로그인 화면으로 리다이렉트 해주는 기능이다. 그 과정에서 next라는 url query 변수에 로그인 성공 후 이동할 url이 자동으로 전달되고, LoginView 함수는 next에 담긴 url로 리다이렉트 한다.

Django 공식 문서 Using the Django authentication system을 보면서 login_required 데코레이터는 현재 url을 next라는 변수에 저장해놓고, LoginView는 next 변수에 값이 저장되어 있는지에 따라 로그인 성공 후 리다이렉트 url이 달라진다는 점까지 이해했다. (next 값이 없다면 settings.py의 변수 LOGIN_REDIRECT_URL에 할당된 url로 이동한다.)

login_required() does the following:

  • If the user isn’t logged in, redirect to settings.LOGIN_URL, passing the current absolute path in the query string. Example: /accounts/login/?next=/polls/3/.
  • If the user is logged in, execute the view normally. The view code is free to assume the user is logged in.

By default, the path that the user should be redirected to upon successful authentication is stored in a query string parameter called "next". If you would prefer to use a different name for this parameter, login_required() takes an optional redirect_field_name parameter:

Here’s what LoginView does:

  • If called via GET, it displays a login form that POSTs to the same URL. More on this in a bit.
  • If called via POST with user submitted credentials, it tries to log the user in. If login is successful, the view redirects to the URL specified in next. If next isn’t provided, it redirects to settings.LOGIN_REDIRECT_URL (which defaults to /accounts/profile/). If login isn’t successful, it redisplays the login form.

하지만 궁극적으로 현재 url을 next라는 인자에 넘기는 과정은 무엇인가라는 질문을 늘 품고 있었는데, 이번에 현재 페이지와 정렬기준을 어떻게 다음 url의 뷰에 전달할까 고민하며 해결하게 되었다. 그 과정에서 읽어본 Django 소스코드를 소개한다.

login_required 데코레이터 소스코드

[django 설치 경로\django\contrib\auth\decorators.py] (django github)

(... 생략 ...)

def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
    """
    Decorator for views that checks that the user passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the user object and returns True if the user passes.
    """

    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            # 사용자 인증
            if test_func(request.user):
                return view_func(request, *args, **kwargs)
            path = request.build_absolute_uri()
            resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
            # If the login url is the same scheme and net location then just
            # use the path as the "next" url.
            login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
            current_scheme, current_netloc = urlparse(path)[:2]
            if ((not login_scheme or login_scheme == current_scheme) and
                    (not login_netloc or login_netloc == current_netloc)):
                path = request.get_full_path()
            # 로그인 화면으로 리다이렉트
            from django.contrib.auth.views import redirect_to_login
            return redirect_to_login(
                path, resolved_login_url, redirect_field_name)
        return _wrapped_view
    return decorator


def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = user_passes_test(
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

login_required 데코레이터가 두 함수에 걸쳐 정의되어 있다.

데코레이터 설명

데코레이터에 관한 자세한 설명은 다른 게시물을 참고

파이썬 데코레이터는 원래 외부함수 안에 내부함수(클로저)를 정의하고 외부함수에서는 내부함수 객체를 반환하는 형태로 정의한다. 이때 외부함수를 데코레이터라고 부른다. user_passes_test는 내부에 데코레이터를 정의한다. 즉, 세 겹으로 함수가 정의되어 있다. login_required는 user_passes_test를 호출하고 반환된 (두 겹) 함수를 최종 반환하므로, 데코레이터의 형태에 부합한다.

login_required 데코레이터의 역할

view_func는 사용자가 꾸며준(데코레이터를 달아준) view 함수이다. test_func는 사용자 인증 함수이다. 만약 현재 사용자가 인증된 상태(로그인 O)라면, 내가 원래 의도했던 view를 호출한다. 반면 현재 사용자가 인증되지 않았다면(로그인 X), 로그인 화면으로 리다이렉트 한다. redirect_to_login라는 함수가 아마 로그인 화면으로의 리다이렉트를 반환할 것이다.

next는 어디에

Django 공식문서(https://docs.djangoproject.com/en/4.0/topics/auth/default/)를 참고하면, redirect_field_name이 로그인 후 리다이렉트하는 url을 저장하는 변수의 이름이다. 즉, redirect_field_name의 디폴트 값이 next이다. redirect_field_name를 이름으로 가진 변수가 할당되는 코드를 찾았으나 없다. 다만, redirect_field_name를 redirect_to_login라는 함수로 전달하여 호출한다. 그 함수의 정의를 보자.

redirect_to_login 함수 소스코드

[django 설치 경로\django\contrib\auth\views.py] (django github)

from urllib.parse import urlparse, urlunparse
(... 생략 ...)

def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
    """
    Redirect the user to the login page, passing the given 'next' page.
    """
    resolved_url = resolve_url(login_url or settings.LOGIN_URL)

    login_url_parts = list(urlparse(resolved_url))
    if redirect_field_name:
        querystring = QueryDict(login_url_parts[4], mutable=True)
        querystring[redirect_field_name] = next
        login_url_parts[4] = querystring.urlencode(safe='/')

    return HttpResponseRedirect(urlunparse(login_url_parts))

next는 redirect_field_name라는 이름의 변수에 전달할 값이다. 위에서 이 함수를 호출할 때 next에는 request.get_full_path()를 전달했다. request.get_full_path()는 view_func가 호출되는 url로, 로그인 상태였다면 연결되었어야 하는 링크이다.

resolved_url은 로그인 화면의 url이다. 전달받지 않았다면 setting 파일을 참고한다. (login_required 정의에서는 로그인 url을 전달한다.)

if문에서는 redirect_field_name가 존재한다면 로그인 url을 일부 수정한다. redirect_field_name의 값으로 이전에 next를 디폴트로 전달한다고 하였다. urlparse에 관한 Python 공식문서를 참조하여 urlparse가 반환하는 네임드튜플의 각 원소에 대한 인덱스 번호를 확인 가능하다. 4는 query string을 의미한다. QueryDict는 url query string를 딕셔너리 형태로 저장하는 자료형이다. 딕셔너리가 수정 가능하도록 mutable=True를 전달했다. query string에 "(이름이 redirect_field_name인 변수)=로그인 후 이동할 url"을 추가한다. 참고로 딕셔너리 원소에 추가하는 방식으로 값을 할당하므로, next라는 변수 이름을 redirect_field_name를 통해 전달한 이유도 알 수 있다.

urlunparse는 리스트 형태의 url 구성요소를 문자열 형식으로 변환한다.

결론

다음 작동에 대한 django 소스코드의 구현을 살펴보았다.

  • login_required가 의도한 view를 대체하여 로그인 화면으로 리다이렉트 하는 방법
  • 로그인 url에 "next=로그인 후 이동 링크"라는 query string을 첨가하는 방법
profile
파이썬 백엔드 개발자

0개의 댓글