아직까지 그러나 치명적이게도 API 접근에 제한이 없다.
누구나 데이터를 조회하고 삭제하고 생성하고 업데이트할 수 있다.
snippet 프로젝트의 요구사항을 정리해보자.
owner
필드 추가highlighted
필드 추가class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default="")
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(
choices=LANGUAGE_CHOICES,
default="python",
max_length=100,
)
style = models.CharField(choices=STYLE_CHOICES, default="friendly", max_length=100)
owner = models.ForeignKey(
"auth.User", related_name="snippets", on_delete=models.CASCADE
)
highlighted = models.TextField()
class Meta:
ordering = ["created"]
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)
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Snippet.objects.all(),
)
class Meta:
model = User
fields = [
"id",
"username",
"snippets",
]
PrimaryKeyRelatedField
라는 기능을 사용했다.perform_create
를 오버라이딩하여 snippet 인스턴스 생성 전에 유저 정보를 넣어준다.
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def perform_create(self, serialzier):
serialzier.save(owner=self.request.user)
위에서 Snippet 이 생성될 때, owner정보가 입력되도록 하고 있다.
따라서, SnippetSerialzier에도 owner 정보가 표현되도록 수정한다.
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source="owner.username")
class Meta:
model = Snippet
fields = [
"id",
"title",
"code",
"linenos",
"language",
"style",
"owner",
]
source
argument는 필드를 생성하기 위해 어떤 속성을 참조해야 하는지 알려주며, 정의된 인스턴스의 필드라면 모두 접근할 수 있게 한다.
ReadOnlyField로 정의할 수도 있지만, CharField(read_only=True)로 정의할 수도 있다.
어떤 방법이든 필드를 위와 같이 정의하면, 업데이트 시 API 표현에 나오지 않고 읽기 행동 시에만 나타나게 된다.
코드 snippet이 owner(유저)와 연결되도록 했다.(models.py, views.py, serializer.py)
그러므로 CUD작업에 있어 인증된 유저만 가능하도록 리팩터링이 필요하다.
IsAuthenticatedOrReadOnly
를 도입할건데, 인증된 유저는 읽기쓰기 권한이 있고, 미인증 유저는 읽기 권한만 있다.
class SnippetList(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def perform_create(self, serialzier):
serialzier.save(owner=self.request.user)
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
브라우저에서 로그인할 수 있는 기능이 없다.
프로젝트의 URLconf를 urls.py로 수정하여 로그인 뷰를 추가한다.
urlpatterns += [
path("api-auth/", include("rest_framework.urls")),
]
우측 상단에 Log in
버튼이 생성됐다.
'api-auth/'
를 다른 이름으로 변경해도 상관없다.
커스텀 permission 객체를 만들어 본다.
# permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""객체의 소유자만 수정할 수 있는 커스텀 permission"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
# views.py
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
DRF는 디폴트로 아래 두 가지의 인증이 적용된다.
SessionAuthentication
BasicAuthentication
# settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
],
}
SessionAuthentication
는 웹 브라우저에서 로그인하여 적합한 인증된 Browsable API를 확인해 볼 수 있게 한다.
(만약 SessionAuthentication
을 해제하면, 웹 브라우저 환경에서 로그인할 수 없게 된다.)
하지만 DRF를 사용하는 주 목적은 API 자체의 인증이다. 각 요청에 자격인증이 부여돼야 한다.
자격 인증 없이 인증이 필요한 API를 호출하면 아래와 같이 에러가 발생한다.
Authentication credentials were not provided.
>> curl -X POST 'Accept: application/json; indent=4' http://127.0.0.1:8000/snippets/
curl: (3) URL rejected: Malformed input to a URL function
{"detail":"Authentication credentials were not provided."}
따라서, API에 인증 자격을 부여하여 POST 요청을 보내도록 수정한다.
>>> curl -X POST -u 'test:12341234' http://127.0.0.1:8000/snippets/ -d code='print(789)'
{
"id":2,
"title":"",
"code":"print(789)",
"linenos":false,
"language":"python",
"style":"friendly",
"owner":"test"
}