DRF3 튜토리얼 4 - 인증과 권한

원문 - Authentication & Permissions

번역을 허락해 준 Tom Christie에게 고마움을 전합니다.


튜토리얼 4: 인증과 권한

지금까지 우리가 만든 API에서는 누구라도 코드 조각을 편집하거나 삭제할 수 있습니다. 아무 제한이 없죠. 여기에 다음과 같은 고급 기능을 추가하고 싶습니다.

  • 코드 조각은 만든 사람과 연관이 있다.
  • 인증받은 사용자만 코드 조각을 만들 수 있다.
  • 해당 코드 조각을 만든 사람만, 이를 편집하거나 삭제할 수 있다.
  • 인증받지 않은 사용자는 '읽기 전용'으로만 사용 가능하다.

모델에 속성 추가하기

먼저 Snippet 모델을 조금 수정해보겠습니다.

필드를 두 개 추가합니다. 하나(owner)는 코드 조각을 만든 사람을 가리킵니다. 다른 하나는 하이라이트된 코드를 HTML 형태로 저장하는 데 사용됩니다.

models.py에 있는 Snippet 모델에 다음 내용을 추가하세요.

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

그리고 모델이 저장될 때 하이라이트된 코드를 highlighed 필드에 저장해야 합니다. 코드 하이라이팅에는 pygments 라이브러리를 사용합니다.

필요한 라이브러리들을 임포트하고,

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

모델에 .save() 메서드를 작성합니다.

def save(self, *args, **kwargs):
    """
    `pygments` 라이브러리를 사용하여 하이라이트된 코드를 만든다.
    """
    lexer = get_lexer_by_name(self.language)
    linenos = self.linenos and 'table' or False
    options = self.title and {'title': self.title} or {}
    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 tmp.db db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate

API를 테스트하는 데 사용할 사용자 계정을 만들고 싶다면, 다음과
같이 createsuperuser 명령을 사용하시면 됩니다.

python manage.py createsuperuser

사용자 모델에 엔드포인트 추가하기

사용자를 추가했으니 사용자를 보여주는 API도 추가합시다. 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'는 사용자 모델과 반대 방향으로 이어져 있기 때문에 ModelSerializer에 기본적으로 추가되지 않습니다. 따라서 명시적으로 필드를 지정해주었습니다.

사용자와 관련된 뷰도 추가해야겠죠. 읽기 전용 뷰만 있으면 되니까, 제네릭 클래스 기반 뷰 중에서 ListAPIViewRetrieveAPIView를 사용합시다.

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 클래스도 임포트하고요.

from snippets.serializers import UserSerializer

마지막으로 뷰에 URL을 연결합시다. urls.py 파일에 다음 내용을 추가합니다.

url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),

사용자가 만든 코드 조각 연결하기

지금까지는 코드 조각을 만들었더라도, 해당 코드 조각을 만든 사용자와 아무 관계도 맺지 않았습니다. 사용자는 직렬화된 표현에 나타나지 않았고, 요청하는 측에서 지정하는 속성이었을 뿐입니다.

이를 해결하기 위해 코드 조각 뷰에서 .perform_create() 메서드를 오버라이딩합시다. 이 메서드는 인스턴스를 저장하는 과정을 조정하며, 따라서 요청이나 요청 URL에서 정보를 가져와 원하는 대로 다룰 수 있습니다.

SnippetList 뷰 클래스에 다음 내용을 추가합니다.

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

우리가 만든 시리얼라이저의 create() 메서드는 검증한 요청 데이터에 더하여 'owner' 필드도 전달합니다.

시리얼라이저 업데이트하기

이제 코드 조각이, 해당 코드 조각을 작성한 사용자와 연결되었습니다. SnippetSerializer에도 이를 반영합시다. 다음 내용을 serializers.pySnippetSerializer에 추가합시다.

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

안내: Meta 클래스의 필드 목록에도 'owner'를 추가해야 합니다.

이 필드에는 조금 재미있는 면이 있는데요. source 인자로는 특정 필드를 지정할 수 있습니다. 여기에는 직렬화된 인스턴스의 속성 뿐만 아니라 위의 코드에서처럼 마침표 표기 방식을 통해 특정 속성을 탐색할 수도 있습니다. 마치 Django의 템플릿 언어와 비슷하죠.

이 필드는 CharFieldBooleanField와는 달리 타입이 없는 ReadOnlyField 클래스로 지정했습니다. 타입이 없는 ReadOnlyField는 직렬화에 사용되었을 땐 언제나 읽기 전용이므로, 모델의 인스턴스를 업데이트할 때는 사용할 수 없습니다. CharField(read_only=True)도 이와 같은 기능을 수행합니다.

뷰에 요청 권한 추가하기

이렇게 해서 코드 조각이 사용자와 연결되었습니다. 이제 인증 받은 사용자만 코드 조각을 생성/업데이트/삭제해봅시다.

REST 프레임워크는 특정 뷰에 제한을 걸 수 있는 권한 클래스를 제공하고 있습니다. 그 중 한 가지인 IsAuthenticatedOrReadOnly는 인증 받은 요청에 읽기와 쓰기 권한을 부여하고, 인증 받지 않은 요청에 대해서는 읽기 권한만 부여합니다.

뷰 파일에 다음 내용을 추가합니다.

from rest_framework import permissions

그리고 SnippetList 클래스와 SnippetDetail 클래스에 모두 다음 속성을 추가합니다.

permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

탐색 가능한 API에 로그인 추가하기

여러분이 지금 시점에 브라우저에서 API에 접속해본다면 더이상 새 코드 조각을 만들 수 없다는 사실을 알 수 있을 겁니다. 이를 해결하려면 사용자 로그인 기능이 필요합니다.

URL 설정 파일인 urls.py를 수정하면 탐색 가능한 API에 사용할 로그인 뷰를 추가할 수 있습니다.

다음 내용을 urls.py 파일의 첫 부분에 추가합니다.

from django.conf.urls import include

그리고 파일의 끝 부분에 다음 내용을 추가합니다. 탐색 가능한 API의 로그인 뷰와 로그아웃 뷰에 사용되는 url 패턴입니다.

urlpatterns += [
    url(r'^api-auth/', include('rest_framework.urls',
                               namespace='rest_framework')),
]

url 패턴에서 r'^api-auth/' 부분은 여러분이 사용하고 싶은 URL을 나타냅니다. 여기에는 한 가지 제약만 따르는데, 바로 namespace에 'rest_framework'를 지정해야 한다는 점입니다.

다시 브라우저로 돌아가 API에 접근해 보면 오른쪽 상단에 'Login' 링크가 보일 겁니다. 이제 앞에서 만들었던 사용자로 로그인하면 코드 조각을 만들 수 있습니다.

코드 조각을 몇 개 만든 후에는 '/users/'에도 가보세요. 해당 사용자가 만든 코드 조각 목록이 'snippets' 필드에 포함되어 있을 겁니다.

객체 수준에서 권한 설정하기

코드 조각은 아무나 볼 수 있어야 하지만, 업데이트와 삭제는 해당 코드를 만든 사용자만 할 수 있어야 합니다.

이를 위해 커스텀 권한을 만들어 봅시다.

snippets 앱 안에 permissions.py 파일을 만들고 다음 내용을 입력합니다.

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    객체의 소유자에게만 쓰기를 허용하는 커스텀 권한
    """

    def has_object_permission(self, request, view, obj):
        # 읽기 권한은 모두에게 허용하므로,
        # GET, HEAD, OPTIONS 요청은 항상 허용함
        if request.method in permissions.SAFE_METHODS:
            return True

        # 쓰기 권한은 코드 조각의 소유자에게만 부여함
        return obj.owner == request.user

이렇게 만든 커스텀 권한을 코드 조각 인스턴스에 추가합시다. SnippetDetail 클래스에 permission_classes 속성을 추가합니다.

permission_classes = (permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly,)

당연히 IsOwnerOrReadOnly 클래스를 임포트해야겠고요.

from snippets.permissions import IsOwnerOrReadOnly

다시 브라우저로 돌아가 보면, 코드 조각에 대한 'DELETE'와 'PUT' 기능은 해당 사용자에게만 나타날 겁니다.

API에 인증 붙이기

API에 권한을 설정했으므로, 이제는 코드 조각을 수정할 수 있는 인증 절차가 필요합니다. 지금까지는 인증 클래스를 만들지 않고 기본으로 제공되는 SessionAuthenticationBasicAuthentication을 사용했습니다.

웹 브라우저로 API를 사용하는 경우, 로그인을 하면 브라우저의 세션에 인증 정보가 저장됩니다.

프로그램 상에서 API를 사용하는 경우, 인증에 필요한 내용을 명시적으로 전달해야만 합니다.

인증 없이 코드 조각을 생성하려는 경우, 다음과 같이 에러를 보여줍니다.

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

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

사용자 계정과 비밀번호를 포함하여 요청한다면, 이 요청은 성공합니다.

http -a tom:password POST http://127.0.0.1:8000/snippets/ code="print 789"

{
    "id": 5,
    "owner": "tom",
    "title": "foo",
    "code": "print 789",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

요약

이렇게 해서 웹 API 위에 권한들이 잘 설정되었고, 사용자의 코드 조각에 대한 엔드 포인트도 완성되었습니다.

튜토리얼 5부에서는 이 모든 것을 한 데 엮어 하이라이트된 코드 조각을 보여주는 HTML 엔드 포인트를 만들고, API에서는 관련 모델에 대한 링크도 제공해 보겠습니다.

Show Comments