Django REST framework (2)

Authentication & Permissions, Relationships & Hyperlinked APIs

RESTDjangoauthentication

2017-09-04


Django REST framework๋Š” ๋‹ค์–‘ํ•œ ์•ฑ์—์„œ DB์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” API endpoint๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. Django REST framework Tutorial์„ ๋”ฐ๋ผํ•˜๋ฉฐ ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ‘‰ Django REST framework (1) - Serialization, Requests & Responses, Class-based views


1. Authentication & Permissions

  1. Adding information to our model

๋Œ€๋ถ€๋ถ„์˜ 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')
  1. Adding endpoints for our User models

/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

  1. Associating Snippets with Users

๊ทธ๋Ÿฐ๋ฐ 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 ์ƒ์†๋ฐ›๋Š”๋ฐ ์™œ ์ •๋ณด๊ฐ€ ์•ˆ๋“ค์–ด๊ฐ€์ง€?

  1. Updating our serializer
# serializer.py
class UserSerializer(serializers.ModelSerializer):
    ...
    owner = serializers.ReadOnlyField(source='owner.username')

source๋Š” ์–ด๋–ค attribute๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ ์ €๋ฅผ ๊ตฌ๋ถ„ํ•  ๊ฒƒ์ธ์ง€ ํŒ๋‹จํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” owner์˜ username์œผ๋กœ ๊ตฌ๋ถ„ํ•œ๋‹ค. ReadOnlyField๋Š” CharField(read_only=True)์™€ ๊ฐ™์€ ํ•„๋“œ๋กœ, ์กฐํšŒ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค.

  1. Adding required permissions to views

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 ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๊ฒŒ ๋œ๋‹ค.

  1. Adding login to the Browsable API

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 ๋ฒ„ํŠผ์ด ์ƒ๊ธด๋‹ค.

  1. Object level permissions

์ด์ œ 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"
}

2. Relationships & Hyperlinked APIs

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!