수학과의 좌충우돌 프로그래밍

[Django] Authentication 과 Permissions 본문

웹프로그래밍/DRF

[Django] Authentication 과 Permissions

ssung.k 2020. 1. 8. 16:45

DRF 에서의 접근제한을 알아보도록 하겠습니다.

우선 기본적으로 접근제한을 제외한 기본적인 코드들에 대해서는 설명을 생략하겠습니다.

앞의 포스팅들을 참고해주세요.

현재 까지의 진행 상황은 다음과 같습니다.

 

Post 모델에 author 를 추가하여 글 작성자를 연결하였습니다.

# 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)

 

# serializers.py

from rest_framework.serializers import ModelSerializer
from .models import Post

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

 

# urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'post', views.PostViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

 

또한 임시로 admin page 에서 user 를 만들어주었습니다.

 

Authentication

인증의 종류

지원하는 인증의 종류는 총 4가지가 있습니다.

  • SessionAuthentication

    • 세션을 통한 인증 여부 체크
    • APIView를 통해 디폴트 지정 (우선순위 1)
  • BasicAuthentication

    • Basic 인증헤더를 통한 인증 수행
    • ex) Authorization: Basic YWxsaWV1czE6MTAyOXNoYWtl
    • APIView를 통해 디폴트 지정 (우선순위 2)
  • TokenAuthentication

    • Token 헤더를 통한 인증 수행
    • ex) Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
  • RemoteUserAuthentication

    • User 정보가 다른 서비스에서 관리될 때, Remote 인증 (장고 공식문서)
    • Remote-User 헤더를 통한 인증 수행

 

 

HTTPie 를 통한 TEST

API 요청 시에 인증정보를 제공하지 않았기 때문에 self.request.user에 AnonymouseUser 인스턴스가 할당되어 모델저장에 실패하게 됩니다.

http --form POST localhost:8000/post/ title="제목"

# 500 Internal Server Error

 

이럴 경우 HTTP Basic 인증헤더를 통해 유저 정보를 넘겨줘야 합니다.

위 username 과 password 에는 각각 맞는 정보를 입력하면 됩니다.

http --auth username:password --form POST localhost:8000/post/ title="제목"

# 201 Created

 

이 요청에서 username:password 문자열은 base64로 인코딩되어 Authorization 헤더로 전달되고, 이 헤더를 BasicAuthorization에서 인지하여 인증을 처리합니다.

httpbin.org를 통해 헤더를 확인할 수 있습니다.

12번째 줄에 인코딩된 값이 있습니다.

http -a username:password --form POST httpbin.org/post

"""
{
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate",
        "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
        "Connection": "close",
        "Content-Length": "0",
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        "Host": "httpbin.org",
        "User-Agent": "HTTPie/0.9.9"
    },
    "json": null,
    "origin": "221.148.61.230",
    "url": "http://httpbin.org/post"
}
"""

 

웹 브라우저에서의 로그인

DRF 는 웹 브라우저에서의 로그인도 지원합니다.

8번째 줄과 같이 추가해주시면 아래 이미지에서 오른쪽 상단에 로그인, 로그아웃 기능이 추가됩니다.

# 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 에서의 권한

django 는 기본적인 권한들을 제공해주고 있습니다.

  • is_superuser

    • createsuper 로 생성한 user 에 대해 True
  • True일 경우 별도 permission 없이 모든 권한 허용

  • is_staff

    • True 일 경우 admin 페이지 접속가능
    • 나머지는 일반 유저와 동일
  • is_active

    • False 일 경우 모든 권한 불허
    • 로그인도 불가능

 

DRF 에서 기본제공하는 Permission 은 다음과 같습니다.

  • AllowAny : 인증여부에 상관없이 뷰 호출 허용 (default)

  • IsAuthenticated : 인증된 요청에 한해서 뷰호출 허용

  • IsAdminUser : Staff 인증 요청에 한해서 뷰호출 허용

  • IsAuthenticatedOrReadOnly : 비인증 요청에게는 읽기 권한만 허용

  • DjangoModelPermissions : 인증된 요청에 한해서만 뷰 호출 허용, 추가로 유저별 인증 권한체크를 수행

  • DjangoModelPermissionsOrAnonReadOnly : DjangoModelPermissions 와 유사하나 비인증 요청에 대해서는 읽기 권한만 허용

  • DjangoObjectPermissions

    • 비인증된 요청 거부
    • 인증된 레코드 접근에 대한 권한체크를 추가로 수행

 

 

권한 지정하기

기본적으로 제공되는 권한을 적용해보도록 하겠습니다.

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 = [
        IsAuthenticated,
    ]

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

 

IsAuthenticated 의 경우, 위에서 봤듯이 인증된 요청에 한해서만 뷰호출을 허용합니다.

로그인을 안한 상태에서는 권한이 없으므로 아래와 같이 권한이 없다고 나옵니다.

 

커스텀 Permission

DRF 에서 제공해주긴 하지만 이들만으로는 부족합니다.

따라서 커스텀하는 방법에 대해서 알아보도록 하겠습니다.

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

  • has_permission(request, view)

    • 뷰 호출 접근 권한
    • APIView 접근 시 체크
  • has_object_permission(request, view, obj)

    • 개별 레코드 접근 권한
    • APIView 의 get_object 함수를 통해 object 획득 시 체크
    • 브라우저를 통한 API 접근시에 CREATE/UPDATE Form 노출 여부 확인 시에

 

permission 들의 코드를 살펴보고 어떤 식으로 커스텀 해야할 지 방향을 잡아보겠습니다.

https://github.com/encode/django-rest-framework/blob/master/rest_framework/permissions.py

 

encode/django-rest-framework

Web APIs for Django. 🎸. Contribute to encode/django-rest-framework development by creating an account on GitHub.

github.com

우선 상당부에 안전한 method 들을 따로 정의해두었습니다.

이들은 수정 삭제 삽입을 하지 않아서 안전하다고 부릅니다.

SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')

 

간단히 몇 개의 permission 에 대해서 살펴보겠습니다.

AllowAny 는 모든 요청에 대해 허가합니다.

class AllowAny(BasePermission):
    def has_permission(self, request, view):
        return True

IsAuthenticated 는 유저가 존재하고 로그인 되어 있을 경우에 허가합니다.

class IsAuthenticated(BasePermission):
    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated)

IsAdminUser 는 유저가 존재하고 스태프일 경우에 허가합니다.

class IsAdminUser(BasePermission):
    def has_permission(self, request, view):
        return bool(request.user and request.user.is_staff)

IsAuthenticatedOrReadOnly 는 안전한 request method 이거나 유저가 존재하고 로그인 되어 있을 경우에 허가합니다.

class IsAuthenticatedOrReadOnly(BasePermission):
    def has_permission(self, request, view):
        return bool(
            request.method in SAFE_METHODS or
            request.user and
            request.user.is_authenticated
        )

 

이하의 다른 permission 에 대해서는 위의 링크를 통해 참고하시면 되겠습니다.

 

커스텀 permission 을 만들기 위해서 permissions.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
    
    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
        
        return obj.author == request.user

 

두 예시 중 IsAuthorUpdateOrReadOnly 을 직접 적용시켜보면 views 12번째 줄에 추가해주었습니다.

# views.py
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny, IsAuthenticated
from .models import Post
from .serializers import PostSerializer
from .permissions import IsAuthorUpdateOrReadOnly

class PostViewSet(ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [
        IsAuthorUpdateOrReadOnly,
    ]

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

 

이를 HTTPie 와 브라우저를 통해 확인해보도록 하죠.

user1 이 사용한 2번 글에 대한 요청들입니다.

  • root 로 접속하는 경우
http --auth root:root --form PUT http://localhost:8000/post/2/ title="제목 수정"
      
"""
403 Forbidden
{
    "detail": "You do not have permission to perform this action."
}
"""

 

  • user1 으로 접속하는 경우
http --auth user1:mskang0710! --form PUT http://localhost:8000/post/2/ title="제목 수정"
      
"""
200 OK
{
    "title": "user1 제목 수정"
}
"""

 

 

POST 조회 응답에 작성자 추가

누가 어떤 글을 썼는지 알기 위해서는 조회 요청이 들어왔을 때 작성자에 대한 정보도 제공해주어야 합니다.

 

아래와 같이 fields 에 추가하게 되면 request 를 받을 때도 author 를 받아야 하는 문제가 발생합니다. 글을 작성하는 경우 작성자에 대해서는 직접 작성하는 경우는 많이 없으니 이는 잘못된 방법이죠.

# serializers.py

from rest_framework.serializers import ModelSerializer
from .models import Post

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

 

따라서 서버에서 자동으로 넣어주어야 합니다.

이를 위해 ReadOnlyField 를 사용하였습니다.

# 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']

 

입력받을 때는 title만 받게 되고, response 대해서는 author_username 도 표시를 해주게 됩니다.

0 Comments
댓글쓰기 폼