DRF(Django REST framework)는 어떻게 인증을 구현했는가?

antoliny0919
202464
웹에는 다양한 인증방식이 존재한다.
그중에서 토큰을 통한 인증방식을 간단하게 아래 4단계로 분리해봤다.
no-img
1. 클라이언트가 아이디와 비밀번호를 HTTP Request Message Body에 담아 로그인을 요청
2. 서버에서 아이디와 비밀번호를 통해 회원가입한 유저라면 토큰을 Response
3. 인증이 필요한 로직에 Resource를 요청하기 위해 클라이언트가 HTTP Request + 헤더에 인증과 관련된 필드에 Token을 추가해서 전달
4. 서버에서 Request메시지의 헤더부분에서 인증과 관련된 헤더필드값(토큰)을 통해 유저를 파악후 Response 전달
 
놀랍게도 위와같은 과정은 DRF에서 굉장히 간단하게 구현할 수 있다.
일단 DRF와 같이 서버에서 가장 먼저 구현해야하는 부분은 2단계 토큰을 발급하는 단계이다.
 
토큰 발급하고 인증 구현
 

토큰 발급

 
DRF는 토큰을 발급하는 로직을 따로 제공한다.
python
COPY

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework.authtoken',
    ...
]

------------------------------------------------------
# urls.py

from django.urls import path
from rest_framework.authtoken import views


urlpatterns = [
  path('token/', views.ObtainAuthToken.as_view(), name='token_obtain'),
]
먼저 settings.py에 'rest_framework.authtoken'을 추가하고 authtoken에는 Token모델이 따로 존재하기 때문에 migrate를 진행해야한다.
sh
COPY

$ python3 manage.py makemigrations
$ python3 manage.py migrate
그다음 urlpatterns에 본인이 원하는 로그인과 관련된(토큰 발급)url경로와 해당 경로에 접근했을시 동작할 로직(View)으로 DRF에서 제공하는 ObtainAuthToken클래스를 사용하면된다.
로그인 API 경로
필자는 로그인 API 경로로 'api/token/'을 사용했고 앞으로도 계속 해당경로를 사용한다.
놀랍게도 토큰 발급을 위한 코드는 이게 끝이다.
no-img
유저를 생성하여 API테스트를 진행해보면 Response로 token을 성공적으로 응답받은걸 확인할 수 있다.
 

인증 구현

 
클라이언트가 로그인에 성공해 token을 발급받았다.
그리고 클라이언트는 본인의 데이터를 얻고자 한다.
python
COPY

# views.py
from rest_framework import views, status
from rest_framework.response import Response
from rest_framework.authentication import TokenAuthentication
from .serializers import UserSerializer

# Create your views here.

class UserView(views.APIView):
  
  authentication_classes = [TokenAuthentication]
  
  def get(self, request):
    
    serializer = UserSerializer(request.user)
    return Response(serializer.data, status=status.HTTP_200_OK)

-----------------------------------------------------------------------
# serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()

class UserSerializer(serializers.ModelSerializer):
  
  class Meta:
    model = User
    fields = ('id', 'username', 'email')
필자가 만든 UserView클래스는 굉장히 간단하다.
authentication_classes로 TokenAuthentication을 설정함으로써 해당 View는 인증이 필요한 로직을 의미하며 클라이언트는 본인을 인증하기 위해 Header에 인증과 관련된 필드값으로 토큰을 추가하여 HTTP Request메시지를 전송해야한다.
 
여기서 클라이언트가 먼저 알아야할건 인증과 관련된 필드가 무엇인가 이다.
no-img
인증과 관련된 필드는 DRF공식문서에서 알 수 있듯이 Authorization이라는 키로 'Token {Token Value}'값을 HTTP Request헤더에 추가하여 전송하면 된다.
추가로 마지막 부분을 보면 성공적으로 인증될시 request.user는 Django의 User객체가 되고 request.auth는 전달한 Token객체가 된다.
 
실제로 테스트를 진행해보면
no-img
헤더에 Authorization키와 값으로 'Token {Token Value}'를 추가하여 Request를 보냈더니 Token을 통해 판별한 유저로(request.user) 유저의 정보를 직렬화해 전달했기 때문에 응답으로 유저의 정보가 출력된걸 확인할 수 있다.
UserSerializer(request.user) --> Response(Serializer.data, ...)
 

어떻게 가능한걸까??

 
이러한 비밀을 파악하기 위해서는 View 동작에 대한 이해가 조금 필요하다.
 
DRF View 인증로직 파악하기
 
 DRF공식문서에서 API Guide Views페이지를 보면 Dispatch methods와 관련된 글이 있다.
no-img
간단하게 get, post, put, patch, delete 메서드는 dispatch메서드에 의해 직접 호출된다는 의미이다.
python
COPY

# urls.py
from django.urls import path
from .views import UserView

urlpatterns = [
  path('user/', UserView.as_view(), name="test-view")
]

---------------------------------------------------------------
# views.py

class UserView(views.APIView):
  
  authentication_classes = [TokenAuthentication]
  
  def get(self, request):
    
    serializer = UserSerializer(request.user)
    return Response(serializer.data, status=status.HTTP_200_OK)
만약 클라이언트가 'api/user/' 경로와 함께 HTTP GET Method로 요청을 보내면 UserView의 get메서드가 호출될 것이고 get 메서드가 호출되기 전에 dispatch라는 메서드가 먼저 호출될 것이다.
 

Dispatch

 
그렇다면 dispatch메서드를 확인하기 위해 상속받은 APIView의 dispatch메서드를 확인해보자
python
COPY

    # Note: Views are made CSRF exempt from within `as_view` as to prevent
    # accidental removal of this exemption in cases where `dispatch` needs to
    # be overridden.
    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
만약 처음 dispatch메서드와 마주한다면 조금 복잡해보일 수 있으나 생각보다 굉장히 간단한 형태이다.
일단 이번 포스트는 인증과 관련된 부분만 설명할 것이기 때문에 dispatch메서드의 모든 코드를 이해할 필요는 없다.
우리가 Authentication이 어떻게 동작하는지 알기위해서는 dispatch메서드에서 아래 코드만 알면된다.
no-img
상단에 있는 코드는 메서드명에서 유추할 수 있듯이 Request객체를 생성하는 동작이고 하단에 있는 코드블럭은 사실 이 부분은 인증과는 딱히 관련은 없지만 이해를 돕기위해 추가한 코드로 우리가 View에 작성한 메서드('get')가 실질적으로 호출되는 부분이다.
 

View Method 호출

 
위 예제처럼 'api/user/'경로로 HTTP Request GET을 보냈다고 가정해보자
그리고 먼저 하단에 있는 코드부터 봐보면 먼저 조건검사가 진행되는데 request.method에는 우리가 요청한 HTTP Request의 메서드가 담겨있다. 즉 문자열 'GET'을 값으로 가질테고 lower메서드를 호출하여 소문자로 변환한다.
 
그 다음으로 변환된 'get'이 self.http_method_names에 존재해야하는데 http_method_names속성은 APIView의 부모클래스인 View클래스의 클래스 변수로 존재한다.
python
COPY

class View:
    ...
    http_method_names = [
        "get",
        "post",
        "put",
        "patch",
        "delete",
        "head",
        "options",
        "trace",
    ]
    ...
변수명 그대로 HTTP Method들을 배열에 가지고 있는 형태이고 Connect을 제외한 모든 HTTP Method들을 가지고 있다.
조건문 if request.method.lower() in self.http_method_names는 클라이언트가 보낸 HTTP Request가 Django View에서 허용하는 http_method로 요청했는지 확인하는 과정이다.
우린 GET으로 요청했기 때문에 조건문을 통과하게 되고 아래 코드를 실행한다.
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
getattr함수는 첫번째 인자로부터 두 번째 인자값의 속성을 가져오는 함수이다. 그리고 세 번째 인자는 없을때를 대비하는 default값이라고 보면 된다.
python
COPY

class UserView(views.APIView):
  
  authentication_classes = [TokenAuthentication]
  
  def get(self, request):
    
    serializer = UserSerializer(request.user)
    return Response(serializer.data, status=status.HTTP_200_OK)
우리는 get메서드를 작성했기 때문에 self(UserView클래스 객체)에 get이라는 속성이 존재하고 getattr함수를 통해 get 메서드 객체를 가져와 handler변수에 할당하게 된다.
 
만약 우리가 get메서드를 구현하지도 않고 GET요청을 보냈다면 위 과정에서 handler는 self.http_method_not_allowed가 할당되게 되며 결국 해당 함수를 호출하게 되어 에러가 발생할 것이다.
python
COPY

def http_method_not_allowed(self, request, *args, **kwargs):
    """
    If `request.method` does not correspond to a handler method,
    determine what kind of exception to raise.
    """
    raise exceptions.MethodNotAllowed(request.method)
실제로 get메서드 부분을 주석처리하고 다시 요청을 보내보면
no-img
no-img
Method Not Allowed라는 에러를 확인할 수 있고 응답메시지도 아래 MethodNotAllowed클래스에서 생성된 메시지라는걸 알 수 있다.
python
COPY

class MethodNotAllowed(APIException):
    status_code = status.HTTP_405_METHOD_NOT_ALLOWED
    default_detail = _('Method "{method}" not allowed.')
    default_code = 'method_not_allowed'

    def __init__(self, method, detail=None, code=None):
        if detail is None:
            detail = force_str(self.default_detail).format(method=method)
        super().__init__(detail, code)
하지만 우린 정상적으로 get메서드를 구현했기 때문에 구현한 get메서드 객체가 위에서도 설명했듯이 handler변수에 할당되고 마지막으로 handler함수를 호출하게 되어 결국 우리가 작성한 메서드들이 이 과정에서 호출된다.
response = handler(request, *args, **kwargs)
no-img
사실 위 부분은 앞에서도 말했듯이 인증과 관련된 코드가 등장하는 부분은 아니지만 따로 설명한 이유는 필자가 초반에 DRF 공식문서 API Guides View페이지 Dispatch Method와 관련된 글을 첨부하면서
해당 문서에 "get, post, put, patch, delete 메서드는 dispatch메서드에 의해 직접 호출된다"라고 소개한 부분이 있는데
이 부분을 실제로 보여주기 위함이였다.
 
이제 진짜 인증과 관련된 동작들을 알아볼 차례다.
 
DRF Authentication 과정
 

Request객체 생성하기

 
no-img
위 코드는 dispatch메서드에 있는 코드로 아까봤던 동작 이전에 등장하는 코드다.
즉 우리가 작성한 get, post, put 메서드를 호출하기 전에 하는 전처리 동작에 해당하는 코드인데 더 자세히 알기 위해서는 initialize_request메서드는 무슨 동작을 하는지 알아야한다.
python
COPY

def initialize_request(self, request, *args, **kwargs):
    """
    Returns the initial request object.
    """
    parser_context = self.get_parser_context(request)

    return Request(
        request,
        parsers=self.get_parsers(),
        authenticators=self.get_authenticators(),
        negotiator=self.get_content_negotiator(),
        parser_context=parser_context
    )
initialize_request 메서드명만 봐도 무슨 동작을 하는지 유추가 가능하다.
의미 그대로 Request객체를 생성하는 동작인데 Request객체를 생성하는 부분 인자들을 보면 많은 매개변수 중에 authenticators라는 매개변수가 눈에띄고 해당값으로 self.get_authenticators()가 전달되는걸 확인할 수 있다.
python
COPY

    def get_authenticators(self):
        """
        Instantiates and returns the list of authenticators that this view can use.
        """
        return [auth() for auth in self.authentication_classes]
get_authenticators메서드는 사전에 지정한 authentication_classes들을 반복하고 각 클래스들의 객체들을 생성하여 배열형태로 반환한다.
여기서 눈치빠른 독자라면 authentication_classes라는 값이 익숙하게 느껴질 수도 있다.
왜냐하면 필자의 UserView에도 authentication_classes가 정의되어 있기 때문이다.
python
COPY

class UserView(views.APIView):
  
  authentication_classes = [TokenAuthentication]
  
  def get(self, request):
    
    serializer = UserSerializer(request.user)
    return Response(serializer.data, status=status.HTTP_200_OK)
이렇게 authentication_classes는 의미그대로 인증과 관련된 클래스들을 반복할 수 있는 형태로 가져야한다.
위와같이 필자의 UserView클래스의 경우에는 get_authenticators메서드에서 해당 클래스의 객체가 생성되고 해당 값을 가진 배열을 리턴하게 되어 Request클래스를 생성할 때 authenticators매개변수 값으로 전달된다.
참고로 APIView에서 authentication_classes의 값은 api_settings.DEFAULT_AUTHENTICATION_CLASSES로
python
COPY

class APIView(View):
    ...
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    ...
프로젝트 settings.py에 아무런 설정을 하지 않았다면 DRF는 DEFAULT_AUTHENTICATION_CLASSESSessionAuthenticationBasicAuthentication을 사용한다.
python
COPY

DEFAULTS = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication'
    ],
    ...
}
하지만 프로젝트 settings.py에 REST_FRAMEWORK키로 DEFAULT_AUTHENTICATION_CLASSES를 오버라이딩 하면 해당 DEFAULT_AUTHENTICATION_CLASSES의 값이 기본값이 되므로 우리가 기본적으로 적용되어야 하는 authentication이 필요하다 할때는 settings.py에 아래와 같은 형식으로 작성하면 API View의 authentication_classes변수 값으로 api_settings.DEFAULT_AUTHENTICATION_CLASSES가 프로젝트 settings.py에 정의된값을 따르게 된다.
python
COPY

REST_FRAMEWORK = { 
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}
예시로 위와같이 설정하고 UserView의 authentication_classes를 지워도
no-img
[TokenAuthentication]이 기본값으로 설정되기 때문에 잘 동작하는걸 확인할 수 있다.
 
어떻게 저런 경로형태의 문자열값이 잘 동작하는지 의심할 수 있지만 위 형태는 Django의 설정값을 설정하는 주된 형태로 위와 같은 동작은 drf의 api_settings와 django의 import_string, 내장 모듈 importlib의 import_module에 대해 알아야 이해할 수 있다.
위와 관련된 내용은 이번 포스트의 주제와는 조금 벗어나기 때문에 일단은 기본값을 설정해야할때 위와같은 형태로 프로젝트 settings.py에 추가해야한다 라고만 알고 있으면 된다.
다시 initialize_request메서드로 돌아가자
no-img
이제는 authenticators에 무슨값이 전달될지 알고있다.
바로 authentication_classes내에 있는 클래스들의 객체들이 전달되게 되는데 그렇기 때문에 우리의 UserView같은 경우는 TokenAuthentication의 객체가 담긴 배열이 --> [TokenAuthentication()]
Request객체를 생성할때 전달하는 authenticators 매개변수의 값으로 전달된다.
이제 Request코드를 확인해보자.
 

Request의 디스크립터 프로퍼티

 
python
COPY

class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.

    Kwargs:
        - request(HttpRequest). The original request instance.
        - parsers(list/tuple). The parsers to use for parsing the
          request content.
        - authenticators(list/tuple). The authenticators used to try
          authenticating the request's user.
    """

    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
        )
        self._request = request
        self.parsers = parsers or ()
        -----------------------------------------------------------------
        self.authenticators = authenticators or ()
        -----------------------------------------------------------------
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty
        if self.parser_context is None:
            self.parser_context = {}
        self.parser_context['request'] = self
        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET
        force_user = getattr(request, '_force_auth_user', None)
        force_token = getattr(request, '_force_auth_token', None)
        if force_user is not None or force_token is not None:
            forced_auth = ForcedAuthentication(force_user, force_token)
            self.authenticators = (forced_auth,)
Request의 __init__ 매직메서드를 확인해보면 우리가 전달한 authenticators객체를 객체의 속성으로 만드는것 외에는 객체가 생성될때 인증과 관련된 동작은 딱히 없어보인다.
Request객체의 코드를 조금 더 보다보면 user라는 속성과 관련된 코드가 존재하는데 이전에 요청을 보낸 클라이언트가 인증된 유저인지 확인할때 request.user를 사용해 확인했었다.
Request의 user메서드는 아래와 같이 디스크립터로 제공되어 최종적으로 Request클래스 객체의 _user속성을 향하게 된다.
python
COPY

    @property
    def user(self):
        """
        Returns the user associated with the current request, as authenticated
        by the authentication classes provided to the request.
        """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user

    @user.setter
    def user(self, value):
        """
        Sets the user on the current request. This is necessary to maintain
        compatibility with django.contrib.auth where the user property is
        set in the login and logout functions.

        Note that we also set the user on Django's underlying `HttpRequest`
        instance, ensuring that it is available to any middleware in the stack.
        """
        self._user = value
        self._request.user = value
지금쯤 이미 위 코드를 보고 어디서 authentication이 이루어지는지 눈치챈 독자가 있을거라고 생각한다.
_user속성의 getter메서드인 프로퍼티 데코레이터user 메서드를 확인해보면
if not hasattr(self, '_user')로 먼저 객체에 _user속성이 존재하는지 확인한다.
만약 존재하지 않는다면 with wrap_attributeerrors()라는 구문이 실행되는데 위 wrap_attributeerrors함수는 컨텍스트 매니저 함수로 간단하게 동작만 설명하자면 user라는 속성에 접근할때 AttributeError가 발생하면 에러 출력의 형식을 일반적인 AttributeError와는 다르게 출력한다.
아무튼 간단하게 _user라는 속성이 존재하지 않는다면 self._authenticate() --> _authenticate메서드가 호출된다.
 

Authenticate

 
python
COPY

    def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        for authenticator in self.authenticators:
            try:
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                self._not_authenticated()
                raise

            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return

        self._not_authenticated()
_authenticate메서드는 이름 그대로 인증을 진행하는 과정이며 이전에 봤던 authenticators가 쓰이는 부분이다.
이전에 get_authenticators()에서 authentication_classes속성을 통해 인증클래스들의 객체를 생성하여 배열에 담아 반환했다.
self.authenticators의 값은 인증클래스들 객체가 담긴 배열이며 필자가 테스트하는 뷰인 UserView같은 경우의 self.authenticators값은 --> [TokenAuthentication()]라고 볼 수 있다.
 
위와 같이 _authenticate함수는 해당값을 반복하여 각 객체마다 authenticate라는 메서드를 호출한다.
그러므로 UserView는 TokenAuthentication의 authenticate메서드를 호출하게 된다.
이 과정에서 우리가 알 수 있는건 인증을 진행하는 부분은 결국 Request클래스의 _authenticate라는 메서드이며 실질적인 인증에 대한 과정은 우리가 지정한 인증 클래스(authentication_classes의 값)의 authenticate메서드이다.
필자는 TokenAuthenticate를 인증 클래스로 사용했다.
만약 Token 인증 방식이 아니라 Session, JWT Token 등등 필자와 다른 인증 방식을 사용하려 한다고 해도 위와 같이 동일한 과정을 거치며 단지 인증 클래스로 사용한 클래스만 다름으로써 각 인증클래스에 구현된 authenticate메서드가 호출된다.
python
COPY

def get_authorization_header(request):
    """
    Return request's 'Authorization:' header, as a bytestring.

    Hide some test client ickyness where the header can be unicode.
    """
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if isinstance(auth, str):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth
    
class TokenAuthentication(BaseAuthentication):
    """
    Simple token based authentication.

    Clients should authenticate by passing the token key in the "Authorization"
    HTTP header, prepended with the string "Token ".  For example:

        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
    """
    ...
    def authenticate(self, request):
        auth = get_authorization_header(request).split()
        ...
    ...
TokenAuthenticate의 authenticate메서드는 가장 먼저 get_authorization_header함수를 통해 request의 헤더중 'HTTP_AUTHORIZATION'헤더값을 가져온다.
위 헤더값은 익숙하다. 이전에 클라이언트의 입장에서 인증이 필요한 로직에 request를 보낼때 request header에 인증과 관련된 필드를 추가하여 전송한걸 기억하는가??
TokenAuthenticate같은 경우는 인증과 관련된 필드가 'Authorization'이였고 위 코드의 HTTP_AUTHORIZATION이 해당 필드를 의미한다.
 
즉 get_authorization_header함수의 auth변수에는 클라이언트가 헤더로 전달한 Authorization필드값이 담기게 된다.
그 다음으로 if instance(auth, str): auth = auth.encode(HTTP_HEADER_ENCODING)코드는 가져온 Authorization필드값이 str타입이면 str.encode를 호출하여 바이트로 인코딩한뒤 반환한다.
python
COPY

class TokenAuthentication(BaseAuthentication):
    ...
    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != self.keyword.lower().encode():
            return None

        if len(auth) == 1:
            msg = _('Invalid token header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid token header. Token string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            token = auth[1].decode()
        except UnicodeError:
            msg = _('Invalid token header. Token string should not contain invalid characters.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(token)
TokenAuthentication클래스 authenticate함수 auth는 반환된(Authorization필드값 바이트)값을 공백기준으로 분리하는데
이전에 Authorization값으로 'Token {Token Value}'형식으로 클라이언트가 전달했었다.
보통 Authorization의 값으로 '{type} {value}'형식으로 전달하는데 TokenAuthentication같은 경우는 type으로 Token + 공백 + Token Value로 발급받은 토큰값을 전달해줬던 것이다.
 
그렇기 때문에 정상적으로 전달했을시 split()으로 분리하면 값이 두개인 배열이 되고([type, value])해당값에 len을 사용함으로써 클라이언트가 올바른 형식으로 전달했는지 확인한다.
 
자주하는 실수로 타입을 입력하지 않고 토큰값만 전달하는 경우가 많다.
만약 위와 같이 토큰값만 전달했다고 가정하면 split메서드가 동작하여 값이 하나인 배열이 될테고 처음 if문에서 [0]번 인덱스로 해당값이 토큰인지 확인하는데 self.keyword.lower().encode() --> b'token'
우린 타입인 Token을 전달하지 않았기 때문에 토큰값과 비교하게 되어 해당 조건문이 true가 되고 None을 반환하게 된다.
no-img
반대인 상황(타입만 전달)에서는 처음 if문은 통과하겠지만 그 다음 if문인 len(auth) == 1 이 참이되어 에러가 발생한다.
no-img
이렇게 TokenAuthentication클래스의 authenticate는 먼저 인증과 관련된 헤더값을 가져오고 헤더값이 올바른 형식으로 전달되었는지 확인한다음 토큰값으로 유저를 파악하는 단계(return self.authenticate_credentials(token))로 이동한다.
python
COPY

class TokenAuthentication(BaseAuthentication):

    def get_model(self):
        if self.model is not None:
            return self.model
        from rest_framework.authtoken.models import Token
        return Token
        
    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_('Invalid token.'))

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (token.user, token)
authenticate_credentials메서드를 보면 가장 먼저 self.get_model()로 Token클래스를 가져오고 Token클래스의 selelct_related쿼리를 통해 토큰과 OneToOne필드로 연결된 related_object인 user를 파악하게 된다.
python
COPY

class Token(models.Model):
    """
    The default authorization token model.
    """
    key = models.CharField(_("Key"), max_length=40, primary_key=True)
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, related_name='auth_token',
        on_delete=models.CASCADE, verbose_name=_("User")
    )
    created = models.DateTimeField(_("Created"), auto_now_add=True)
    ...
만약 토큰이 유효할시 token.user에 request를 보낸 user객체가 담기게 되고 TokenAuthentication클래스 authenticate_credentials는 튜플형태로 두개의 값을 반환한다. --> (token.user, token)
 
지금까지 TokenAuthenticate과정을 정리해보았다.
no-img
아직 잊지 말아야할 부분은 authenticate를 호출하는 부분은 Request클래스의 user속성의 getter메서드라는 것이다.
왜냐하면 이제 다시 Request클래스의 코드로 돌아가야할 때이기 때문이다.
TokenAuthentication클래스 authenticate_credentials메서드가 성공적으로 반환되면 해당 반환값은 Request클래스 _authenticate메서드 user_auth_tupe변수에 할당된다.
 
그리고 self.user, self.auth = user_auth_tuple로 값이 self.user, self.auth로 할당되는데
즉 User객체는 self.user, Token객체는 self.auth에 할당되고 user속성은 디스크립터가 구현된 속성이기 때문에 self.user에 값이 할당될때 self.user의 setter메서드가 호출된다.
self.auth
self.auth또한 디스크립터가 구현된 속성이지만 동작은 self.user와 동일하기 때문에 user로 설명하겠다.
python
COPY

    @user.setter
    def user(self, value):
        """
        Sets the user on the current request. This is necessary to maintain
        compatibility with django.contrib.auth where the user property is
        set in the login and logout functions.

        Note that we also set the user on Django's underlying `HttpRequest`
        instance, ensuring that it is available to any middleware in the stack.
        """
        self._user = value
        self._request.user = value
user속성의 setter메서드는 굉장히 간단하다.
실질적인 user속성인 _user에 User값을 할당하고 _request.user속성에도 같은 값을 할당한다.
(※ 여기서 _request.user에도 같은 User객체를 할당했다는걸 기억하자.)
python
COPY

   @property
    def user(self):
        """
        Returns the user associated with the current request, as authenticated
        by the authentication classes provided to the request.
        """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user
이제 authenticate과정이 완료되었고 Request클래스의 user속성 getter메서드는 self._user를 반환함으로써 인증에 성공한 유저객체를 반환하게 된다.
python
COPY

class APIView(View):
    def dispatch(self, request, *args, **kwargs):
        ...
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        ...
        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                   self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
이렇게 initialize_request로 생성한 Request객체가 우리가 정의한 View로직이 호출되는 부분인 handler(request, *args, **kwargs)부분에 인자로 전달되는걸 보면 우리가 정의한 View에서는 이미 인증과정이 다 끝난 상태이고 아래와 같이 request._request.user에 접근해도 인증된 유저 객체가 잘 전달되는걸 확인할 수 있다.
python
COPY

class UserView(views.APIView):
  
  authentication_classes = [TokenAuthentication]
  
  def get(self, request):
    
    serializer = UserSerializer(request._request.user)
    return Response(serializer.data, status=status.HTTP_200_OK)
여기서 request.user가 아닌 request._request.user로 접근해도 동작하는 모습을 보여준 이유는 단지 설명을 위해서다.
request.user로 접근하면 Request객체의 user getter메서드가 호출되기때문에 최초 접근에도 인증과정이 동작해서 올바른 객체가 반환된다.
필자가 설명하려는 부분은 우리가 정의한 View는 이미 인증과정이 다 끝난 상태라는것이다.
즉 getter메서드(request.user)를 View내에서 호출하지 않았음에도 request._request.user로 접근했을시 인증된 유저 객체를 반환한다는걸 보여주기 위함이었다.
그렇다면 정의한 View가 호출되기 이전에 request.user에 이미 한 번 접근해서 getter메서드가 호출되어 이미 인증로직이 동작했다는걸 의미하는데 최초로 request.user를 접근하여 인증로직을 호출한 곳은 어디일까??
 

Authentication Trigger

python
COPY

class APIView(View):
    def dispatch(self, request, *args, **kwargs):
        ...
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        ...
        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                   self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
다시 APIView클래스의 dispatch메서드를 보면 request객체를 생성하는 부분 다음으로 self.initial(request, *args, **kwargs) --> initial메서드를 호출하는 부분이 있는데
python
COPY

    def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
        ...

        # Ensure that the incoming request is permitted
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)
initial메서드 코드를 보면 self.perform_authentication(request) --> 메서드명 그대로 인증을 이행하는 메서드를 호출하는걸 알 수 있다.
python
COPY

    def perform_authentication(self, request):
        """
        Perform authentication on the incoming request.

        Note that if you override this and simply 'pass', then authentication
        will instead be performed lazily, the first time either
        `request.user` or `request.auth` is accessed.
        """
        request.user
perform_authentication은 굉장히 간단한 함수다.
지금까지의 과정을 통해 request.user에 한 번이라도 접근하면 getter가 호출되어 인증로직이 동작한다는걸 알 고 있다.
그렇기 때문에 인증을 이행하는 메서드도 request.user로 접근(호출)만 하면 되고 함수명대로 인증을 이행하는 시작점으로 완벽하게 동작하는걸 알 수 있다.
 
이렇게 최초 인증이 수행되는 부분은 perform_authentication메서드라는것도 알았다.
실제로 perform_authentication을 호출하기 전 후로 request._request.user속성에 접근하여 최초로 인증로직이 진행되는 과정인지 확인해보면
no-img
perform_authentication을 수행하기 전은 'AnonymousUser'(request._request.user)가 출력되지만 수행된 후에는 인증된 유저 'antoliny0919'(request._request.user)가 출력되는걸 확인할 수 있다.
 
이렇게 지금까지 DRF는 어떻게 인증을 구현했는가? 에 대해서 알아봤다.
지금까지의 내용을 잘 따라왔다면 구조를 통한 이해를 바탕으로 필자가 진행한 Token인증이 아닌 다른 인증방식을 사용하거나 커스텀 인증방식을 구현해야 할지라도 어떻게 코드를 작성해야할지 감이 잡힐거라고 생각한다.