수학과의 좌충우돌 프로그래밍

[Django] Token 인증 적용하기, TokenAuthentication 본문

웹프로그래밍/DRF

[Django] Token 인증 적용하기, TokenAuthentication

ssung.k 2020. 1. 14. 22:56

DRF 에서 지원하는 인증은 다음과 같습니다.

  • rest_framework.authentication.SessionAuthentication

  • rest_framework.authentication.BasicAuthentication

  • rest_framework.authentication.TokenAuthentication

    • 초기에 username/password 으로 Token 발급
    • 이 Token을 매 API 요청에 담아서 보내어 인증을 처리

SessionAuthentication,BasicAuthentication 은 django default 이고 TokenAuthentication 은 따로 설정을 해줘야합니다.

그래서 기본적인 SessionAuthentication,BasicAuthentication 을 사용하면 좋겠지만 각각 문제점이 있습니다.

외부 서비스 / 앱에서는 세션인증은 사용 할 수 없고 매번 username/password 를 넘기는 것은 위험합니다.

이럴 경우 TokenAuthentication 을 사용해야 하니 이에 대해서 알아보도록 하겠습니다.

기본세팅

TokenAuthentication 을 적용하기 전에 기본적인 django project 를 설정해보겠습니다.

# models.py

from django.db import models
from django.conf import settings

class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='+', on_delete=models.CASCADE)
    message = models.TextField(blank=True)
    photo = models.ImageField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

ImageField 를 사용하기 위해서는 pillow 를 설치하셔야 합니다.

pip isntall pillow

 

authorusername 은 자동으로 저장하기 위해서 ReadOnlyField 를 사용하였습니다.

# serializers.py

from rest_framework.serializers import ModelSerializer, ReadOnlyField
from .models import Post

class PostSerializer(ModelSerializer):
    auth_username = ReadOnlyField(source='author.username')
    class Meta:
        model = Post
        fields = ['id', 'auth_username', 'message', 'photo']

 

token 인증을 하기 위해서 TokenAuthentication 을 설정해주고, Permissions 은IsAuthenticated 을 설정해주었습니다.

Permissions 과 관련된 내용은 아래 링크를 참고해주세요.

https://ssungkang.tistory.com/entry/Django-Authentication-%EA%B3%BC-Permissions

# views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

 

ViewSet을 사용했으므로 DefaultRouter 를 사용할 수 있습니다.

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet

router = DefaultRouter()
router.register('post', PostViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

 

httpie 로 request 를 보내보겠습니다.

그 결과, 아래와 같이 인증이 필요하다고 나옵니다.

http localhost:8000/post/

'''
HTTP/1.1 401 Unauthorized
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 58
Content-Type: application/json
Date: Sun, 12 Jan 2020 05:22:54 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Accept
WWW-Authenticate: Token
X-Frame-Options: SAMEORIGIN

{
    "detail": "Authentication credentials were not provided."
}
'''

 

Token 인증 설정

rest_framework 에서는 authtoken app 을 제공해주고 있습니다.

# settings.py

INSTALLED_APPS = [
  # 생략
  'rest_framework.authtoken',
]

 

이는 Token 이라는 model을 지원하고 있기때문에 migrate 를 해줘야합니다.

python manage.py migrate

 

Token model 은 다음과 같은 특징이 있습니다.

  • User model 과 1대1 관계
  • key 값을 저장하는 key필드와 생성한 날짜 시간을 저장하는 created 필드
  • key 필드는 primary_key 이기 때문에 각 User 별로 고유
  • 각 자리수는 16진수이고 최대 길이가 40 이므로 16^40
# rest_framework/authtoken/models.py

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 을 만들기 위해서는 ObtainAuthToken APIView 에 post 요청을 보내서 생성합니다.

class ObtainAuthToken(APIView):
    # 생략

    def post(self, request, *args, **kwargs):
				# 생략
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})

 

Token 획득을 API endpoint 로 노출

config 의 urls.py 에 이를 추가해줍니다.

# config/urls.py

from rest_framework.authtoken.views import obtain_auth_token

urlpatterns += [
  path('api-token-auth/', obtain_auth_token),
]

 

해당 url로 POST 요청을 보낼 시 password를 틀리면 error가 오게 되고

http POST localhost:8000/api-token-auth/ username="user1" password="잘못된 패스워드"
"""
HTTP/1.1 400 Bad Request
Allow: POST, OPTIONS
Content-Length: 68
Content-Type: application/json
Date: Mon, 13 Jan 2020 14:15:35 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Cookie
X-Frame-Options: SAMEORIGIN

{
    "non_field_errors": [
        "Unable to log in with provided credentials."
    ]
}
"""

 

제대로 된 password로 요청을 보내면 token이 만들어지게 됩니다.

이 때 계속 되는 요청에 대해 토큰을 계속 생성하지는 않고 이미 만들어진 같은 토큰을 반환합니다.

http POST localhost:8000/api-token-auth/ username="user1" password="정확한 패스워드"

"""
HTTP/1.1 200 OK
Allow: POST, OPTIONS
Content-Length: 52
Content-Type: application/json
Date: Mon, 13 Jan 2020 14:38:45 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Cookie
X-Frame-Options: SAMEORIGIN

{
    "token": "158b16d3ccd098c6a8025d29ccec18f994c0aed3"
}
"""

 

Token 사용하기

post 목록을 요청하기 위해서는 인증이 필요합니다.

인증이 없으면 request 를 보내도 인증이 필요하다고 나옵니다.

http localhost:8000/post/
  
"""
HTTP/1.1 401 Unauthorized
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 58
Content-Type: application/json
Date: Tue, 14 Jan 2020 12:55:08 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Accept
WWW-Authenticate: Token
X-Frame-Options: SAMEORIGIN

{
    "detail": "Authentication credentials were not provided."
}
"""

 

이 때 인증 token을 함께 넘겨주면 정상적으로 요청이 넘어옵니다.

token을 넘겨줄 때는 띄어쓰기도 조심해주셔야 합니다.

http localhost:8000/post/ "Authorization: Token 158b16d3ccd098c6a8025d29ccec18f994c0aed3"
"""
HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 126
Content-Type: application/json
Date: Tue, 14 Jan 2020 13:07:50 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Accept
X-Frame-Options: SAMEORIGIN

[
    {
        "auth_username": "root",
        "id": 1,
        "message": "root 가 하는 말",
        "photo": "http://localhost:8000/post/8ton_logo2_X7kfW29.jpeg"
    }
]
"""

 

마찬가지로 글을 작성할 때도 위와 같이 token을 넘겨줘서 작성할 수 있습니다.

http POST localhost:8000/post/ "Authorization: Token 158b16d3ccd098c6a8025d29ccec18f994c0aed3" message="hello"
"""
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 62
Content-Type: application/json
Date: Tue, 14 Jan 2020 13:37:02 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Accept
X-Frame-Options: SAMEORIGIN

{
    "auth_username": "root",
    "id": 2,
    "message": "hello",
    "photo": null
}
"""

 

image를 같이 업로드 하기 위해서는 다음과 같이 httpie 문법을 사용하여 넘겨줄 수 있습니다.

이 때는 form 으로만 넘겨야 합니다.

http --form POST localhost:8000/post/ "Authorization: Token 158b16d3ccd098c6a8025d29ccec18f994c0aed3" message="hello" photo@'django_image.jpg'
"""
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 103
Content-Type: application/json
Date: Tue, 14 Jan 2020 13:40:50 GMT
Server: WSGIServer/0.2 CPython/3.7.1
Vary: Accept
X-Frame-Options: SAMEORIGIN

{
    "auth_username": "root",
    "id": 3,
    "message": "hello",
    "photo": "http://localhost:8000/post/django_image.jpg"
}
"""
Comments