User Authentication in Django(전반부)

Overview

Django authentication 시스템은 authentication(인증)과 authorization(허가) 두 가지를 모두 처리한다. Authentication(인증)은 유저가 누구인지를 검증하는 절차이며, Authorization(허가)은 인증된 유저가 무엇을 할 수 있는지 허가하는 작업이다. Django의 authentication 시스템은 아래와 같이 구성된다.

  • Users
  • Permissions: Binary flag로 이루어져 있으며, 유저에게 어떤 작업을 허가할지 결정할 수 있음
  • Groups: 한 명 이상의 유저를 구분하고 권한을 부여할 수 있는 방법
  • 암호 해싱 시스템(원하는대로 설정 가능)
  • 유저 인증/권한 처리를 할 수 있는 Form들
  • 제한된 내용에 대해 로그인한 유저 혹은 그렇지 않은 유저들에 따라 처리할 수 있는 View처리 도구들
  • 조립 가능한 백엔드 시스템

Django는 일반적인 인증시스템을 목표로 설계되었으며, 웹의 인증시스템에서 찾아볼 수 있는 다양한 기능들은 third-party에서 제공하는 것들을 사용해야한다(Django 1.8기준). 예를 들면 다음과 같은 기능들이 있다.

  • 유저가 입력한 암호의 복잡도 체크
  • 로그인 시도 횟수 제한
  • Third-party를 통한 로그인(e.g.: Oauth)

User 객체

유저 생성하기

어드민을 통해서 유저를 생성할 수도 있지만, create_user() 헬퍼 함수를 통해서 생성할 수도 있다.

>>> from django.contrib.auth.models import User 
>>> user = User.objects.create_user('rabbit', 'carrot@farm.com', 'whitefur')

# 유저가 생성된 후에 속성을 변경하거나 지정할 수도 있다.
>>> user.last_name = 'little'
>>> user.save()

비밀번호 변경

Django는 해시된 암호만을 저장한다. 따라서 유저객체의 비밀번호 속성을 직접 수정하면 안된다. 유저를 생성할 때 헬퍼 함수를 이용해야하는 이유이다. 유저의 비밀번호를 변경하는데는 두 가지 방법이 있다.

  • command line으로 비밀번호를 변경하는 방법: manage.py changepassword [USER_NAME]
  • 프로그램에서 처리하는 방법: set_password() 함수를 사용.

    >>> from django.contrib.auth.models import User
    >>> user = User.objects.get(username='rabbit')
    >>> user.set_password('[NEW_PASSWORD]')
    >>> user.save()
    

만약 SessionAuthenticationMiddleware가 활성화되어 있다면, 유저의 비밀번호를 변경함과 동시에 모든 세션에서 로그아웃처리가 된다.

Permissions(권한) and Authorization(허가)

Django에서 권한을 부여하는 방법은 어드민의 유저 혹은 그룹페이지를 사용하는 것이다. 기본적으로 특정 타입의 Object에 대해 ‘Add’, ‘Update’, ‘Delete’ 권한을 부여할 수 있는데, Django의 ModelAdmin 클래스에서 제공되는 has_add_permission(), has_change_permission(), has_delete_permission()과 같은 함수들을 사용하면 개별 인스턴스에 대해 상황에 따른 권한 제어도 가능하다. 또한 User 객체는 groupsuser_permissions라는 두 개의 many-to-many 필드를 가지고 있으며, 설정된 권한은 이 곳에 저장된다.

기본 권한

INSTALLED_APPS 설정에 django.contrib.auth가 들어가 있다면, INSTALLED_APPS에 지정된 앱 내부의 각 모델마다 add, change, delete 세 개의 기본 권한이 생성된다. 이 권한들은 manage.py migrate 명령어가 실행될 때 자동으로 생성된다.

Groups

Django의 그룹 기능은 흔히 생각하는대로, 특정 그룹에 속한 유저들에게 특정 권한들을 부여하는데 사용되며, 유저들은 여러개의 그룹에 속할 수 있다.

코드로 권한 만들기

권한은 모델의 Meta 클래스에 선언될 수 있으나, 직접 생성할 수도 있다. 예를 들어 can_invest라는 권한을 만들어서 Loan이라는 모델에서 사용하기 위해서는 다음과 같이 처리하면 된다.

  from deal.models import Loan
  from django.contrib.auth.models import Group, Permission
  from django.contrib.contenttypes.models import ContentType

  content_type = ContentType.objects.get_for_model(Loan)
  permission = Permission.objects.create(codename='can_invest',
                                         name='Can Invest Loan',
                                         content_type=content_type)

권한은 User 모델의 user_permissions 속성이나 Group 모델의 permissions 속성을 통해 할당할 수 있다.

권한 캐싱

ModelBackend은 최초 권한검사를 위해 User 객체 정보를 가져올 때 권한 정보를 함께 가져오는데, 이를 캐싱하게 된다. 일반적인 request-response 처리에서는 문제가 없으나, 권한 변경을 즉시 검사해야하는 경우에는 정보를 다시 불러오는 처리를 해야한다.

  from django.contrib.auth.models import Permission, User
  from django.shortcuts import get_object_or_404
  
  def user_gains_perms(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    # 어떤 권한 검사가 이루어졌든, 모든 권한은 캐싱된다.
    user.has_perm('books.change_bar')
    permission = Permission.objects.get(codename='change_bar')
    user.user_permissions.add(permission)
    # 한 번 권한 검사가 이루어졌다면, 캐싱된 결과를 사용한다.
    user.has_perm('books.change_bar')  # False
    # 따라서 새로운 User 인스턴스를 요청해야 하며,
    user = get_object_or_404(User, pk=user_id)
    # 새로운 인스턴스를 통해 권한 검사를 하면, DB에서 권한 정보를 다시 얻어온다.
    user.has_perm('books.change_bar')  # True
    # ...

Web request에서의 Authentication

Django는 authentication 시스템을 request 객체에서 적용하기 위해 세션과 미들웨어를 사용한다. 이로 인해 모든 request에는 현재 접속한 유저를 보여주는 user 속성이 할당된다. user는 만약 로그인을 하지 않은 유저라면 AnonymousUser 인스턴스이며, 로그인을 했다면 User 인스턴스이다. 이는 is_authenticated() 함수로 검사가 가능하다.

  if request.user.is_authenticated():
    # 인증된 유저 처리
  else:
    # 익명의 유저 처리

로그인 처리

로그인 처리를 하기 위해서는 login() 함수를 사용해야 한다. 이 함수는 HttpRequest 객체와 User 객체를 인자로 받는다. login() 함수는 Django의 세션 프레임워크를 이용해 유저의 ID를 세션에 저장한다.

  from django.contrib.auth import authenticate, login

  def my_view(request):
    username = request.POST['username']
    password = request.POST['password']
    user = authenticate(username=username, password=password)
    if user is not None:
      if user.is_active:
        login(request, user)
        # 성공 화면으로 이동시킨다.
      else:
        # 활성화되지 않은 계정을 위한 화면으로 이동시킨다.
    else:
      #  로그인 실패 화면으로 이동시킨다.

직접 로그인 처리를 할 때는 반드시 authenticate() 함수를 loginc 함수보다 먼저 호출해야 한다. authenticate() 함수는 User 객체에 인증에 성공했음을 나타내는 속성을 설정하며, 이 정보는 로그인 프로세스에서 사용되기 때문이다. 데이터베이스에서 직접 가져온 유저 객체로 로그인을 처리하면 에러가 발생한다.

로그아웃 처리

로그아웃 처리를 위해서는 logout() 함수를 사용한다. 이 함수는 HttpRequest 객체를 인자로 받는다.

  from django.contrib.auth import logout

  def logout_view(request):
    logout(request)
    # 로그아웃 성공 페이지로 이동시킨다.

logout() 함수는 유저가 로그인되지 않았더라도 에러를 발생시키지 않는다. 이 함수가 호출되면, 다른 유저가 이전에 로그인했던 유저의 정보를 사용할 가능성을 방지하기 위해 세션에 있는 모든 정보를 삭제한다.

로그인된 유저의 접근을 제어하기

직접 제어

request.user.is_authenticated()를 사용해서 로그인 페이지로 이동시키거나,

  from django.shortcuts import redirect
  def my_view(request):
    if not request.user.is_authenticated():
      return redirect('/login/?next={}'.format(request.path))
    # ...

에러 페이지를 보여주는 방법이 있다.

  from django.shortcuts import render

  def my_view(request):
    if not request.user.is_authenticated():
      return render(request, 'deal/login_error.html')
    # ...

login_required 데코레이터 사용

  from django.contrib.auth.decorators import login_required

  @login_required
  def my_view(request):
    ...

login_required()는 다음과 같은 작업을 처리한다.

  • 유저가 로그인되지 않았다면 설정된 LOGIN_URL로 이동시키며 현재 주소를 쿼리 파라미터로 넘겨준다. 예를 들면, /accounts/login/?next=/deals/1/ 과 같다.
  • 만약 로그인된 유저라면, view 함수를 실행한다.

만약 로그인 처리 이후 이동할 주소를 처리할 쿼리 파라미터의 이름을 다르게 지정하고 싶다면, redirect_field_name을 사용한다. 또한 login_url을 사용하여 로그인 페이지 주소를 지정할 수도 있다.

  from django.contrib.auth.decorators import login_required

  @login_required(login_url='/accounts/login/', 
                  redirect_field_name='my_redirect_field')
  def my_view(request):
    ...

login_required 데코레이터는 User 객체의 is_active 플래그를 검사하지 않는다.

로그인된 유저의 특수 권한 검사하기

유저가 특수한 권한을 갖고 있는지 검사하기 위해서는 별도의 코드가 필요하다. 예를 들어 이메일이 특정 도메인으로 구성되었는지 확인하기 위해서는 다음과 같은 처리가 필요하다.

  def my_view(request):
    if not request.user.email.endswith('@peoplefund.co.kr'):
      return HttpResponse("이 채권에 투자할 수 없습니다.")
    # ...

이런 경우, user_passes_test() 데코레이터를 이용해서 재사용가능하게 만들 수 있다.

  from django.contrib.auth.decorators import user_passes_test
  
  def email_check(user):
    return user.email.endswith('@peoplefund.co.kr')
  
  @user_passes_test(email_check)
  def my_view(request):
    ...

user_passes_test() 데코레이터는 User 객체를 인자로 받고 bool을 반환하는 함수를 인자로 받는다. 또한 User가 익명유저인지를 자동으로 체크하지는 않으며, 다음과 같은 선택가능한 인자를 받을 수 있다.

  • login_url: 유저가 검사에 통과하지 못한 경우 이동할 페이지이며, 별도로 인자를 설정하지 않으면 LOGIN_URL로 이동시킨다.
  • redirect_field_name: login_required() 데코레이터와 동일하게 동작한다.

permission_required 데코레이터

Django에서는 유저가 권한을 갖고 있는지 체크하기 위한 permission_required() 데코레이터를 제공한다.

  from django.contrib.auth.decorators import permission_required
  
  @permission_required('deals.can_invest')
  def my_view(request):
    ...

has_perm() 함수와 동일하게, . 과 같이 지정하면 된다. `permission_required()` 함수 또한 `login_url` 를 인자로 받을 수 있으며, `raise_exception`이 주어지면, `PermissionDenied` 에러가 발생하며 로그인 페이지로 이동하는 대신 403에러 페이지를 보여주게 된다.

비밀번호 변경 시 세션 무효화

AUTH_USER_MODEL에 설정된 모델이 AbstractBaseUser를 상속했거나 get_session_auth_hash() 함수를 구현했다면, 인증된 세션은 이 함수가 반환한 해쉬를 포함한다. AbstractBaseUser의 경우, 비밀번호 필드의 HMAC(Hash Message Authentication Code)이다.

만약 SessionAuthenticationMiddleware가 활성화되어 있다면, request에 포함된 해쉬가 서버에서 처리한 것과 동일한지 검증한다. 이는 유저가 비밀번호를 변경했을 때, 모든 세션에서 로그아웃처리가 가능하게 만들어준다.

django.contrib.auth.views.password_change()django.contrib.auth 어드민의 user_change_password 뷰는 비밀번호를 변경해도 세션에서 로그아웃 처리가 되지 않도록 만들어져 있다. 만약 비밀번호 변경 처리를 하는 뷰를 직접 만든다면, django.contrib.auth.decorators.update_session_auth_hash(request, user) 함수를 활용하면 된다.

  from django.contrib.auth import update_session_auth_hash
  
  def password_change(request):
    if request.method == 'POST':
      form = PasswordChangeForm(user=request.user, data=request.POST)
      if form.is_valid():
        form.save()
        update_session_auth_hash(request, form.user)
    else: 
      ...

get_session_auth_hash() 함수는 SECRET_KEY 설정을 사용하므로, 새로운 SECRET_KEY를 사용한다면 모든 세션이 무효화된다.

Authentication Views

Django에서는 인증과 관련된 뷰들을 기본적으로 제공한다. 기본제공 인증 뷰들을 사용하려면 url 패턴에 다음 설정을 추가한다.

  urlpatterns = [url('^', include('django.contrib.auth.urls'))]

Django에서 제공되는 인증 관련 뷰들

다음과 같은 뷰들이 제공되니 필요한 경우 찾아보자.

  • login
  • logout
  • logout_then_login
  • password_change
  • password_change_done
  • password_reset
  • password_reset_done
  • password_reset_confirm
  • password_reset_complete

redirect_to_login 헬퍼 함수

redirect_to_login 함수는 직접 접근제한을 구현하는 경우 사용하기 편리한 헬퍼 함수이다. 이 함수는 유저를 로그인페이지로 이동시켰다가 로그인에 성공하면 다시 돌아갈 주소를 지정할 수 있다.

Django에서 제공되는 인증 관련 폼들

다음과 같은 폼들이 제공되니 필요한 경우 찾아보자.

  • AdminPasswordChangeForm
  • AuthenticationForm
  • PasswordChangeForm
  • PasswordResetForm
  • SetPasswordForm
  • UserChangeForm
  • UserCreationForm

템플릿에서의 인증 데이터

Users

Django의 Template에서 사용되는 RequestContext에는 항상 {{ user }} 변수에 User 혹은 AnonymousUser가 할당된다. 만약 RequestContext를 사용하지 않는 템플릿이라면 user 변수는 할당되지 않는다.

Permissions

로그인된 유저라면 템플릿에서 {{ perms }} 변수가 설정된다. 이 변수는 django.contrib.auth.context_processors.PermWrapper의 인스턴스이다. {{ perms }} 내부의 변수들은 True 혹은 False를 반환하며 User.has_module_perms를 나타내고, 특정 앱의 사용권한이 있는지를 나타낸다. 예를 들어 deals라는 앱이라고 가정하면, {{ perms.deals }} 라고 사용할 수 있다. 또한 앱의 하부에 있는 권한은 User.has_perm을 대신하는 {{ perms.deals.can_invest }} 변수를 사용할 수 있다. 권한을 템플릿에서 사용하려면 다음과 같이 활용할 수 있다.

  {% if perms.deals %}
    <p>거래 페이지에서 다음과 같은 권한을 갖고 있습니다.</p>
    {% if perms.deals.can_invest %}
      <p>투자가 가능합니다.</p>
    {% endif %}
    {% if perms.deals.can_use_point %}
      <p>포인트를 사용할 수 있습니다.</p>
    {% endif %}
  {% else %}
    <p>거래 페이지를 사용할 수 있는 권한이 없습니다.</p>
  {% endif %}