SW개발/Django

[Django]Django REST Framework 튜토리얼 4 (Authentication & Permissions)

이번 포스팅에서는 DRFAuthentication & Permissions 공식 문서를 공부하면서 번역해보려고 한다.
해석에 틀린 내용이 있을 수 있으니 이해가 안가는 부분은 아래의 공식문서를 참조하기 바란다.

 

DRF Authentication & Permissions tutorial 공식 Documentation

 

4 - Authentication and permissions - Django REST framework

Currently our API doesn't have any restrictions on who can edit or delete code snippets. We'd like to have some more advanced behavior in order to make sure that: Code snippets are always associated with a creator. Only authenticated users may create snipp

www.django-rest-framework.org

현재 API는 제약 없이 누구나 code snippet을 수정하고 삭제할 수 있다. 아래와 같은 고급 기능을 추가하려고 한다.

  • Code snippet은 항상 만든 사람과 연관이 있어야 한다
  • 인증된 유저만 snippet을 만들 수 있다
  • 만든 사람만 updatedelete가 가능하다
  • 미인증 유저는 읽기 권한만 가지게 한다
Adding information to our model

먼저 Snippet 모델을 약간 수정할 것이다. 첫째로, 몇가지 필드를 추가할 것이다. 이 필드들중 하나는 code snippet을 누가 생성하였는지 보여주는데 사용될 것이다. 다른 필드는 codeHTML 표현을 하이라이팅 하는 것을 저장하는데 사용할 것이다.

models.pySnippet 모델에 두 필드를 추가하자.

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

또한 모델이 저장 될 때 강조된 코드를 highlight드에 저장해야 한다. 코드 하이라이팅을 위해 pygments 라이브러리를 사용한다.

다음과 같은 라이브러리들을 import 하자.

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

모델 클래스에 .save() 메서드를 추가하자.

def save(self, *args, **kwargs):
    """
    code snippet의 하이라이팅을 생성하기 위해
    `pygments` 라이브러리를 사용한다
    """
    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

사용자를 추가하였으니 사용자를 보여주는 API도 추가하자. Serializers.py에 새로운 Serializer를 작성하자.

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

'snippets' User model과 역의 관계이기 때문에 ModelSerializer에 기본적으로 추가되지 않는다. 따라서 명시적으로 필드를 작성해주어야 한다.

또한 views.py 에 view를 추가해주어야 한다. 사용자를 표시하기 위한 읽기 전용 view만 필요하기 때문에 generic 클래스 기반 뷰의 ListAPIView RetrieveAPIView를 사용한다.

from django.contrib.auth.models import User


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


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

또한, UserSerializer 클래스도import 한다.

from snippets.serializers import UserSerializer

마지막으로 URL conf 에서 이러한 view들을 API에 추가해야 한다. snippets/urls.py 패턴을 참고해 다음과 같이 작성하자.

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

 

Associating Snippets with Users

지금까지는 어떠한 code snippet을 만들더라도 사용자와 아무런 관계가 없었다. 사용자는 serialized된 표현에 나타나지 않았고 들어오는 요청의 속성이었을 뿐이다.

이를 해결하기 위해 snippet view.perform_create() 메서드를 오버라이딩하자. 이 메서드는 인스턴스 저장이 관리되는 방식을 수정하고, 들어오는 요청 또는 요청된 URL에서 정보를 가져와 원하는 대로 다룰 수 있다.

SnippetList view 클래스에 다음과 같은 메서드를 추가한다.

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

serializer 에서 create() 메서드는 검증된 요청 데이터에 'owner' 필드를 더해서 전달한다.

Updating our serializer

이제 Snippetssnippet을 생성한 사람과 연관이 되었다. SnippetSerializer 에도 이를 반영하자.

serializers.py에 다음과 같은 serializer 정의 필드를 추가하자.

owner = serializers.ReadOnlyField(source='owner.username')

Note : Meta 클래스의 필드 목록에도 'owner'  필드를 추가해야 한다

이 필드는 조금 재미있다. source 인자는 필드를 채우는데 사용되는 속성을 제어하고 serialized 된 인스턴스의 모든 속성을 가리킬 수 있다.

또한 위에 표시한 것처럼 점(.) 표기법을 사용할 수 있고 이것은Django의 템플릿 언어와 유사하다.

추가한 필드는 CharField, BooleanField 와 같은 다른 필드 유형과는 달리 유형이 지정되지 않은 ReadOnlyField 클래스이다.

형식화 되지 않은 ReadOnlyField 는 항상 읽기 전용이고 serialized 된 표현들에 사용된다, 하지만 읽기 전용이므로 deserialized 할 때 모델 instance 를 업데이트 하는 데는 사용할 수 없다. CharField(read_only=True)도 같은 기능을 수행할 수 있다.

Adding required permissions to views

이제 code snippet이 사용자와 연결 되었다. 이제 인증받은 사용자만 code snippetcreate/update/delete 할 수 있게 해보자.

REST framework는 주어진 view에 엑세스 할 수 있는 사람을 제한하는데 사용될 여러 permission 클래스가 포함되어 있다. 지금의 경우 IsAuthenticatedOrReadOnly 클래스를 사용함으로써 인증된 요청은 읽기/쓰기 권한을, 인증되지 않은 요청은 읽기 권한만 주게 한다.

view 모듈에 permission 클래스를 import 하자.

from rest_framework import permissions

SnippetList SnippetDetail view 클래스에 아래와 같은 속성을 추가하자.

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

 

Adding login to the Browseable API

브라우저를 열고 API를 접속하는 순간 새로운 code snippet을 만들 수 없다는 사실을 알게 될 것이다. 이를 해결하려면 사용자 로그인 기능이 필요하다. APIlogin view를 추가하고 urls.pyURLconf를 수정해야 한다.

아래와 같이 import 하자.

from django.urls import path, include

그리고, 파일의 마지막 부분에 APIlogin, logout 패턴을 추가하자.

urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]

urlpattern 에서 'api-auth' 부분은 실제로 사용하기 원하는 URL이 되는 부분이다.

이제, 브라우저에 접속하여 새로고침을 하면 오른쪽 상단에 Login 표시를 볼 수 있다. 이전에 만들었던 사용자로 로그인 한다면 다시 code snippet을 생성할 수 있다.

몇개의 code snippets 를 만들고 '/users/' endpoint로 이동하면, 해당 사용자가 만든 snippetid의 목록이 각각의 사용자의 'snippets' 필드에 보여질 것이다.

Object level permissions

모든 code snippet의 목록은 누구에게나 보여져야 하지만, 수정과 삭제는 code snippet을 생성한 사람만 할 수있게 해야 한다.

snippet 앱 안에서 permissions.py 파일을 새로 만들자.

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    code snippet의 owner 만 수정할 수 있게 하는 Custom permission 이다
    """

    def has_object_permission(self, request, view, obj):
        # 읽기 권한은 어떤 요청에든 허용되어진다
        # 따라서 GET, HEAD, OPTIONS 요청은 항상 허용한다
        if request.method in permissions.SAFE_METHODS:
            return True

        # 쓰기 권한은 snippet의 owner만 허용하도록 한다
        return obj.owner == request.user

이제 SnippetDetail view 클래스에서 permission_classes 속성을 수정함으로써 snippet instance 의 endpoint에 custom permisson을 적용할 수 있다.

permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

당연히, IsOwnerOrReadOnly 클래스도 import 해야 한다.

from snippets.permissions import IsOwnerOrReadOnly

다시 브라우저에 접속해보면, code snippet'DELETE', 'PUT' action은 생성한 사용자에게만 나타날 것이다.

Authenticating with the API

API에 대한 권한을 설정하였으므로, 이제는 code snippet을 수정하려면 request에 대한 인증이 필요하다.

어떠한 authentication 클래스도 설정하지 않았으므로 현재는 기본값인 SessionAuthenticationBasicAuthentication이 설정 되어있다.

웹 브라우저를 통해 API에 이용할 때 로그인 할 수 있고, 브라우저의 세션은 request에 필요한 인증을 제공한다.

프로그래밍 방식을 통해 API를 이용할 경우 각각의 request에 대해 authentication credentials(인증 자격 증명)을 명시적으로 적어야 한다. 만약 인증 정보 없이 snippet을 생성할 경우 다음과 같은 에러가 발생된다.

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

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

미리 만들어둔 사용자의 이름과 비밀번호를 포함해 요청한다면 성공적인 request가 가능하다.

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"
}

 

Summary

Web API에 상당히 세밀하게 권한들이 설정되었고, 사용자 및 사용자들이 만든 code snippet에 대한 end point도 완성되었다.

tutorial part 5 에서는 highlightedcode snippets에 대한 HTML endpoint를 생성하여 한데 모으는 방법을 살펴볼 것이고, 시스템 내의 관계들에 hyperlink를 사용하여 응집력을 향상시켜볼 것이다.


튜토리얼을 진행하면서 버전에 따라 에러가 발생되는 현상을 찾을 수 있었다. 

djangorestframework의 버전이 3.12.2 와 같이 튜토리얼보다 높을 경우 뷰의 함수를 바꿔주어야 한다.

"""
위는 변경전 코드이다
"""
class SnippetList(APIView):
    """
    코드 snippet의 목록을 보여주거나, 새로 snippet 생성
    """

    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get(self, request, format=None):
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

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

"""
아래는 변경된 코드이다
"""
class SnippetList(APIView):
    """
    코드 snippet의 목록을 보여주거나, 새로 snippet 생성
    """

    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get(self, request, format=None):
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(owner=self.request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

위의 방식처럼 perform_create 함수를 제거하고, post 함수에서 owner=self.request.user 속성을 추가해주면 정상 작동한다.

 

728x90