Authentication과 Permission

guava·2022년 1월 15일
0

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 강의를 듣고 정리한 글입니다.

DRF에서는 인증 및 권한 관리를 지원한다.
세션인증, 토큰인증 등을 지원하며 원하는 인증만 사용할 수 있다.

또한 APIView에 permission_classes를 지정해주어서 권한을 관리할 수 있다. (전역 설정도 가능하다.)
뿐만 아니라 permission_classes에 지정할 클래스를 직접 정의할 수도 있다.

이번 포스팅에서는 DRF에서 인증 및 권한을 어떻게 관리하고 커스텀 하는지 알아본다.

1. 인증 (Authentication)


1.1. 인증


유입되는 요청을 허용 / 거부하는 것을 결정하는 것이 아니라, 단순히 인증정보로 유저를 식별하는 것입니다.

  • Authentication : 유저 식별
  • Permissions : 각 요청에 대한 허용/거부
  • Throttling : 일정 시간 동안에 허용할 최대 요청 횟수

유저 식별 → 해당 유저가 어떤 리소스에 대한 어떤 액션을 취할 때 허용/ 거부할 것인지 결정 → 허용이 된다면, 특정 리소스에 대한 요청 횟수를 넘어서는가 확인해서 허용

1.2. 인증 처리 순서


  1. 매 요청 시마다 APIView의 dispatch(request) 호출

  2. APIView의 initial(request) 호출

  3. APIView의 perform_authentication(request) 호출

    (참고) 세션 인증은 한번의 로그인으로 끝나지만, API에서는 매번 클라이언트가 서버에 요청시마다 인증을 한다. 즉 매번 인증을 해서 이 요청이 유효한지 검사한다. 인증 방법은, 매 인증 시 json토큰을 보내거나, username/password를 올리는 등 여러가지 방법이 있다.

  4. request의 user속성 호출 (rest_framework.request.Request 타입)

  5. request의 _authenticate() 호출 - 실제 인증 로직

1.3. 지원하는 인증 (Authentication)의 종류


SessionAuthentication

  • 세션을 통한 인증, APIView에서 디폴트 지정

BasicAuthentication

  • Base 인증 헤더를 통한 인증 (예 → Authorization: Basic YWxsaWV1czE6MTAyOXNoYWtl )
    • HTTPie를 통한 요청 : 쉘> http --auth 유저명:암호 --form POST :8000 필드명1:값1 필드명2:값2
  • http요청일 경우 누구나 스니핑 해서 볼 수 있음.

TokenAuthentication

  • Token 헤더를 통한 인증 (예 → Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a)
  • 마찬가지로 http요청일 경우 누구나 스니핑 해서 볼 수 있음.

RemoteUserAuthentication

  • User가 다른 서비스에서 관리될 때, Remote 인증
  • Remote-User 헤더를 통한 인증 수행

1.4. 웹브라우저를 통한 API 접근에서 로그인/로그아웃 지원


장고 기본 앱 django.contrib.auth를 통한 지원 → 세션 인증

rest_framework/urls.py

from django.conf.urls import url
from django.contrib.auth import views

app_name = 'rest_framework'
urlpatterns = [
    url(r'^login/$', views.LoginView.as_view(template_name='rest_framework/login.html'),
         name='login'),
    url(r'^logout/$', views.LogoutView.as_view(), name='logout'),
]

프로젝트 내, urls.py

urlpatterns += [
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

2. 허가와 권한 (Authorizations and Permissions)


2.1. 인증과 허가


개체(정보/코드 등)에 대한 접근을 허용하기 위해서, 인증/식별만으로는 충분하지 않습니다. 추가로 각 개체에 대한 허가가 필요합니다.

2.2. DRF의 Permission 시스템


현재 요청에 대한 허용/거부를 결정합니다. APIView 단위로 지정할 수 있습니다.

  • AllowAny (디폴트 전역 설정) : 인증 여부에 상관없이, 뷰 호출 허용
  • IsAuthenticated : 인증된 요청에 한해서, 뷰 호출 허용
  • IsAdminUser : Staff 인증 요청에 한해서, 뷰 호출 허용
  • IsAuthenticatedOrReadOnly : 비인증 요청에게는 읽기 권한만 허용 → 최소 전역 디폴트 설정 권고
  • DjangoModelPermissions : 인증된 요청에 한해 뷰 호출을 허용하고, 추가로 장고의 모델단위 Permissions 체크
  • DjangoModelPermissionsOrAnonReadOnly : DjangoModelPermissions과 유사하나, 비인증 요청에게는 읽기만 허용
  • DjangoObjectPermissions : 비인증 요청은 거부하고, 인증된 요청은 Object에 대한 권한 체크를 수행

2.2.1 permission_classes 지정

아래와 같이, IsAuthenticated지정 시, 인증된 상황임을 보장받을 수 있습니다.

APIView에는 permission_classes 설정

from rest_framework.permissions import IsAuthenticated

class ExampleView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        content = { 'status': 'request was permitted' }
        return Response(content)

@api_view에는 @permission_classes 장식자 설정

from rest_framework.decorators import permission_classes

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def example_view(request, format=None):
    content = { 'status': 'request was permitted' }
    return Response(content)

2.2.2 디폴트 전역 설정

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

2.3. 커스텀 Permission


모든 Permission 클래스는 다음 2가지 함수를 선택적으로 구현

  • has_permission(request, view)
    • APIView 접근 시, 체크
    • 거의 모든 Permission 클래스에서 구현하며, 로직에 따라 True/False 반환
  • has_object_permission(request, view, obj)
    • APIView의 get_object 함수를 통해 object 획득 시에 체크
    • 브라우저를 통한 API 접근에서 CREATE/UPDATE Form 노출 시에 체크
    • DjangoObjectPermissions에서 구현하며, 로직에 따라 True/False 반환

dry-rest-permissions : 룰 기반 퍼미션 지원

2.4. DRF의 Permissions 코드 살펴보기


# https://github.com/encode/django-rest-framework/blob/3.10.1/rest_framework/permissions.py
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  # 유저가 있고, 로그인 된 유저만 허용

class IsAdminUser(BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.is_staff  # 스태프 여부 판단
# https://github.com/encode/django-rest-framework/blob/3.10.1/rest_framework/permissions.py

class DjangoObjectPermissions(DjangoModelPermissions):  # 장고 기본의 퍼미션을 체크한다.
    # ...
    def has_object_permission(self, request, view, obj):
        # authentication checks have already executed via has_permission
        queryset = self._queryset(view)
        model_cls = queryset.model
        user = request.user

        perms = self.get_required_object_permissions(request.method, model_cls)

        if not user.has_perms(perms, obj):
            if request.method in SAFE_METHODS:
                raise Http404

            read_perms = self.get_required_object_permissions('GET', model_cls)
            if not user.has_perms(read_perms, obj):
                raise Http404
            return False

        return True

2.5. 퍼미션 만들어 보기


포스팅 작성자가 아니라면, 읽기 권한만 부여하기.

모델에 author 필드가 있다고 가정

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(self, request, view, obj):
        # 조회 요청(GET, HEAD, OPTIONS) 에 대해서는 인증여부에 상관없이 허용
        if request.method in permissions.SAFE_METHODS:  # SAFE_METHODS = ('GET', HEAD', 'OPTIONS')
            return True

        # PUT, DELETE 요청에 대해, 작성자일 경우에만 요청 허용
        return obj.author == request.user

권한 확인하기

Django Shell에서 간단한 비밀번호를 가진 유저 생성하기

# python shell
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> User.objects.create_user?
Signature:
User.objects.create_user(
    username,
    email=None,
    password=None,
    **extra_fields,
)
Docstring: <no docstring>
...
>>> User.objects.create_user('user2', password='1234')
<User: user2>

인증된 유저로 요청하기

http --auth user2:1234 http://localhost:8000/post/1/   # 
http --auth user2:1234 POST http://localhost:8000/post/ message="user2가 지정하는 첫 메시지"
http --auth user2:1234 PATCH http://localhost:8000/post/6 message="user2가 수정하는 첫 메시지"
http --auth user2:1234 PATCH http://localhost:8000/post/1 message="수정?"  # author가 다르면, 거부됨
http --auth user2:1234 gethttp://localhost:8000/post/1  # author가 달라도 조회는 가능!

포스팅 작성자에게 수정권한만 부여하고, 삭제는 superuser에게만 가능케 하는 Permission 클래스 정의

from rest_framework import permissions

class IsAuthorUpdateOrReadonly(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True

        if (request.method == 'DELETE'):
            return request.user.is_superuser  # 또는 request.user.is_staff

        return obj.author == request.user

0개의 댓글