Authentication
Django Rest Framework 공식문서를 보면 authentication에 대해서 아래와 같이 말하고 있다.
Note: Don't forget that authentication by itself won't allow or disallow an incoming request, it simply identifies the credentials that the request was made with.
즉 authentication은 user가 로그인했는지, 또는 valid user인지를 증명하는 역할을 한다. 어떤 level에 user가 접근할 수 있는지 없는지를 확인하는 건 permission이 한다.
authentication에도 다양한 종류가 있지만 이번에는 Token Authentication에 대해서 공부했다.
Token Authentication
token authentication은 DRF에서 제공하는 authentication 방식이다.
다른 작업을 시작하기 전에 먼저 settings.py를 수정해야 한다.
# settings.py
...
INSTALLED_APPS = [
...
'rest_framework.authtoken'
]
....
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
- INSTALLED_APPS에 새로운 app을 추가하는 이유는, db안에 Token이라고 하는 새로운 테이블을 생성하기 위해서이다.
- 실제로 모델을 수정해 테이블을 만든 것은 아니지만 DRF에서 자체적으로 제공하는 app을 사용해서 테이블을 추가하기 위해서 migrate를 진행해야 한다 (makemigrations는 필요 없다).
- admin page를 이용해 token을 생성할 수 있고, postman을 이용해 결과를 얻기 위해서는 header에 정보를 담는데, token을 담을 때, ‘Token <token>’과 같이 담지 않으면 credential error가 발생한다.
# error example
{
"detail": "Authentication credentials were not provided."
}
token을 발급받는 과정은 크게 로그인 후 발급 또는 회원가입 후 발급의 두 가지 경우로 나눌 수 있다.
1. 로그인 후 발급
먼저 user의 내용을 다를 수 있는 user_app을 생성하고, api 폴더를 만들어 views.py, urls.py, serializers.py를 각각 만든다. 여기서 미리 view, url, serializer에 관련된 파일을 생성하지만, 이 단계에서는 url 외의 파일은 사용하지 않는다.
DRF는 자체적으로 username과 password에 관한 token을 발급해주는 obtain_auth_token이라고 하는 view를 제공한다. 해당 view를 사용하여 urlpatterns로 생성한 login link로 username과 password의 user 정보를 보내면 token을 얻을 수 있다.
# urls.py
from django.urls import path
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
path('login/', obtain_auth_token, name='login'),
]
postman의 form-data를 사용해 username과 password를 보내면 token을 받을 수 있다.
여기서 얻은 token은 지우지 않는 이상, post 요청을 계속 보내면 계속 같은 token을 받는다.
2. 회원가입 (registration) 후 발급
registraion 후 token을 받기 위해서는 user model, serializer, view를 생성할 필요가 있다. 하지만 이미 django에서 제공하는 user model을 사용하고 있기 때문에 model을 따로 만들 필요가 없다.
django에서 제공하는 user model에는 username, email, password가 포함된다. serializer를 이용해서 기존 fields과 password2 field를 추가한다. model에 포함되어 있지 않는 field는 ‘variable_name = serializers.CharField()’와 같이 써서 추가할 수 있다.
# serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
class RegistrationSerializer(serializers.ModelSerializer):
password2 = serializers.CharField(style={'input_type':'password'}, write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'password2']
extra_kwargs = {
'password' : {'write_only': True}
}
- 새로 만든 field 내부를 보면 write_only=True로 설정되어 있다. 이것은 정보를 보낼 (write) 수는 있지만 읽어(read) 오진 못하는 것을 의미한다.
- 또한 serializer 내부에서 style을 지정할 수 있는데, key & value 형태로 작성하면 된다.
- password는 이미 user 모델에 정의되어 있는 field이기 때문에 password2와 같이 따로 관리할 수 없다. 따라서 write_only와 같은 설정을 하기 위해서는 extra_kwargs를 이용해서 지정할 수 있다.
# views.py
from rest_framework.decorators import api_view
from user_app.api.serializers import RegistrationSerializer
@api_view(['POST', ])
def registration_view(request):
if request.method == 'POST':
serializer = RegistrationSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return serializer.data
FBV를 이용하여 위와 같이 만들면 우선 TypeError가 발생한다. 해당 error를 해결하기 위해서는 save method를 override 해야 하고, serializers 안에서 save method를 재정의 하면 된다. 즉, 원래 save method가 가지고 역할은 그대로 가지고, own requirements에 맞게 몇 가지 역할을 추가하는 작업이 필요하다.
save method 안에서 password와 password2가 같은지를 체크하고, email이 unique 한지도 체크한다.
# serializers.py
class RegistrationSerializer(serializers.ModelSerializer):
....
def save(self):
password = self.validated_data['password']
password2 = self.validated_data['password2']
if password != password2:
raise serializers.ValidationError({'error':'both password should be same'})
if User.objects.filter(email=self.validated_data['email']).exists():
raise serializers.ValidationError({'error':'email already exists'})
account = User(email=self.validated_data['email'], username=self.validated_data['username'])
account.set_password(password)
account.save()
return account
account.set_password에 사용한 sat_password는, 회원가입 시 입력받은 password를 hash 하여 저장하는 함수이다.
이후 post 요청을 보내면 정상적으로 작동되고, 아래와 같은 결과를 볼 수 있다.
{
"username": "example",
"email": "example@aaa.com"
}
여기까지 하면 보통의 registraion 과정이 마무리되고, 이후 registration을 진행한 username과 password를 이용해 login을 하면 token을 발급받을 수 있다.
하지만 registraion 이후에 login 과정이 없어도 token이 바로 발급될 수 있게 처리하기 위해서는 몇 가지 로직을 추가를 해야 한다.
이를 위해서는 DRF에서 이미 제공하고 있는 함수를 활용해야 한다.
해당 함수를 models.py 추가하면 모든 user에 대해서 자동으로 token을 발급할 수 있다. 해당 함수는 새로운 user를 등록하면 작동한다.
# models.py
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
이후에 view에서 serializer.save() 후 token을 처리하면 된다.
# views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from user_app import models
from user_app.api.serializers import RegistrationSerializer
@api_view(['POST', ])
def registration_view(request):
if request.method == 'POST':
serializer = RegistrationSerializer(data=request.data)
data = {}
if serializer.is_valid():
account = serializer.save()
data['response'] = "Registration Successful"
data['username'] = account.username
data['email'] = account.email
token = Token.objects.get(user=account).key
data['token'] = token
else:
data = serializer.errors
return Response(data)
- 중간에 빈 딕셔너리를 만들어, 만들어지는 data를 딕셔너리에 저장하고, 이전에 Reponse로 serializer.data를 한 것과 다르게 딕셔너리를 직접 반환하게 한다.
- serializer.is_valid()를 통해 serializer가 유효하면 딕셔너리에 data를 저장하고, 아니면 error를 저장한다.
- 여기서 중요한 것은, import 내용 중에 from user_app import models이 있다는 것이다. 코드를 보면 import 한 model은 view에서는 직접적으로 사용되지 않는다. 하지만 model을 import 하지 않으면 DoesNotExist - Token matching query does not exist 에러가 발생한다.
post 요청이 가면 아래와 같이 registraion 후 token을 바로 발급할 수 있는 것을 알 수 있다.
{
"response": "Registration Successful",
"username": "example8",
"email": "example8@aaa.com",
"token": "03a534a88446955d306526de0f387d07a99a4586"
}
로그아웃 후 token 삭제
로그아웃 후에 token을 삭제하는 과정은 아래와 같이 view를 작성하면 된다.
# views.py
...
from rest_framework import status
...
@api_view(['POST', ])
def logout_view(request):
if request.method == 'POST':
request.user.auth_token.delete()
return Response(status=status.HTTP_200_OK)
Problem of Token Authentication
token authentication의 경우, token이 db에 저장된 후 token을 확인할 때마다 request를 한 번 더 보내게 된다. user가 적은 경우에는 큰 문제가 되지 않지만 user의 수가 많은 경우에는 문제가 발생할 수 있다.
'Django' 카테고리의 다른 글
[Django] DRF - API Testing (0) | 2022.06.01 |
---|---|
[Django] DRF 정리 - instance, validated_data, @api_view, reuiqred, ModelSerializer, Nested Serializer (4) | 2022.05.29 |
[Django] DRF - Permission (0) | 2022.05.28 |
[Django] Django ORM - N+1 problem (0) | 2022.04.09 |
[Django] corsheaders (0) | 2022.03.23 |