안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

장고 (Django)

[Django] REST framework - ④ Authentication & Permissions

피그브라더 2020. 6. 27. 13:31

본 포스팅은 아래 링크의 내용을 나름대로 정리한 글이다.

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/

 

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

 

1. 개요

지금까지 우리가 정의한 REST API 뷰들은 특별한 접근 권한이 존재하지 않아서 누구나 접근이 가능하다. 이와 관련하여 이번 포스팅에서 다룰 내용은 크게 다음과 같이 네 가지이다.

 

  • 각 Snippet 인스턴스가 한 명의 유저와 연관되도록 한다.
  • 오직 인증된(= 로그인한) 유저들만이 Snippet 인스턴스를 생성할 수 있도록 한다.
  • 각 Snippet 인스턴스는 그것을 생성한 유저만 수정 및 삭제가 가능하도록 한다.
  • 인증이 이뤄지지 않은(= 로그인하지 않은 상태의) 요청들은 오로지 읽기 권한만 가지도록 한다.

 

2. Snippet 모델에 필드 추가하기

Snippet 모델에 다음과 같이 두 개의 필드를 추가로 정의하자. 하나는 해당 Snippet 인스턴스를 생성한 유저를 저장하고, 나머지 하나는 해당 코드의 하이라이트 된 버전의 HTML 표현식을 저장한다. 그리고 Snippet 모델의 save() 메소드를 오버라이딩하여 추가로 정의한 highlighted 필드의 값을 자동으로 채워줄 수 있도록 하자. 이때 pygments 파이썬 라이브러리를 사용한다.

 

▼ snippets/models.py

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
def save(self, *args, **kwargs):
    # 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)

 

이제 데이터베이스 마이그레이션을 해주자. 원래는 모델의 변경 사항이 생겼기 때문에 마이그레이션 파일을 추가로 생성하고 마이그레이트를 수행하면 되지만, 여기서는 그냥 데이터베이스 자체를 날리고 새로 마이그레이션 파일을 만든 뒤 마이그레이트를 해보자. 또한 테스트에 사용할 유저를 하나 생성하자. 이를 위한 가장 빠른 방법은 "python manage.py createsuperuser" 커맨드를 이용하는 것이다.

 

▼ 데이터베이스 마이그레이션

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

 

▼ 슈퍼 유저 생성

python manage.py createsuperuser

 

3. User 모델의 시리얼라이저 및 REST API 뷰 정의하기

Snippet 모델에 대한 REST API가 존재하는 것과 마찬가지로, User 모델에 대해서도 REST API를 만들어 보자. 이를 위해, 먼저 User 모델에 대응하는 시리얼라이저를 정의하고, 이러한 시리얼라이저를 이용하여 REST API 뷰들을 정의하도록 하자.

 

▼ snippets/serializers.py

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/views.py

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

 

▼ snippets/urls.py

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

 

여기서 주목할 점은 두 가지이다. 첫째, 시리얼라이저에 정의된 snippets 필드는 User 모델의 역관계(Reverse Relationship)를 나타낸다. 역관계의 의미는 대략 이러하다. 여러 Snippet 인스턴스가 하나의 User 인스턴스를 가리키는 경우, 해당 User 인스턴스는 그러한 여러 개의 Snippet 인스턴스를 가리킨다고 볼 수 있다는 것이다. 그런데 User 모델에는 역관계에 대한 명시적인 필드가 존재하는 것이 아니므로, ModelSerializer 클래스를 사용하더라도 역관계 필드가 자동으로 불러와지지 않는다. 따라서 시리얼라이저에 snippets 필드를 명시적으로 정의해줘야 한다. 둘째, Snippet 모델과 달리 User 모델의 경우에는 List와 Retrieve 기능에 관한 REST API 뷰들만 정의하였다. 즉 User 인스턴스에 대해서는 읽기 작업만 수행할 수 있도록 한 것이다. 이때 클래스 기반의 뷰를 사용하였음에 주목하자.

 

4. Snippet 인스턴스 생성 시 User 연결하기

그러면 이제 Snippet 인스턴스를 생성할 때 request 객체로 넘어오는 User 인스턴스가 연결되도록 해보자. 이를 위해 SnippetList 뷰에서 다음과 같이 perform_create() 메소드를 오버라이딩 해준다. perform_create() 메소드는 create() 메소드가 호출하는 메소드로, 기본적으로는 인자로 전달되는 시리얼라이저에 대해 save() 메소드를 인자 없이 호출하도록 구현되어 있다. 이를 오버라이딩 하면 DB 인스턴스가 생성되는 방식을 커스터마이징 할 수 있을 뿐 아니라, request 객체로 전달되는 특정 정보들을 처리하는 방식도 수정이 가능해진다. 다음과 같이 SnippetList 뷰에 perform_create() 메소드를 오버라이딩하자. User 인스턴스의 정보는 request 객체에 담겨 있음을 볼 수 있다.

 

▼ snippets/views.py (SnippetList 뷰)

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

 

이로써 시리얼라이저의 save() 함수를 통해 Snippet 인스턴스를 생성할 때 User 인스턴스의 정보를 넘겨줄 수 있게 되었다. 시리얼라이저에는 owner 필드가 존재하지 않지만, Snippet 인스턴스를 생성할 때 시리얼라이저의 validated_data 뿐만 아니라 인자로 전달되는 owner 필드의 값도 사용하도록 구현한 것이다.

 

5. 시리얼라이저에 owner 필드 추가하기

이제 Snippet 인스턴스를 생성할 때 User 인스턴스를 연결할 수 있게 되었다. 그렇다면 이번에는 그렇게 연결된 User 인스턴스의 정보를 참조할 수 있도록 시리얼라이저에 다음과 같이 owner 필드를 추가로 정의해보자. 그리고 Meta 클래스의 fields에도 'owner'를 추가해주는 것도 잊지 말자.

 

▼ snippets/serializers.py

class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    
    class Meta:
        model = Snippet
        fields = ['id', 'title', 'code', 'linenos', 'language', 'style', 'owner']

 

여기서 크게 주목할 만한 점은 두 가지이다. 첫째, 필드에 명시되는 source 인자는 해당 필드의 값을 채우기 위해 사용할 속성을 명시하며, 시리얼라이즈 되는 DB 인스턴스의 그 어떤 속성이든 가리킬 수 있다. 또한 위와 같이 '.' 표현식을 사용하여 특정 속성의 속성을 참조하는 것도 당연하다. 이는 장고의 템플릿 문법과 유사하다.

 

둘째, 시리얼라이즈의 owner 필드가 ReadOnlyField 클래스로 정의되었다. 이는 CharField 혹은 BooleanField와 같이 타입이 명시된 필드가 아님을 의미한다. ReadOnlyField로 정의되는 필드들은 읽기 전용 특성을 가지게 되며, 오로지 시리얼라이즈를 통해 DB 인스턴스의 정보를 표현할 때만 사용이 된다. 즉, 디시리얼라이즈를 통해 DB 인스턴스를 생성 및 수정할 때는 사용되지 않는다. 이는 "read_only=True" 인자가 명시된 필드들의 경우에도 마찬가지이다. 위 코드의 경우 "CharField(read_only=True)"를 사용하더라도 동일한 결과를 얻는다.

 

6. REST API 뷰에 접근 권한 설정하기

이제 본격적으로 각 REST API 뷰에 접근 권한을 설정해줄 때가 왔다. 우선, 인증된(= 로그인한) 유저만 Snippet 인스턴스를 생성하고, 수정하고, 삭제할 수 있도록 해보자. DRF는 특정 뷰에 대한 접근 권한을 설정하기 위한 여러 개의 Permission 클래스들을 제공하고 있다. 그중에서 방금 말한 우리의 목적에 사용할 수 있는 클래스는 바로 IsAuthenticatedOrReadOnly이다. 이는 인증이 이뤄진(= 로그인한 상태의) 요청들은 읽기 및 쓰기 권한을 가지도록 하고, 인증이 이뤄지지 않은(= 로그인하지 않은 상태의) 요청은 읽기 권한만 가지도록 한다. 이를 사용하는 방법은 다음과 같다. 먼저 사용하고자 하는 Permission 클래스를 import 하고, 해당 접근 권한을 설정할 뷰의 permission_classes 변수에 그 Permission 클래스를 명시해주면 된다. 다음 코드처럼 말이다.

 

▼ views.py

from rest_framework import permissions
class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    
    def perform_create(self, serializer):
    	 serializer.save(owner=self.request.user)


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

 

7. Browsable API에 로그인 기능 추가하기

웹 브라우저로 REST API를 요청했을 때 보이는 페이지에서는 아직 로그인 기능이 없다. 따라서 Snippet 인스턴스를 생성, 수정, 삭제하는 것이 불가능할 것이다. 따라서 이러한 Browsable API에 로그인 기능을 추가해줄 필요가 있다. 그런데 이러한 기능 역시 DRF가 이미 제공하고 있다. 프로젝트 레벨의 urls.py의 내용을 다음과 같이 수정해주자. 'api-auth'는 본인의 입맛대로 다른 걸로 바꿔도 상관없다.

 

▼ tutorial/urls.py

from django.urls import path
from django.conf.urls import include

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

 

이제 웹 브라우저로 REST API를 요청해 보면 Browsable API에서 로그인 기능이 생겨났다는 것을 볼 수 있을 것이다. 이를 이용하여 앞서 생성해둔 슈퍼 유저 정보로 로그인을 하고 나면 드디어 Snippet 인스턴스를 생성, 수정, 삭제할 수 있게 된다. 그리고 Snippet 인스턴스를 생성하고 나서 본인에 해당하는 User 인스턴스의 정보를 조회해보면('/users/'), snippets 필드로서 해당 유저가 생성한 Snippet 인스턴스들의 정보가 조회된다는 것을 확인할 수 있을 것이다.

 

▼ Browsable API에 추가된 로그인 기능

 

8. 객체 수준의 접근 권한 설정

더 나아가, 특정 Snippet 인스턴스의 수정 및 삭제는 그것을 생성한 유저만 가능하도록 접근 권한을 설정해 보자. 이에 대한 내장 Permission 클래스는 없으므로 직접 정의해줘야 한다. 이를 위해, permissions.py 파일을 생성하고 다음과 같이 작성하자.

 

▼ snippets/permissions.py

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    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 클래스를 추가해주면 된다. 이후 웹 브라우저로 특정 Snippet 인스턴스의 정보를 REST API로 조회해 보면, 내가 생성한 Snippet 인스턴스인 경우에만 PUT, DELETE 버튼이 보이게 될 것이다.

 

▼ snippets/views.py (SnippetDetail 뷰)

from snippets.permissions import IsOwnerOrReadOnly
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

 

9. REST API 요청 시 인증 정보 넘기기

웹 브라우저로 우리의 REST API를 요청할 때는, 한 번만 로그인하면 그 정보가 서버의 세션에 저장되고 브라우저가 로그인 정보를 매 요청마다 서버에게 보내기 때문에 계속해서 인증된 요청만 보낼 수 있게 된다. 그러나 httpie 클라이언트로 직접 요청을 보내는 것과 같이, 프로그래밍적으로 REST API를 요청하는 경우에는 매 요청마다 명시적으로 인증 정보(Credential)를 같이 넘겨줘야 한다. 그렇지 않으면 인증되지 않은 요청으로 인식하게 될 것이다.

 

▼ 인증 정보를 명시하지 않은 요청

 

▼ 인증 정보를 명시한 요청 (-a username:password)