장고 튜툐리얼#4 포스팅이다. 인증 및 권한을 다루는데 게시물을 작성할때 유저체크를 하고, 유저가 아니라면 ReadOnly만 할 수 있게 개선해 나가는 튜툐리얼이다. 이것저것 코드가 조금 늘어났고, 제너릭뷰를 사용하기 때문에 함축 된 것이 많아 직관적으로 한번에 이해하기는 다소 어려움이 있다. 그렇지만 일단 따라해보자. 이해가 안되더라도 일단 해보자!
# Snippet 모델에 owner 추가
class Snippet(models.Model) :
# ...
# ...
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
먼저 owner를 추가한 이유에 대해서 생각해보자. 하나의 Snippet에는 누가 작성했는지에 대한 정보가 기록되어야 한다. 한마디로 작성자에 대한 정보가 필요한데 이것을 저장해놓기 위해 owner를 추가시킨다. owner는 외래키로써 장고자체에 내짱된 auth.User를 이용한다. Snippet안에 여러 정보가 들어있고, 소유자에 대한 id가 외래키로 들어오고, 그에 대한 상세정보를 찾기 위해서는 auth.User에서 찾아볼 수 있게 하는 시스템이라고 생각하면 될 것 같았다.
highlighted를 추가한 이유는 주요 기능상에 이유이기 보다는, 강조표시가 되어야하는 코드에 대해서 저장하기 위한것을 추정했다.
## pygments 관련
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
강조되는 코드에 대해서 하이라이팅을 적용하기 위해서는 위와같은 모듈들을 임포트 한 후 .save()메서드를 Snippet에 추가해줬다. pygments에 대해서 정확히 모르기 때문에 공식문서를 따라하려고 한다.
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
linenos = 'table' if self.linenos else False
options = {'title': self.title} if self.title else {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super().save(*args, **kwargs)
pygments를 사용하기 위한 코드들이라고 생각하면 될 것 같다.
# 데이터베이스 초기화(리셋)
데이터베이스를 리셋해준다. 공식문서에서는 터미널을 이용해 지워주라고 하지만, 그냥 마이그레이션폴더와 sqlite를 지워주고 마이그레이션과 마이그레이트를 다시 진행해주면 된다. 삭제 했다면
python manage.py makemigrations snippets
python manage.py migrate
python manage.py createsuperuser
# Userserializer 추가
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'snippets']
User에 대한 시리얼라이즈는 존재하지 않기때문에 serializers.py에 추가해준다. 처음에 추가해주는 필드가 뭔지 몰랐었다 그래서 구글링하면서 찾아봤다. Meta에는 사용할 필드들을 적어주고, 그 시리얼라이즈에서 쓰이는 필드들을 class내부에 선언해주면 되는거 같다. 일단 Meta에는 이전에 다뤘던 것이지만, 시리얼라이즈 할 모델의 이름을 model= 에 적어주고, fields에 그 대상들을 적어주면 된다.
user에서 snippets은 역의 관계이기때문에 별도로 정의해서 알려줘야한다고 한다. 그래서 위 코드를 추가 시킨 것이다.
#View추가
from django.contrib.auth.models import User
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
당연히 View를 추가해줘야한다. 우리는 제너릭뷰를 통해 간단하게 만드는 방법을 배웠기 때문에 다음과 같이 코드를 줄여서 사용 할 수 있다. UserList에서는 List를 쫙 뽑아 보여주면 되기때문에 다음과 같이 ListAPIView를 상속받고, UserDetail에서는 RetrieveAPIView를 사용시켜준다. 해당 뷰는 하나의 인스턴스만 뽑아올 수 있게 해주는 것이다.
## RetrieveAPIView
지금까지 Detail을 뽑아오기 위해서는 그 인스턴스의 고유 id(pk)값을 알아야 했다. 근데 저렇게 제너릭을 사용하면 pk는 어디서 받아오는건가 의문이 생긴다. 그래서 찾아봤다.
장고에서 해당 코드를 찾아봤다. 저기에는 get이 정의 돼 있는데 여기서 알아서 인자를 받아가서 get해온다고 한다.
### *args 와 **kwargs
*args와 **kwargs에 대해서는 따로 포스팅하여 정리하도록 하겠다. 먼저 포인터와는 전혀 다른 개념이라는 것을 알 고 있어야한다.
*args는 인자의 개수가 몇개인지 모를때 그냥 통째로 가져오는 개념이다.
**kwargs도 비슷하지만 key-value값으로 받아 올 수 있다.
여튼 우리가 pk값을 인자로 넘겨주면 이렇게 내부적으로 get을 이용해 해결한다는 점을 알 수 있다.
# urls.py
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
당연히 이제 경로를 추가해준다. UserDetail은 당연히!! <int:pk>로 어떤 인스턴스를 찾을것인지 알려줘야한다~~
# User 와 Snippets 연결
SnippetsList에
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
을 추가해준다. 정확히 말해서는 오버라이딩이다.
serializer.save()는 인자로 들어오는 key - value형태로 DB에 저장한다고한다. 해당 값이 있으면 update없으면 create를 해서 DB를 최신화 한다고 한다.
serailizer.py에 다음과 같이 추가해준다.
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta :
model = Snippet
fields = ['id','title','code','linenos','language','style','owner']
공식문서에 따르면 fields에도 'owner'를 추가시키라고 한다. owner는 ReadOnlyFields이기때문에 DB에 저장되지 않고, 항상 읽기전용이라고 한다. 아직은 뭔말인지 정확히 모르겠다.
다음 코드를 SnippeList와 SnippetDetail에 추가해준다. 인증된 사용자인지의 과정을 거치는것 같다.
#views.py
from rest_framework import permissions
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
일단 여기까지 하고 실행해보면 colum에서 뭔 user를 찾을수없다고도하고, DRF공식문서가 말하기로는 새로운 snippets를 추가할 수 없다고한다. 왜냐하면 우린 로그인인증 기능을 만들었기 때문에 그 기능을 활용해야하기 때문이다. 그래서 인증을 담당하는 것을 추가해준다
# 최상위 urls.py에 추가
path('api-auth/', include('rest_framework.urls')),
을 추가해준다! 오 신기하게도 해당 api-auth로 접근하면 login과 logout기능이 생겼다!
친절하게 ui도 짜준다.
#그리고 에러..
DRF공식문서에는 여기까지 따라하면 새로고침하면, Login링크가 생긴다고하는데.. 그건 개뿔 눈 코빼기도 안보인다.. 구글링하러 간다 ㅠ
그래서 db랑 migrations를 다지우고 다시 생성해주고 , superuser도 다시 만들어줬다. 그랬더니 신기하게도
이렇게 다시 잘 연결된다. Login 버튼도 우측상단에 생겼다 얄루~
그리고 로그인상태이면
요로코롬 보낼 수 있다. 그러면 post맨으로 쏘면 어떻게 될까?
# POSTMAN으로 쏴보기!
혹시 브라우저가 내 세션이나 쿠키를 가지고있는게 영향을 줄 수 있기때문에 크롬 로그아웃 상태에서 진행했다. 이게 유의미한 영향을 끼치는지도 잘 모르겠지만, 비교해보려고 한다.
## 크롬 비로그인 상태 && 로그인 상태
당연히 크롬의 로그인 여부는 영향을 주지 않았다. 그리고 비인증상태라고 POST를 거부했다.
## 로그인 상태
로그인 상태에서는 잘 쏴지고 잘 저장도 된다~
## '/users/' 엔드포인트로 이동하여 각 사용자의 '스니펫' 필드에 각 사용자와 연결된 스니펫 ID 목록이 표시확인
# 객체(object) 수준 권한
이제 작성자만 해당 snippet를 수정하거나 삭제할 수 있게 추가적인 권한을 줘야한다. permissions.py를 생성해준다. 그리고 해당 코드를 복붙한다.
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
딱 봐도 snippet의 주인과 현재 로그인한 유저가 같은지 판단해서 true/false를 넘겨주는 코드같다.
삭제/수정은 어디서 쓰여야할까? 봐로 Detial즉 상세 파일에 대해서 사용 되어야하기때문에 views/SnippetDetail에 아래 코드로 수정해준다.
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
저렇게 삭제 수정권한을 같은 사용자일때만 가능하게 해준 것이다.
일단 내가 DRF를 공부하는 이유 중 하나가 이 로그인 구현을 위해서다. 아직 정확히는 모르지만 인증의 개념에 대해서 로그인 기능에 대해서 공부해보고, 어디서 건너들은 JWT토큰 방식을 이용해보려고한다. 사실 이 백서버를 로그인 전용 API로만 구현해보고싶은데, 그럴경우에는 어떻게 이 쿠키와 세션같은걸 사용해야하는지 또 프론트에서는 뭘 받아야하는지 잘 모르겠다.. 열심히 공부해보면서 하나씩 알아가야겠다!