DRF(Django REST framework)는 어떻게 인증을 구현했는가?
2024년6월4일
웹에는 다양한 인증방식이 존재한다.
그중에서 토큰을 통한 인증방식을 간단하게 아래 4단계로 분리해봤다.
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테스트를 진행해보면 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메시지를 전송해야한다.
여기서 클라이언트가 먼저 알아야할건 인증과 관련된 필드가 무엇인가 이다.
인증과 관련된 필드는 DRF공식문서에서 알 수 있듯이 Authorization이라는 키로 'Token {Token Value}'값을 HTTP Request헤더에 추가하여 전송하면 된다.
추가로 마지막 부분을 보면 성공적으로 인증될시
request.user
는 Django의 User객체가 되고 request.auth
는 전달한 Token객체가 된다.실제로 테스트를 진행해보면
헤더에 Authorization키와 값으로 'Token {Token Value}'를 추가하여 Request를 보냈더니 Token을 통해 판별한 유저로(
request.user
) 유저의 정보를 직렬화해 전달했기 때문에 응답으로 유저의 정보가 출력된걸 확인할 수 있다.UserSerializer(request.user)
--> Response(Serializer.data, ...)
어떻게 가능한걸까??
이러한 비밀을 파악하기 위해서는 View 동작에 대한 이해가 조금 필요하다.
DRF View 인증로직 파악하기
DRF공식문서에서 API Guide Views페이지를 보면 Dispatch methods와 관련된 글이 있다.
간단하게 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메서드에서 아래 코드만 알면된다.
상단에 있는 코드는 메서드명에서 유추할 수 있듯이 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메서드 부분을 주석처리하고 다시 요청을 보내보면
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)
사실 위 부분은 앞에서도 말했듯이 인증과 관련된 코드가 등장하는 부분은 아니지만 따로 설명한 이유는 필자가 초반에 DRF 공식문서 API Guides View페이지 Dispatch Method와 관련된 글을 첨부하면서
해당 문서에 "get, post, put, patch, delete 메서드는 dispatch메서드에 의해 직접 호출된다"라고 소개한 부분이 있는데
이 부분을 실제로 보여주기 위함이였다.
이제 진짜 인증과 관련된 동작들을 알아볼 차례다.
DRF Authentication 과정
Request객체 생성하기
위 코드는 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_CLASSES로 SessionAuthentication과 BasicAuthentication을 사용한다.
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를 지워도
[TokenAuthentication]이 기본값으로 설정되기 때문에 잘 동작하는걸 확인할 수 있다.
어떻게 저런 경로형태의 문자열값이 잘 동작하는지 의심할 수 있지만 위 형태는 Django의 설정값을 설정하는 주된 형태로 위와 같은 동작은 drf의 api_settings와 django의 import_string, 내장 모듈 importlib의 import_module에 대해 알아야 이해할 수 있다.
위와 관련된 내용은 이번 포스트의 주제와는 조금 벗어나기 때문에 일단은 기본값을 설정해야할때 위와같은 형태로 프로젝트 settings.py에 추가해야한다 라고만 알고 있으면 된다.
다시 initialize_request메서드로 돌아가자
이제는 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를 인증 클래스로 사용했다.
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을 반환하게 된다.
반대인 상황(타입만 전달)에서는 처음 if문은 통과하겠지만 그 다음
if문인 len(auth) == 1
이 참이되어 에러가 발생한다.이렇게 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과정을 정리해보았다.
아직 잊지 말아야할 부분은 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
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
속성에 접근하여 최초로 인증로직이 진행되는 과정인지 확인해보면perform_authentication을 수행하기 전은 'AnonymousUser'(
request._request.user
)가 출력되지만 수행된 후에는 인증된 유저 'antoliny0919'(request._request.user
)가 출력되는걸 확인할 수 있다.이렇게 지금까지 DRF는 어떻게 인증을 구현했는가? 에 대해서 알아봤다.
지금까지의 내용을 잘 따라왔다면 구조를 통한 이해를 바탕으로 필자가 진행한 Token인증이 아닌 다른 인증방식을 사용하거나 커스텀 인증방식을 구현해야 할지라도 어떻게 코드를 작성해야할지 감이 잡힐거라고 생각한다.