[DRF] Authentication과 Permission

Django REST framework의 Authentication과 Permission에 대해 공부한다.

Posted by Seoyoung Lee on August 18, 2020 · 10 mins read

API요청을 할 때, 아무나 다른 사람의 글을 수정/삭제할 수 있어서는 안되기 때문에 DRF에서는 접근제한을 지원한다. 이를 코드로 살펴보기 위해 Post 모델에 작성자 필드를 추가한다.

# models.py

from django.db import models
from django.conf import settings

class Post(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


Authentication (사용자 인증)


지원하는 인증의 종류 (rest_framework/authentication.py)

1. SessionAuthentication : 세션을 통한 인증 여부 체크
2. BasicAuthentication : Basic 인증헤더를 통한 인증 수행
3. TokenAuthentication : Token 헤더를 통한 인증 수행
4. RemoteUserAuthentication : User 정보가 다른 서비스에서 관리될 때, Remote 인증 / Remote-User 헤더를 통한 인증 수행

포스팅을 저장할 때 현재 인증된 유저 정보를 기록하되, 사용자가 직접 입력하는 방식이 아닌 자동으로 DB에 저장되도록 구현한다. 이를 위해 PostSerializer의 Meta.fields에서 author 필드를 제외시킨다.

# serializers.py

from rest_framework.serializers import ModelSerializer
from .models import Post

class PostSerializer(ModelSerializer):
    class Meta:
        model = Post
        fields = ['pk', 'title', 'content']

그리고 API를 통해 Post 저장 시에 현재 인증된 유저를 지정하도록 perform_create 함수를 오버라이딩한다. serializer.save(**kwargs) 함수는 kwargs 항목을 통해, 추가로 저장할 필드를 지정할 수 있다.

# views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

이 상태에서 새로운 포스팅 API를 요청하면 500 에러 응답을 받는다. 요청 시에 인증정보를 제공하지 않았기 때문에 self.request.user에 AnonymousUser 인스턴스가 할당되어 모델 저장에 실패한 것이다.

이를 해결하기 위해 perform_create 함수를 호출하기 전에 인증여부를 체크하는 것이 필요하겠지만 이는 뒤에서 살펴보고 지금은 API 요청 시에 HTTP Basic 인증헤더를 제공하여 성공적으로 API 요청을 처리해본다.


웹브라우저를 통한 로그인/로그아웃 지원

DRF에서는 웹 브라우저에서의 로그인/로그아웃도 지원한다. 아래 코드와 같이 urls.py 를 수정해주면 웹의 오른쪽 상단에 로그인/로그아웃 기능이 추가된다.

# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
		# 생략
    path('api_auth/', include('rest_framework.urls', namespace='rest_framework')),
]


Permission (권한 시스템)


Django에서는 기본적으로 is_superuser(별로 permission 없이 모든 권한 허용), is_staff(admin 페이지 접속가능), is_active(False일 경우 로그인 포함 모든 권한 불허)를 제공한다.

또한 DRF에서는 AllowAny(default), IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly 등 여러가지 permission을 제공한다.

권한 지정하기

기본적으로 DRF에서 제공하는 권한을 적용해본다. APIView에서는 permission_classes를 통해 권한을 지정할 수 있다. 우리가 사용하는 ViewSet 역시 APIView를 상속받았으므로 동일하게 권한을 지정한다.

# views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    permission_classes = [
        # 디폴트는 AllowAny
        IsAuthenticated,
    ]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

IsAuthenticated 는 인증된 요청에 한해서만 뷰호출을 허용한다. 로그인을 하지 않은 유저는 권한이 없으므로 403 에러가 뜨며, 권한이 없다는 메세지가 출력된다.

커스텀 Permission 만들기

DRF에서 기본 제공해주는 permission만으로 보통 충분하지만, 필요에 의해 커스텀 permission이 필요할 때가 있다. 모든 permission 클래스는 다음 2가지 함수를 선택적으로 구현한다.

- has_permission(request, view) : '뷰 호출 접근권한'으로 APIView 접근 시, 체크
- has_object_permission(request, view, obj) : '개별 record에 대한 접근권한'으로 APIView의 get_object 함수를 통해 object 획득 시, 체크

먼저 기본적으로 구현되어 있는 permission 클래스 코드(rest_framework/permissions.py) 몇가지를 살펴보면,

# rest_framework/permissions.py

# 안전한 메소드 종류. 이 METHOD만으로는 단순조회만 될 뿐, 데이터 파괴(추가/수정/삭제) 불가
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')

# 인증여부에 상관없이, 뷰 호출 허용
class AllowAny(BasePermission):
    def has_permission(self, request, view):
        return True

# 인증된 요청에 한해서, 뷰 호출 허용
class IsAuthenticated(BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.is_authenticated

# Staff 인증 요청에 한해서, 뷰 호출 허용
class IsAdminUser(BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.is_staff

# 비인증 요청에게는, 읽기 권한만 허용
class IsAuthenticatedOrReadOnly(BasePermission):
    def has_permission(self, request, view):
        # 안전한 METHOD요청이면, 인증여부에 상관없이, 뷰 호출 허용
        if request.method in SAFE_METHODS:
            return True
        # 안전하지 않은 METHOD일 경우, 인증유저에게만, 뷰 호출 허용
        elif request.user and is_authenticated(request.user):
            return True
        # 안전하지 않은 METHOD일 경우, 비인증유저에게는, 뷰 호출 제한
        return False

커스텀 permission을 만들기 위해서는 permission.py 파일을 새로 만들어 위 클래스의 함수를 재정의하면 된다.

포스팅 작성자에 한해서, 수정/삭제 권한을 부여
# permissions.py

from rest_framework import permissions

class IsAuthorOrReadonly(permissions.BasePermission):
    # 인증된 유저에 대해 목록 조회/포스팅 등록 허용
    def has_permission(self, request, view):
        return request.user.is_authenticated

    # 작성자에 한해 Record에 대한 수정/삭제 허용
    def has object_permission(slef, request. views, obj):
        # 조회 요청은 항상 True
        if request.method in permissions.SAFE_METHODS:
            return True
        # PUT, DELETE 요청에 대해 작성자에게만 허용
        return obj.author == request.user

포스팅 작성자에게만 수정 권한을 부여, 삭제 권한은 superuser에게만 부여
# permissions.py

from rest_framework import permissions

class IsAuthorUpdateOrReadOnly(permissions.BasePermission):
    # 인증된 유저에 대해 목록 조회/포스팅 등록 허용
    def has_permission(slef, request, view):
        return request.user.is_authenticated
    
    # superuser에게는 삭제 권한만 부여하고, 작성자에게는 수정 권한만 부여
    def has_object_permission(self, request, view, obj):
        # 조회 요청은 항상 True
        if request.method in permissions.SAFE_METHODS:
            return True
        # 삭제 요청의 경우, superuser에게만 허용
        if (request.method == 'DELETE'):
            return request.user.is_superuser
        # PUT 요청에 대해, 작성자일 경우에만 요청 허용
        return obj.author == request.user

이렇게 permission 함수를 재정의했다면 views.py의 permission_classes에 permission 클래스 이름을 추가해야 적용이 된다.


POST 조회 응답에 작성자 추가

보통 포스팅 조회 응답에는 그 포스팅의 작성자 정보도 같이 제공한다. 이는 PostSerializer를 수정함으로써 구현할 수 있는데, 만약 PostSerializer.Meta.fields에 'author'를 지정한다면 사용자가 항상 입력해야한다는 문제가 있다. author 필드는 서버에서 인증에 의해서만 지정이 되어야하기 때문에 serializers.ReadOnlyField(source='참조할필드명.속성명')을 쓴다.

# serializers.py

from rest_framework.serializers import ModelSerialzer, ReadOnlyField
from .models import Post

class PostSerializer(ModelSerializer):
    author_username = ReadOnlyField(source='author.username')
    
    class Meta:
      model = Post
      fields = ['author_username', 'title']

위와 같이 serializer를 구현하면 입력을 받을 때에는 'title'필드만 받고 reponse에 대해서는 'author_username'도 표시를 해준다.