Authentication & Permissions, Relationships & Hyperlinked APIs
2017-09-04
Django REST framework๋ ๋ค์ํ ์ฑ์์ DB์ ์ ๊ทผํ ์ ์๋ API endpoint๋ฅผ ๋ง๋ค์ด์ฃผ๋ ํ๋ ์์ํฌ์ ๋๋ค. Django REST framework Tutorial์ ๋ฐ๋ผํ๋ฉฐ ๊ณต๋ถํ ๋ด์ฉ์ ์ ๋ฆฌํ์ต๋๋ค.
๐ Django REST framework (1) - Serialization, Requests & Responses, Class-based views
๋๋ถ๋ถ์ API๋ authenticated ๋ ์ ์ ๋ง์ด ๊ธ์ ์์ฑํ๊ณ , ๋ณธ์ธ์ด ์์ฑํ ๊ธ๋ง ์์ /์ญ์ ํ ์ ์๋๋ก ํด์ผํ๋ค. ์ผ๋จ Model์ User ํด๋์ค๋ฅผ ์ถ๊ฐํ๋ค. ํํ ๋ฆฌ์ผ์์๋ pygments๋ผ๋ ์ฝ๋ ํ์ด๋ผ์ดํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ถ๋ฌ์์ค๋ค.
# models.py
...
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
class Snippet(models.Model):
...
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
๊ทธ๋ฆฌ๊ณ ์ด๋ฅผ ์ ์ฅํ .save()
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
...
def save(self, *args, **kwargs):
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) # super - ์์ ๊ผฌ์์๋ ์ฌ์ฉํ๋. ์ด๊ธฐํ ํจ์.
๊ธ์ ์์ฑํ User๋ฅผ python manage.py createsuperuser
๋ช
๋ น์ ํตํด ์ถ๊ฐํด ์ค๋ค.
User์ ๋ํ serializer๋ serializers.py์ ์ถ๊ฐํด์ค๋ค. Snippet ๋ชจ๋ธ์ owner๊ฐ ForeignKey๋ก ๊ฑธ๋ ค์๊ธด ํ์ง๋ง ์๋์ฒ๋ผ snippets๋ฅผ ๋ช ์ํด์ค์ผ ํ๋ค. (Because 'snippets' is a reverse relationship on the User model, it will not be included by default when using the ModelSerializer class, so we needed to add an explicit field for it.)
# serializers.py
...
from django.contrib.auth.models import User
...
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = User
fields = ('id', 'username', 'snippets', 'owner')
/users
endpoint๋ก ์ ๊ทผํ ์ ์๋๋ก view์ url๋ ์๋์ฒ๋ผ ์ ์ํ๋ค.
# views.py
from snippets.serializers import UserSerializer
...
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer # ๊นํ ์์ค์ฝ๋์ class UserSerializer ์ฃผ์์ฒ๋ฆฌ ๋์ด ์์...?
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all() # detail์ธ๋ฐ, ์ ๋ค๋ถ๋ฌ์ค์ง?
serializer_class = UserSerializer
# urls.py
urlpatterns = [
...
url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view())
]
** .as_view()
๋ฉ์๋ - Returns a callable view that takes a request and returns a response
๊ทธ๋ฐ๋ฐ snippet ์ธ์คํด์ค๊ฐ ์์ฑ๋ ๋ serialize ๋๋ ์ ๋ณด์ user๋ ์์ง ๋ค์ด๊ฐ์ง ์๋๋ค. ๊ทธ๋์ SnippetList
ํด๋์ค์ create ๋ฉ์๋๋ฅผ perform_create()
๋ก ์ค๋ฒ๋ผ์ด๋ฉํด์ฃผ๊ณ ๊ทธ ์์ ์ ์ ์ ๋ณด๋ฅผ ๋ด๋๋ค.
# views.py
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
์ด๋ ๊ฒ ํ๋ฉด serializer์ ์๋ create()
์ owner๋ผ๋ ์ถ๊ฐ ์ ๋ณด๊ฐ ๋ค์ด๊ฐ ๊ฒ์ด๋ค.
** ๊ทผ๋ฐ models.py์ owner ์ ์ํ๊ณ serializer๋ modelserializer ์์๋ฐ๋๋ฐ ์ ์ ๋ณด๊ฐ ์๋ค์ด๊ฐ์ง?
# serializer.py
class UserSerializer(serializers.ModelSerializer):
...
owner = serializers.ReadOnlyField(source='owner.username')
source๋ ์ด๋ค attribute๋ฅผ ๊ธฐ์ค์ผ๋ก ์ ์ ๋ฅผ ๊ตฌ๋ถํ ๊ฒ์ธ์ง ํ๋จํ๋ค. ์ฌ๊ธฐ์๋ owner์ username์ผ๋ก ๊ตฌ๋ถํ๋ค. ReadOnlyField
๋ CharField(read_only=True)
์ ๊ฐ์ ํ๋๋ก, ์กฐํ๋ง ๊ฐ๋ฅํ๋ค.
REST framework์์ ์ ๊ณตํ๋ ๋ค์ํ permission class ์ค์ IsAuthenticatedOrReadOnly
๋ฅผ SnippetList, SnippetDetail ํด๋์ค์ ์ถ๊ฐํด์ค๋ค.
# views.py
from rest_framework import permissions
class SnippetList(generics.ListAPIView):
...
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
class SnippetDetail(generics.RetrieveAPIView):
...
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
์ด ํ๋กํผํฐ๋ฅผ ์ถ๊ฐํ ๋, tuple์ ๋ง๋ค์ด์ฃผ๊ธฐ ์ํด ๊ผญ ,
๋ฅผ ์ถ๊ฐํด์ผํ๋ค. ์ด๊ฒ ์์ผ๋ฉด TypeError: 'type' object is not iterable
์ค๋ฅ๊ฐ ๋๊ฒ ๋๋ค.
localhost:8000/snippets/
์ ์ ๊ทผํ๋ฉด ์ด์ snippets๋ฅผ ์ถ๊ฐํ ์ ์๊ฒ ๋๋ค. ๋ธ๋ผ์ฐ์ ์์ ๋ก๊ทธ์ธ์ด ๊ฐ๋ฅํ๋๋ก urls.py๋ฅผ ์์ ํด์ผ ํ๋ค.
# urls.py
from django.conf.urls import include
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
์ด๊ฑธ ์ถ๊ฐํ๊ณ ๋ค์ localhost:8000/snippets/
์์ ๋ธ๋ผ์ฐ์ ๋ฅผ ์๋ก๊ณ ์นจํ๋ฉด ์ฐ์ธก ์๋จ์ Login
๋ฒํผ์ด ์๊ธด๋ค.
์ด์ snippet์ ์์ฑํ ๋ณธ์ธ๋ง์ด ์ด๋ฅผ ์์ /์ญ์ ํ ์ ์๋๋ก permission์ ์ถ๊ฐํด์ค์ผ ํ๋ค. snippets ์ฑ์ permissions.py๋ฅผ ์ถ๊ฐํ๋ค.
# permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
BasePermission์ด ์ด๋ป๊ฒ ์๊ฒผ๋์ง ๊ถ๊ธํด์ ์์ค์ฝ๋๋ฅผ ๋ดค๋๋ return True
๋ฐ์ ์๋ค... ๋๋ ํด๋ฆฐํ ์ด ์ฝ๋๋ ๋๋์ฒด ๋ญ์ง ใ
ใ
aใ
# django-rest-framework/rest_framework/permissions.py
class BasePermission(object):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return True
์ด์จ๋ permissions.py์ ์ด๋ ๊ฒ ์ถ๊ฐํด์ค IsOwnerOrReadOnly๋ฅผ views.py์ ์ํฌํธํ๊ณ SnippetList, SnippetDetail ํด๋์ค์ permissions_classes์ ์ถ๊ฐํด์ค๋ค.
# views.py
from snippets.permissions import IsOwnerOrReadOnly
class SnippetList(generics.ListAPIView):
...
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
class SnippetDetail(generics.RetrieveAPIView):
...
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
์ด์ ์ฝ์์ฐฝ์์๋ id์ ํจ์ค์๋๋ฅผ ์ ๋ ฅํ๋ฉด http POST ์์ฒญ์ ๋ณด๋ผ ์ ์๋ค.
http -a admin:password POST http://127.0.0.1:8000/snippets/ code="test"
{
"id": 1,
"owner": "admin",
"title": "foo",
"code": "test",
"linenos": false,
"language": "python",
"style": "friendly"
}
from rest_framework.decorators import api_view
from rest_framework.reverse import reverse
@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})
format=None
์ ์ง์ ํด์ค์ผ๋ก์ ํน์ ํฌ๋งท์ ๋ช
์ํ url๋ก๋ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋๋ก ๋ง๋ค์ด์ค๋ค.(์ฐธ๊ณ )To be continued!