본문 바로가기

web/Django_rest_framework

[Django rest framework] 2. token / authentication

이번 포스팅의 소스는 여기에 있습니다.

 

- python3.6, django 2.1, django-rest-framework 3.8 을 사용합니다.


 

rest fremework는 기본적으로 템플릿 방식이 아닌, api 호출을 기반을 위주로 합니다.

또한 같은 도메인 호출도 있겠지만, 다른 도메인에서 호출하는 경우도 있습니다.

이때 해당 회원(권한)인지를 인식해야 되는 과정이 필요한데, 이를 위해 헤더에 해당 회원의 token을 포함하여 전송함으로써 서버에서는 해당 token값으로 회원을 인식합니다.

 

이번 포스팅에서는 DRF token 설정과 각 api에서의 auth 설정에 관한 내용이며,  API 서버에 자주 사용되는 JWT(Json Web Token) 인증을 장고(django)를 사용하여 구현하려고 합니다

 

JWT의 자세한 설명은 https://velopert.com/2389 를 참조하시면 됩니다.

 

 

1. 토큰 발급 및 auth 확인

 

https://jpadilla.github.io/django-rest-framework-jwt/

 

Django REST framework JWT

From here you can search these documents. Enter your search terms below.

jpadilla.github.io

먼저 JWT를 쓰기 위해 JWT 패키지를 설치합니다.

$pip install djangorestframework-jwt

 

먼저 settings.py에 authtoken을 사용한다고 명시해야 합니다.

# settings.py

INSTALLED_APPS = [
  ....
  'rest_framework.authtoken',
]


...............



JWT_AUTH = {
    'JWT_SECRET_KEY': SECRET_KEY,
    'JWT_ALGORITHM': 'HS256',
    'JWT_AUTH_HEADER_PREFIX': 'Token',
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
}

JWT_SECRET_KEY : JWT 발급을 위해선 암호화 키값이 필요합니다. 장고 프로젝트가 생성 시 있는 SECRET_KEY를 사용

JWT_ALOGITHM : JWT의 토큰 발급 시 사용될 암호화

JWT_AUTH_HEADER_PREFIX : header에 발급된 JWT토큰 앞에 들어갈 키워드입니다. (default : JWT )

JWT_EXPRIRATION_DELTA : 토큰이 발급되고 상용할 수 있는 기간입니다. ( 여기선 7일만 사용 가능하며, 그 후엔 재발급해야 합니다.)

 

그 외에 많은 옵션은 여기에서 확인해 보시면 됩니다.


각 유저별 토큰은 DB에 저장됩니다. token 테이블 생성을 위해 migrate을 실행합니다.

$ ./manage.py migrate

 

이제 프로젝트의 url에 token 발급을 위한 api을 설정해 줍니다.

# django_best_practice_example/urls.py

from rest_framework_jwt.views import obtain_jwt_token


urlpatterns = [
	......
    path('^api/auth/', obtain_jwt_token),
]

 

이제 서버를 재시작한 후 http://127.0.0.1:8000/api-token-auth/ 로 접속해보면

{"detail":"Method \"GET\" not allowed."}

의 화면이 나옵니다. 토큰 발행은 기본적으로 username / password를 기반으로 하며, post 메서드만 지원합니다.

 

토큰 발행을 위해  admin 유저를 생성해 보겠습니다.

$./manage.py createsuperuser

어드민 유저를 만들었다면 post를 날려 토큰 발행을 위해 postman을 통해 테스트해보도록 하겠습니다.

다운로드 링크 : www.postman.com/

 

Postman | The Collaboration Platform for API Development

Simplify each step of building an API and streamline collaboration so you can create better APIs—faster

www.postman.com

postman은 curl 테스트를 GUI 환경을 제공하며, 스크립트를 통해 http 테스트까지 제공하기 때문에 사용법을 익혀두시면, 테스트에 유용합니다.

 

 

post 메서드로 http://127.0.0.1:8000/api/auth/ 를 전송해보면 아래와 같이 필수 파라미터를 넣으라는 에러 메시지가 옵니다.

{
    "username": [
        "This field is required."
    ],
    "password": [
        "This field is required."
    ]
}

방금 생성했던 admin//패스워드를 파라미터로 넣어주고 다시 전송해 봅니다.

 

body탭에서 form-data 형식으로 파라미터와 값을 넣어준 후 전송해 보면 정상적으로 token이 발행된 것을 확인할 수 있습니다.

 

토큰에 대한 설정이 끝났으므로, 이제 호출하려는 API에 적용해 보도록 하겠습니다.

 

해당 http://127.0.0.1:8000/api/v1/post/ API에 token이 있을 때에만 호출되게 하려면 다음과 같이 설정합니다.

views.py에 적용

 

from rest_framework.viewsets import ModelViewSet

from .models import Post
from .serializers import PostSerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response


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

 

1편에서 만들었던 http://127.0.0.1:8000/api/v1/post/ 에 GET을 호출해 보면 데이터를 확인할 수 있습니다.

토큰을 넣었을 때

 

 

토큰이 없다면 401 authentication 에러가 리턴됩니다.

 

 

 

2. 로그인된(토큰 값의 유저)의 데이터만 볼 수 있게 하기

 

데이터에 작성자(owner)의 칼럼을 넣어서 로그인한 유저가 작성자(owner) 일 경우에만 데이터를 리턴하는 로직을 추가합니다.

 

모델 수정을 수정해 줍니다. owner 칼럼을 추가 합니다.

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


class OwnedModel(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)

    class Meta:
        abstract = True


class Post(OwnedModel):
    message = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

 

디비 적용에 적용하기 위해 migrate를 진행합니다.

$./manage.py makemigrations
$./manage.py migrate

번외

 

만약 필드를 추가했을 경우 원래 저장되어있던 객체들의 새로운 필드에는 어떤 조치를 취해야 하는지 물어보는 메시지라고 보면 된다.

이럴 땐 필드의 속성에 null=True를 추가해주거나 필드의 default 값을 설정해주면 된다.

 


 

permissions.py 파일을 추가해 줍니다.

해당 파일은 view의 response에 대한 제한을 위한 파일입니다.

# -*- coding: utf-8 -*-
from rest_framework import permissions


class IsOwner(permissions.BasePermission):
    message = "Not an owner."

    def has_object_permission(self, request, view, obj):
        return request.user == obj.owner

    # def has_permission(self, request, view):
    #     return True

rest_freamework.permissions를 상속받으면 has_object_permission / has_permission의 함수를 오버 라이딩할 수 있습니다.

has_object_permission : 객체를 하나인 메서드에서 호출 (get / put / delete)

has_permission : 객체가 없거나, 여러 개 일 경우 호출 (list / create)

하지만 list의 경우 객체 하나하나의 권한을 비교하는 것은 비효율적입니다. 쿼리문에서 해당 유저만 가져오면 되기 때문입니다.

 

 

serializers.py 수정

모델에 칼럼이 추가되었으므로, owner를 추가해줍니다.

# -*- coding: utf-8 -*-
from rest_framework import serializers
from .models import Post


class PostSerializer(serializers.ModelSerializer):
    owner = serializers.HiddenField(
        default=serializers.CurrentUserDefault()
    )

    class Meta:
        model = Post
        fields = ['id', 'message', 'created_at', 'owner']

 

 

views.py 수정

앞써 authentication_classes와 함께 permission_classes가 추가되었습니다.

permission_classes에서는 로그인이 되어 있는지, 해당 객체가 요청한 유저의 것인지를 판단해서 리턴해 줍니다.

또한 list의 경우 객체 하나하나를 확인하는 것보다 쿼리문을 통해 한번에 가져오기 위해서 오버 라이딩해줍니다.

from rest_framework.viewsets import ModelViewSet

from .models import Post
from .serializers import PostSerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from .permissions import IsOwner
from rest_framework.response import Response


class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsOwner, IsAuthenticated]
    authentication_classes = [JSONWebTokenAuthentication, SessionAuthentication]

    def list(self, request, *args, **kwargs):
        posts = self.get_queryset().filter(owner=request.user)
        serializer = self.get_serializer(posts, many=True)
        return Response({"results": serializer.data})

 

기존 데이터 수정

더미 데이터를 앞 썬 시간에 생성했습니다. 아직 owner에 값을 넣지 않았기 때문에

데이터베이스에 접속하여 (sqlite3) owner_id에 해당 유저의 user_id 넣습니다. 저의 경우 admin으로 확인하기 위해 1번을 넣었습니다.

 

 

postman으로 실제 테스트해봅니다. adminDml 토큰 값 세팅 후 요청을 하면 데이터가 오는 것을 확인할 수 있습니다.

 

 

비교를 위해서 다른 유저 생성을 생성합니다.

127.0.0.1:8000/admin으로 접속하여 새로운 계정을 추가해줍니다. (저의 경우 test라 이름을 지었습니다.)

 

 

 

이제 test 유저의 토큰 값을 세팅하여 같은 url로 접근하면 아래와 같이 에러 메시지가 나오는 것을 볼 수 있습니다.

(list의 경우 []이 리턴됩니다. 해당되는 값이 없기 때문입니다.)

 

 

이것으로 장고(django)에서 JWT를 사용하는 방법에 대해서 알아보았습니다. 이제 JWT 토큰을 사용하여 서버와 서버 간 / 프론트엔드와 서버간 통신에 사용할 수 있으며, 권한에 따라서 리턴되는 값을 조정할 수 있는 rest freamwork의 설정을 보았습니다.