Skip to content

6. Authentication & Permissions


1. Adding information to our model

  • Snippet 모델 클래스에 몇 가지 변경 사항을 적용할 것이다.


  • 먼저, 몇 개의 필드를 추가해 보자.
  • 이러한 필드 중 하나는 코드 스니펫을 만든 사용자를 나타내는 데 사용되고, 다른 필드는 코드의 강조 표시된 HTML 표현을 저장하는 데 사용된다.
  • snippets/models.py 파일의 Snippet 모델에 다음 두 필드를 추가한다.


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


  • 또한 모델이 저장될 때 pygments 코드 강조 라이브러리를 사용하여 강조 표시된 필드를 채우는지 확인해야 한다.
  • 해당 파일에 다음과 같이 추가적으로 임포트한다.


snippets/models.py
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight


  • 이제 모델 클래스에 .save() 메서드를 추가할 수 있다.


snippets/models.py
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().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


2. Adding endpoints for our User models

  • 이제 작업할 사용자가 있으므로 해당 사용자를 표시해 보자.
  • snippets/serializers.py 파일에 다음과 같이 추가한다.


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


  • snippetsUser 모델의 반대 관계이므로 ModelSerializer 클래스를 사용할 때 기본적으로 포함되지 않으므로 이에 대한 명시적 필드를 추가했다.


  • 또한 snippets/views.py 파일에 몇 가지 뷰를 추가할 것이다.
  • 사용자 표현에 대해 읽기 전용 뷰만 사용하고 싶으므로 ListAPIViewRetrieveAPIView 일반(generics) 클래스 기반의 뷰를 사용한다.


snippets/views.py
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 클래스를 임포트했는지 꼭 확인한다.


snippets/views.py
from snippets.serializers import UserSerializer


  • 마지막으로 snippets/urls.py 파일의 urlpatterns를 다음과 같이 수정한다.


snippets/urls.py
urlpatterns = [
    path("snippets/", views.SnippetList.as_view()),
    path("snippets/<int:pk>/", views.SnippetDetail.as_view()),
    path("users/", views.UserList.as_view()),
    path("users/<int:pk>/", views.UserDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)


3. Association Snippets with Users

  • 지금 상황에서 코드 스니펫을 생성했다고 해도 스니펫을 생성한 사용자를 스니펫 인스턴스와 연결할 방법이 없다.
  • 사용자는 직렬화된 표현의 일부로 전송되지 않고 들어오는 요청의 속성으로 전송된다.
  • 이를 처리하는 방법은 스니펫 뷰에서 .perform_create() 메서드를 재정의하는 것이다.
  • 이를 통해 인스턴스 저장이 관리되는 방식을 수정하고 들어오는 요청 또는 요청된 URL에 내재된 모든 정보를 처리할 수 있다.
  • snippets/views.py 파일의 SnippetList 뷰 클래스에 다음 메서드를 추가한다.


snippets/views.py
def perform_create(self, serializer):
    serializer.save(owner=self.request.user)


  • 이제 직렬 변환기의 create() 메서드에 요청된 검증된 데이터와 함께 'owner' 필드가 전달된다.


4. Updating our serializer

  • 이제 스니펫이 이를 생성한 사용자와 연결되었으므로 이를 반영하도록 snippets/serializers.py 파일의 SnippetSerializer를 다음과 같이 업데이트한다.


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 인수는 필드를 채우는 데 사용되는 속성을 제어하고 직렬화된 인스턴스의 모든 속성을 가리킬 수 있다.
  • 또한 .(점) 표기법을 사용할 수도 있는데, 이 경우 Django의 템플릿 언어와 함께 사용되는 것과 유사한 방식으로 주어진 속성을 순회한다.
  • ReadOnlyField 클래스는 CharField, BooleanField 등과 달리 타입이 지정되지 않은 클래스이다.
  • 타입이 지정되지 않은 ReadOnlyField는 항상 읽기 전용이며 직렬화된 표현에 사용되지만 역직렬화될 때 모델 인스턴스를 업데이트하는 데 사용된다.
  • 여기서 CharField(read_only=True)를 사용할 수도 있다.


5. Adding required permissions to views

  • 이제 코드 스니펫이 사용자와 연결되었으므로 인증된 사용자만 코드 스니펫을 생성, 업데이트 및 삭제할 수 있도록 한다.
  • REST framework에는 주어진 뷰에 액세스할 수 있는 사람을 제한하는 데 사용할 수 있는 여러 권한 클래스가 포함되어 있다.
  • 그 중 하나는 IsAuthenticatedOrReadOnly이며, 인증된 요청은 읽기-쓰기 액세스 권한을 얻고 인증되지 않은 요청은 읽기 전용 액세스 권한을 얻는다.


  • 먼저, snippets/views.py 파일에 다음과 같이 임포트한다.


snippets/views.py
from rest_framework import permissions


  • 그런 다음 SnippetList와 SnippetDetail 클래스 기반의 뷰에 다음의 코드를 추가한다.


snippets/views.py
permission_classes = [permissions.IsAuthenticatedOrReadOnly]


6. Adding login to the Browsable API

  • 현재 상황에서는 웹 브라우저에서 새 코드 스니펫을 만들 수 없다.
  • 그렇게 하려면 사용자로 로그인해야 한다.


  • 프로젝트 루트 tutorial/urls.py 파일에서 URLConf를 편집하여 탐색 가능한 API(Browsable API)와 함께 사용할 로그인 뷰를 추가할 수 있다.
  • 먼저, 다음과 같이 임포트한다.


tutorial/urls.py
from django.urls import path, include


  • 그리고 맨 아래에 탐색 가능한 API에 대한 로그인 및 로그아웃 뷰를 포함하는 패턴을 추가한다.


tutorial/urls.py
urlpatterns += [path("api-auth/", include("rest_framework.urls"))


  • 패턴의 "api-auth/" 부분은 실제로 사용하려는 URL이 된다.
  • 이제 웹 브라우저를 다시 열고 페이지를 새로고침하면 페이지 오른쪽 상단에 'Login' 링크가 표시된다.
  • 이전에 생성한 사용자 중 하나로 로그인하면 코드 스니펫을 다시 생성할 수 있다.
  • 코드 스니펫을 여러 개 생성했으면 '/users/' 엔드포인트로 이동하여 각 사용자의 '스니펫' 필드에 각 사용자와 연결된 스니펫 ID 목록이 표시에 포함되어 있는지 확인한다.


7. Object level permissions

  • 모든 코드 스니펫을 누구나 볼 수 있게 하면서, 코드 스니펫을 만든 사용자만 코드 스니펫을 업데이트하거나 삭제할 수 있도록 해야 한다.
  • 그렇게 하려면 사용자 지정 권한을 만들어야 한다.
  • snippets/permissions.py 파일을 새로 생성한 후 다음과 같이 작성한다.


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 속성을 편집하여 사용자 지정 권한을 스니펫 인스턴스 엔드포인트에 추가할 수 있다.
  • 먼저 IsOwnerOrReadOnly 클래스를 임포트한다.


snippets/views.py
from snippets.permissions import IsOwnerOrReadOnly


  • permission_classes 속성을 다음과 같이 편집한다.


snippets/views.py
permission_classes = [
        permissions.IsAuthenticatedOrReadOnly,
        IsOwnerOrReadOnly,
    ]


  • 이제 웹 브라우저를 다시 열면, 코드 스니펫을 만든 동일한 사용자로 로그인한 경우에만 'DELETE' 및 'PUT' 작업이 스니펫 인스턴스 엔드포인트에 나타난다.


8. Authentication with the API

  • 이제 API에 대한 권한 집합이 있으므로 스니펫을 편집하려면 API에 대한 요청을 인증해야 한다.
  • 인증 클래스를 설정하지 않았으므로 현재 기본값인 SessionAuthenticationBasicAuthentication이 적용된다.
  • 웹 브라우저를 통해 API와 상호 작용할 때 로그인할 수 있으며 브라우저 세션은 요청에 필요한 인증을 제공한다.


  • 프로그래밍 방식으로 API와 상호 작용하는 경우 각 요청에 대해 인증 자격 증명을 명시적으로 제공해야 한다.
  • 만약 인증하지 않고 스니펫을 만들려고 하면 오류가 발생한다.


http POST http://127.0.0.1:8000/snippets/ code="print(123)"
HTTP/1.1 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 58
Content-Type: application/json
Date: Fri, 17 Dec 2021 10:01:53 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.8
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

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


  • 이런 경우 이전에 생성한 사용자 중 한 명의 사용자 이름과 비밀번호를 포함하여 요청할 수 있다.


http -a admin:django123 POST http://127.0.0.1:8000/snippets/ code="print(123)"
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 110
Content-Type: application/json
Date: Fri, 17 Dec 2021 10:03:07 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.8
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "code": "print(123)",
    "id": 3,
    "language": "python",
    "linenos": false,
    "owner": "admin",
    "style": "friendly",
    "title": ""
}

References