일하는/Cloud, Web

Django REST Framework : (4) Authentication & Permissions

김논리 2020. 10. 12. 10:21

원문은 Django REST Framework 공식 페이지 🏠 www.django-rest-framework.org/tutorial/4-authentication-and-permissions/ 에서 확인할 수 있습니다.

 

ungodly-hour.tistory.com/26

 

Django REST Framework : (3) Class-based Views

원문은 Django REST Framework 공식 페이지 🏠 www.django-rest-framework.org/tutorial/3-class-based-views/ 에서 확인할 수 있습니다. ungodly-hour.tistory.com/24 Django REST Framework : (2) Requests a..

ungodly-hour.tistory.com


현재 우리가 만든 API에는 Snippet을 편집하거나 삭제할 수 있는 사용자에 대한 제한이 존재하지 않는다. 제대로 된 API를 만들기 위해서는 이와 관련하여 다음과 같은 고급 기능을 추가하여야 한다.

 

  • Snippet은 만든 사람과 연결(Link)되어 있다.
  • 인증된 사용자만이 Snippet을 만들 수 있다.
  • 해당 Snippet을 만든 사람만 편집하거나 삭제할 수 있다.
  • 인증되지 않은 요청에는 '읽기 전용' 권한으로 동작한다.

 

Adding information to our model

Snippet 모델 클래스에 몇개의 필드를 추가하여 수정해 보자. 하나는 Snippet을 만든 사용자 필드, 다른 하나는 코드를 HTML 형태로 저장하기 위해 사용되는 하이라이트 된 코드를 저장하는 필드이다. snippets/models.py 파일의 Snippet 클래스에 다름 내용을 추가한다.

 

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()

 

이제 모델이 저장될 때, 새로 추가가한 highlighted 필드에 하이라이트된 코드를 저장하기 위해 pygments 라이브러리에서 필요한 기능들을 추가하고, Snippet 모델에 .save() 함수를 추가하여 보자.

 

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

class Snippet(models.Model):
    ...
    def save(self, *args, **kwargs):
    """
    Use the `pygments` library to create a highlighted HTML
    representation of the code snippet.
    """
    lexer = get_lexer_by_name(self.language)
    linenos = 'table' if self.linenos else False
    options = {'title': self.title} if self.title else {}
    formatter = HtmlFormatter(style=self.style, linenos=linenos,
                              full=True, **options)
    self.highlighted = highlight(self.code, lexer, formatter)
    super(Snippet, self).save(*args, **kwargs)

 

모델 클래스가 업데이트되었으므로, 데이터베이스 테이블도 함께 업데이트되어야 한다. 일반적으로 이를 수행하기 위해서 데이터베이스 마이그레이션을 작성하지만, 이것은 튜토리얼이니 데이터베이스를 삭제하고 새로 만들도록 한다.

 

$ rm -f db.sqlite3
$ rm -r snippets/migrations
$ python manage.py makemigrations snippets
# python manage.py migrate

 

변경된 API를 테스트하기 위해 사용자 계정을 만들어야 한다. 이를 수행하는 가장 간편한 방법은 createsuperuser 명령을 사용하는 것이다.

 

$ python manage.py createsuperuser

 

Adding endpoints for our User models

이제 Snippets 모델이 사용자 정보가 추가되었으니, 사용자 정보를 보여주는 API도 추가해 보자. snippets/serializers.py 에 새로운 UserSerializer를 만든다.

 

from django.conrtib.auth.models import User

class UserSerizlizer(serializers.ModelSerializer):
    # 명시적으로 필드 지정
    snippets = serializers.PrimaryKeyRelatedFiedl(many=True, queryset=Snippets.objects.all())
    
    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

 

snippetsUser 모델 간의 관계가 반대 방향이므로, ModelSerializer 클래스에 기본적으로 포함되지 않아, 명시적으로 필드를 지정해 주었다.

 

또한 snippets/views.py에 사용자와 관련된 뷰도 추가해 주어야 한다. 읽기 전용 뷰만 필요하므로, generic class 기반 뷰 중에서 ListAPIViewRetrieveAPIView를 사용해 보자.

 

from django.contrib.auth.models import User
from snippets.serializers import UserSerializer


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

 

마지막으로 URL conf에서 해당 뷰를 참조할 수 있도록 snippets/urls.py 파일에 아래와 같이 패턴을 추가한다.

 

path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),

 

이제 User 모델을 위한 endpoint 추가도 완료되었다.

 

Associating Snippets with Users

지금 코드는 Snippet 인스턴스에 해당 Snippet을 만든 사용자를 연결할 방법이 존재하지 않는다. 직렬화(Serialization)하는 과정에 사용자 정보를 Snippet 인스턴스와 연결하는 로직이 없고, 요청하는 측에서 지정하는 하나의 속성으로 존재할 뿐이다.

이를 처리하는 방법은 Snippet 뷰에서 .perform_create() 함수를 오버 라이딩하여, 인스턴스 저장이 관리되는 방식을 요청 또는 요청된 URL에 내재된 정보를 처리할 수 있도록 수정하는 것이다.

 

우선 snippets/views.py 파일의 SnippetLists 클래스(뷰)에 다음 내용을 추가한다.

 

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

 

이렇게 하면, 우리가 만든 Serializercreate() 함수는 이제 validate 된 data와 함께 'owner' 필드도 전달하게 된다.

 

Updating our serializer

이제 Snippet이 사용자와 연결되었으므로, 이를 SnippetSerializer에도 반영해야 한다. snippets/serializers.pySnippetSerializer 클래스를 다음과 같이 수정한다.

 

class SnippetSerialiser(serializers.ModelSerializer):
    # owner 필드를 추가한다.
    owner = serializers.ReadOnlyField(source='owner.username')
    
    class Meta:
        model = Snippet
        # Meta 클래스에도 owner 필드를 추가한다.
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style', 'owner']

 

여기서 추가된 owner 필드는 꽤 흥미로운 일을 하고 있다. source 인자로 특정 필드를 채우는 데 사용되는 속성을 제어하며, 여기에는 직렬화된 인스턴스의 속성뿐만 아니라 위 코드에서 처럼 dot(.) 표기법을 사용할 수도 있다.

추가한 owner 필드는 CharFieldBooleanField와 달리 타입이 지정되지 않은 ReadOnlyField 클래스로 지정하였다. 타입이 없는 ReadOnlyField는 직렬화에 사용되었을 때, 읽기 전용이므로 반 직렬화(Deserialization) 하는 경우 모델의 인스턴스 업데이트에는 사용할 수 없다. 이 경우, ReadOnlyField 대신 CharField(read_only=True)를 사용하여도 동일한 기능을 수행한다.

 

Adding required permissions to views

이제 인증된 사용자만 Snippetcreate/update/delete 할 수 있도록 만들어 보자. DRF는 특정 뷰에 접근할 수 있는 사용자를 제한할 수 있는 permission 클래스를 제공한다.

인증된 요청에는 read-write 권한을 부여하고, 인증되지 않은 요청에는 read-only 권한을 부여할 수 있는 IsAuthenticatedOrReadOnly 클래스를 사용해 보자. snippets/views.py 파일에 다음 내용을 추가한다.

 

from rest_framework import permissions   # permissions 클래스를 임포트한다.


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    # 권한을 위한 필드를 추가한다.
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

class SnippetDetail(generic.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    # 권한을 위한 필드를 추가한다.
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

 

Adding login to the Browsable API

지금 서버를 실행하고 브라우저에서 API에 접속하면, 더 이상 새로운 Snippet을 추가할 수 없을 것이다. (POST 버튼이 보이지 않음) 이를 해결하기 위해서는 사용자 로그인 기능이 필요하다. 프로젝트 수준의 URL 설정 파일인 tutorial/urls.py 파일을 수정하여 browsable API와 함께 사용할 로그인 뷰를 추가할 수 있다.

 

urlpatterns = [
    ...
    # browsable API의 로그인/로그아웃 뷰에 대한 패턴을 추가한다.
    path(r'api-auth/', include('rest_framework.urls')),
]

 

URL 패턴에서 r'api-auth/'는 사용하고 싶은 URL을 나타낸다. 이제 해당 파일을 저장하고 다시 브라우저를 열어 API 페이지를 새로 고치면 오른쪽 상단에 'Login' 링크가 표시되고, 앞에서 createsuperuser 명령어를 사용하여 만든 사용자 정보로 로그인하면, 새로운 Snippet을 만들 수 있을 것이다. 몇 개의 Snippet을 만든 후, '/users/' endpoint로 이동하여 새로 만든 Snippet 목록이 'snippets' 필드에 포함되어 있는지 확인해 보자.

 

Object level permissions

만들어진 모든 Snippet은 사용자 누구나 볼 수 있어야 하지만, 업데이트와 삭제는 해당 Snippet을 만든 사용자만 할 수 있어야 한다. 현재는 로그인된 모든 사용자가 다른 사용자가 만든 Snippet을 수정하거나 삭제할 수 있을 것이다. 이를 막기 위해서는 사용자 지정 권한을 만들어야 한다.

snippets 애플리케이션 안에 permissions.py 파일을 만들고 다음 내용을 추가해 보자.

 

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

 

이제 SnippetDetail 뷰 클래스에서 permission_classes 속성을 편집하여, Snippet 인스턴스의 endpoint에 해당하는 사용자에 대한 권한을 추가할 수 있다.

 

from snippets.permissions import IsOwnerOrReadOnly

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    ...
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]
    ...

 

다시 브라우저를 새로고침 하면, Snippet을 생성한 동일한 사용자로 로그인한 경우에만 DELETEPUT 기능이 나타날 것이다.

 

Authenticating with the API

이제 API에 대한 권한 설정이 완료되었으므로, Snippet을 수정하려면 인증 절차가 필요하다. 현재는 별도 인증 클래스를 만들지 않고 기본으로 제공되는 SessionAuthenticationBasicAuthentication을 사용하고 있다. 웹 브라우저를 통해 API를 사용하는 경우, 로그인을 할 수 있으며, 세션에 필요한 인증 정보가 저장된다. 프로그래밍 방식으로 API를 사용하는 경우, 각 요청에 대해 인증에 필요한 정보를 명시적으로 전달해야 한다. 인증하지 않고 Snippet을 생성하려는 경우, 다음과 같이 오류가 발생한다.

 

$ http POST http://127.0.0.1:8000/snippets/ code="print(123)"

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

 

앞서 만든 사용자 중 하나의 사용자 정보를 포함하면, 해당 요청을 성공적으로 수행할 수 있다.

 

$ http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"

{
    "id": 1,
    "owner": "admin",
    "title": "foo",
    "code": "print(789)",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

 


ungodly-hour.tistory.com/29

 

Django REST Framework : (5) Relationships & Hyperlinked APIs

원문은 Django REST Framework 공식 페이지 🏠 www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/ 에서  확인할 수 있습니다. ungodly-hour.tistory.com/28 Django REST Frame..

ungodly-hour.tistory.com