📌 이 포스팅에서는 DRF에서 인증과 인가를 적용시키는 방법에 대해 정리하였습니다.
🔥 Authentication과 Permission 이란?
🔥 DRF의 Permission 시스템
🔥 Custom Permission 적용하기
✔️ 유입되는 요청을 허용 또는 거부하는 것이 아닌, 단순히 사용자를 식별하는 것을 인증(Authentication) 이라 한다.
✔️ 또한 인증된 사용자가 어떤한 요청을 허용 또는 거부하는 것은 허가(Permission)이다.
✔️ 예를 들어, 가입된 회원만 확인할 수 있는 페이지(ex. 프로필 페이지)는 로그인이 필요하고, 이를 인증이 필요하다 한다.
✔️ 하지만, 인증이 되었다고해서 다른 사람의 프로필을 삭제/수정할 수 잇는 것은 아닌데, 이 때 필요한 것이 허가이다.
✔️ 즉, 서비스의 사용자인지 식별하는 것 자체는 인증이고, 리소스에 대한 접근, 수정, 삭제 권한 등에 있어서는 허가가 필요하다.
✔️ Throttling은 일정 기간 동안 허용할 최대 요청 횟수를 제한하는 것을 의미한다.
✔️ 즉, 매분, 매일, 매월 등 어떠한 주기로 사용자가 요청할 수 있는지 제한시키는 것을 Throttling라 한다.
- 매 요청 마다, APIView의 dispatch(request) 호출
- APIView의 initial(request) 호출
- APIView의 perform_authentication(request) 호출
- request의 user 속성 호출(rest_framework.request.Request 타입)
- request의 _authenticate() 호출
✔️ 단, Django Session을 사용할 때는 로그인할 때만 인증을 거치고, 세션 정보를 저장 후 이를 지속적으로 활용하지만, API로써 사용할 때는 클라이언트에서 서버로 요청을 할 때마다 매번 인증이 이뤄진다.
✔️ DRF의 Permission 시스템은 현재 요청에 대한 허용/거부를 결정하고, 이를 전역 또는 APIView 단위별로 지정할 수 있다.
✔️ DRF permission 종류
- AllowAny : 디폴트로 지정된 전역 설정으로 인증 여부에 상관없이, View를 호출할 수 있다.
- IsAuthenticated : 회원으로 인증된 요청에 한해서, View 호출
- IsAdmin : staff 인증 요청에 한해서, View 호출
- IsAuthenticatedOrReadOnly : 비인증된 요청에 대해서는 읽기 권한만 허용
✔️ CBV의 경우애는 permission class에 list형식으로 permission 종류를 추가한다.
from rest_framework.permissions import IsAuthenticated # 👈 permissions에서 import class ExampleView(APIView): permission_classes = [IsAuthenticated] def get(self, request, format=None): content = {'status': 'request was permitted.'} return Response(content)
✔️ FBV이 경우에는 permission 데코레이션을 사용합니다.
from rest_framework.decorators import permission_classes # 👈 decorators 에서 import @api_view(['GET']) @permission_classes([IsAuthenticated]) # 👈 list 형식으로 전달 def example_view(request, format=None): content = {'status': 'request was permitted.'} return Response(content)
✔️ DRF를 이용하여, 전역으로 Permission을 적용시키려면 settings.py에서 이를 선언할 수 있다.
✔️ 아래와 같이 직접 지정해주지 않는다면, Default로 "AllowAny"가 지정되어있다.
# settings.py REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ # 'rest_framework.permissions.AllowAny', # 👈 지정해주지않으면, Default로 AllowAny가 적용되있다. 'rest_framework.permissions.IsAuthenticated', ], }
✔️ 따라서 API별로 View의 역할에 맞게 지정하더라도, 전역 설정으로 Default는 IsAuthenticated로 해주는 편이 안전한다.
✔️ "IsAuthenticated"로 전역 지정했기 댸문에 View에 별다른 조치를 안해줘도, 인증을 수행한 요청만 응답이 반환된다.
✔️ 앱 내에 permissions.py를 생성하여 custom permission을 직접 적용시킬 수 있다.
✔️ 모든 Permission classes는 has_permission와 has_object_permission를 선택적으로 지정하여 custom할 수 있다.
✔️ has_permission는 APIView에 접근 시 인증을 체크하고, has_object_permission는 APIView의 get_object 함수를 통해 object에 접근 시 인증을 체크해준다.
✔️ 객체에 접근할 때, SAFE_METHODS의 종류 중 하나로 요청되었다면, True를 반환해서 응답을 주지만, PATCH나 DELETE로 요청될 때는 현재 사용자가 참조 중인 해당 겍체의 사용자 일 때만 True를 반환시킨다.
✔️ 여기서 SAFE_METHODS란 OPTION, HEAD, GET 요청을 의미하고, permissions에 이미 구현되있다.
# permissions.py from rest_framework import permissions # 👈 psermissions import class IsAuthorOrReadonly(permissions.BasePermission): # 👈 BasePermission 상속 # has_permission def has_permission(self, request, view): # 👈 인증된 사용자에 한하여 목록조회/포스트 등록 가능 return request.user and request.user.is_authenticated # has_object_permission def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: # 👈 GET, OPTION, HEAD 요청일 때는 그냥 허용 return True return obj.author == request.user # 👈 DELETE, PATCH 일 때는 현재 사용자와 객체가 참조 중인 사용자가 일치할 때마다 허용
✔️ 기본 인증 CBV에서 permission_classes를 통해 각각의 view에 대한 permission을 아래와 같이 적용시킨다.
from rest_framework.permissions import IsAuthenticated # 👈 기본 인증 import from rest_framework.viewsets import ModelViewSet from rest_framework.decorators import api_view, action from rest_framework.response import Response from .permissions import IsAuthorOrReadonly # 👈 custom permission import from .serializers import PostSerializer from .models import Post class PostViewSet(ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [IsAuthenticated, IsAuthorOrReadonly] # 👈 permission 적용 def perform_create(self, serializer): author = self.request.user ip = self.request.META['REMOTE_ADDR'] serializer.save(author=author, ip=ip) @action(detail=False, methods=['GET']) def public(self, request): qs = self.get_queryset().filter(is_public=True) serializer = self.get_serializer(qs, many=True) return Response(serializer.data) @action(detail=True, methods=['PATCH']) def set_public(self, request, pk): instance = self.get_object() instance.is_public = True instance.save(update_fields=['is_public']) serializer = self.get_serializer(instance) return Response(serializer.data)