Django Restful framework Tutorial 5

인증과 권한

지금 까지 만든 API는 누구라도 편집, 삭제 할 수 있다. 고급 기능을 추가하고 싶다.

  • 모델 인스턴스를 만든 사람과 연관이 있다.
  • 인증 받은 사용자만 모델 인스턴스를 만들 수 있다.(데이터 베이스에 있는 유저)
  • 해당 모델 인스턴스를 만든 사람만 이를 편집하거나 삭제 할 수 있다.
  • 인증 받지 않은 사용자는 ‘읽기 전용’으로만 사용 가능하다.

우선 유저 모델을 만들고 유저모델을 SomeModel에 추가하자.

# app
./manage.py startapp members

# members/models.py

from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    pass

# settings.py
8INSTALLED_APP = [
    ...
    'members',
]
AUTH_USER_MODEL = 'members.User'

# app
# db.sqlite3 지우고
# mighrations 들 지우고
./manage.py makemigrations
./manage.py migrate

SomeModel에 owner등록

from django.conf import settings

owner = models.ForeignKey(settings.AUTH_USE_MODEL, related_name='somemodels', on_delete=models.CasCase

이제 사용자를 만들었으니 사용자를 보여주는 API도 추가 members 앱에 serializers.py추가

from django.contrib.auth import get_user_model
from rest_framework import serializers

from SomeApp.models import SomeModel

User = get_user_model()


class UserSErializer(serializers.ModelSerializer):
    somemodels = serializers.PrimaryKeyRelatedField(
        queryset=SomeModel.objects.all(),
        many=True
    )

    class Meta:
        model = User
        fields = ('id', 'username', 'somemodel',)

그리고 유저에 대한 generic View를 만들어 준다.

from django.contrib.auth import get_user_model
from rest_framework import generics

from .serializers import UserSerializer

User = get_user_model()


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

뷰에 대한 ursl를 연결 시켜 주자 urls.py를 만들고 연결

from django.urls import path

from .apis import UserList, UserDetail

urlpatterns = [
    path('', UserList.as_view(), name='user-list'),
    path('<int:pk>/', UserDetail.as_view(), name='user-detail'),
]

지금까지 somemodel을 만들어도 해당 모델을 만든 사용자와 아무 관계도 맺지 않았다. 사용자는 직렬화된 표현에 나타나지 않았고, 요청하는 측에서 지정하는 속성 이었을 뿐이다. 지금 만약 somemodel을 API를 통해서 만들려고 하면 NOT NULL constraint failed 에러가 날 것이다. owner_idNULL이 아니기 떄문이고, SomeModelSerializerfieldsowner에 대한 정보가 없기 때문이다. is_valid()에서는 통과 했지만 save()호출시 onwer에 대한 정보가 없기 때문에 에러가 발생했다. 이를 해결 하기 위해 perform__create()를 써야 한다. 해당 함수는 is_valid()가 통과되고 나서 실행되는 함수로 이 함수를 SomeModelList에서 오버라이드 하여 추가 작업을 하고 저장하는 루틴으로 만들자.

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

owner에 대한 정보를 넘어온 요청에 대한 사용자로 처리 하도록 한다. 하지만 만약 인증되어 있지 않은 유저로 해당 API를 실행할 경우 AnonymousUser라는 확인되지 않은 유저가 왔다고 에러가 난다. 일단 SomeModelSerializerowner에 대한 시리얼라이저를 읽기전용속성으로 만들어서 읽을수 있게하자.

class SomeModelSerializer(...
    owner = serializers.ReadOnlyField(source='owner.username')
    
    class Meta:
        ...
        fields = (
            ...
            'owner',
         )

그리고 장고 어드민에서 SomeModel을 하나만들고 슈퍼유저를 해당 SomeModel과 연결 시켜 보자 그리고 GET API를 하면 유저가 같이 나오는 것을 볼 수 있다.

그리고 이제 인증 받은 사용자만 SomeModel을 생성/업데이트/삭제 해 보자, 특정 뷰에 대한 제한을 걸 수 있는 권한 클래스 중 하나인 IsAuthenticatedOrReadOnly를 넣자. 이것은 인증 받은 요청에 읽기와 쓰기 권한을 부여하고 인증 받지 않은 요청에 대해서는 읽기 권한만 부여 한다 SomeModel List에 다음 내용을 추가 하자

from rest_framework import permissions

permission_classes = (
        permissions.IsAuthenticatedOrReadOnly,
    )

django-rest-framework Authentication

기본적으로 두가지 인증 방식을 제공한다 BasicAUthenticationSessionAuthentication이다.

# settings.py
REST_FRAMEWORF = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAUthentication',
        'rest_framework.authentication.SessionAUthentication',
     )
}

장고 내에 자동적으로 적용되는 것들로 브라우저에서 유저 로그인을 한후 SomeModel을 하나 만들면 해당 유저에 대한 세션값으로 인해 SomeModel이 만들어 진다. 하지만 이것은 테스트용으로 사용하고 RestFul API 개발에는 쓰지 못한다. 세션과 쿠키는 브라우저에만 사용하는데 아이폰/안드로이드/프론트엔드에는 애매한 상황이 되기 때문이다. 그래서 획일화를 시켜서 인증 하는 방식인 TokenAuthentication을 사용한다.

BasicAUthentication은 Http에 기본 인증 스킴으로 base64를 이용하여 인코딩된 사용자 ID/비밀번호 쌍의 인증 정보를 전달한다.

Authorization: Basic <base64로 인코딩된 username:password>

TokenAuthenticataion은 요청이 왔을때 특정 토큰 테이블을 사용하여 토큰을 요청하는 새로운 뷰를 만들어 사용하는 방식이다. 클라이언트가 HTTP Authorization Header에 Token을 담아서 보내면 해당 토큰을 분석해서 request.user를 할당.

TokenAuthentication

  • 특정 유저가 토큰을 요청
  • 서버는 해당 유저의 인증에 성공하면 토큰을 생성, 반환
  • 유저는 받은 토큰을 자신의 저장소에 보관하고 매 요청마다 해당 토큰을 Header에 담아서보냄
  • 서버는 받은 요청에 토큰과 관련된 Header가 있는지 검사후 request.user 할당

토큰을 쓰는 이유는 토큰에다가 특정 권한을 담을수 있다. 토큰에 일부 권한만 담아 발급하여 유저가 쓸수 있는 권한을 제약시킬 수도 있고, 만약 유저의 토큰들이 유출 되었을 경우 해당 토큰만 폐기하고 유저가 다시 접속 할 경우 다시 로그인만 하면 되기 때문에 보안상 좋다. 그리고 토큰은 사용자들이 억지로 임의의 기간마다 접속을 하게 할 수도 있다. 토큰에 인증 기간을 넣어 해당 기간안에 토큰을 탈취 당해도 기간이 지나면 토큰이 삭제되 무용지물이 된다.

이것은 기본값이 아니기 때문에 따로 설정 해주어야 한다.

커스텀 권한 만들기

객체의 소유자에게만 쓰기를 허용하고 다른 사용자는 읽을수 있기만 하는 권한을 만들어 보자 SomeApp 안에 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

StaitcHTMLRenderer

뷰에서 처리를 한후 응답을 할때 HTML 형식으로 리턴해주는 렌더러 해당 렌더러에 데이터를 넣으면 그 데이터로 이루어진 HTML을 불러준다. 파이썬 객체나 JSON이 아닌 HTML 문자열 자체를 반환해야 한다.

페이징 기능 추가하기 pagination

가장 쉬운 방법은 settings.py에 해당 코드를 넣는것이다

REST_FRAMEWORK = {
    'PAGE_SIZE': 10,
}

하지만 이것은 모든 API요청에 대해 적용되는 것으로 추천하지 않는 방법이고, 추천되는 방법은 각 뷰마다 다른 페이징 기능을 적용 하는 것이다. pagination.py를 만들고

# pagination.py
from rest_framework.pagination import PageNumberPagination


class StandardResultsSetPagination(PageNumberPagination):
    page_size = 3
    page_size_query_param = 'page_size'
    max_page_size = 5

한후 뷰에 적용

class SomeModelList(....
    ...
    pagination_class = StandardResultsSetPagination

이렇게 하면 요청시 3개씩 묶어서 오게 된다. 하지만 유저가 page_size 파라미터를 통해 max_page_size이상을 불러 올 경우 max_page_size로 고정되어 반환한다.

Comments