DRF 1

TIL Day 86

By polaris0208

Django REST Framework

JSON을 응답하는 API 설계

설치

  • djangorestframework 패키지 설치
  • settings.py에 등록
pip install djangorestframework
INSTALLED_APPS = [
		...
		'rest_framework',
		...
]

Serialization

  • 직렬화
    • 객체 또는 데이터 구조를 저장, 전송을 위해 다른 포맷으로 변경
    • Queryset 혹은 ModelInstanceJSON, XML, YAML 등의 형태로 변환
  • DRF : serializers를 통해 기능 제공

urls.py

from django.urls import path
from . import views

app_name = "articles"
urlpatterns = [
    path("json-drf/", views.json_drf, name="json_drf"),
]

serializers.py

from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = "__all__"

views.py

  • @api_view(["GET"]) : 함수형 view 사용 시 필수
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import ArticleSerializer

@api_view(["GET"])
def json_drf(request):
    articles = Article.objects.all()
    serializer = ArticleSerializer(articles, many=True)
    return Response(serializer.data)

Postman

  • API를 디자인, 테스트, 문서화, 공유를 할 수 있도록 도와주는 소프트웨어
  • URLRequest를 활용하여 디자인 및 테스트

Seeding

  • 설치 : pip install django-seed
  • 등록 : settings.py / INSTALLED_APPS에 추가
  • 명령어 : python3 manage.py seed articles --number=30
    • No module named 'psycopg2' 에러 : pip install psycopg2
    • 지정 Sedding
      • python3 manage.py seed articles --number=30 --seeder "article_id" 1

Class Based View

  • Http Method에 대한 처리
    • 함수형 : 조건으로 분리
    • 클래서형 : 함수로 분리
      • GET요청에 대한 처리는 get()
      • POST 요청에 대한 처리는 post()
  • 클래스를 사용하기 때문에 코드의 재사용성과 유지보수성이 향상됩니다.
  • 기본 APIView외에도 여러 편의를 제공하는 다양한 내장 CBV가 존재합니다.

종류

  • APIView - DRF 베이스 클래스
  • GenericAPIView
    • 일반적인 API 작성을 위한 기능이 포함된 클래스
    • CRUD 여러가지 기능 내장
  • Mixin
    • 재사용 가능한 여러가지 기능
    • 섞어서 사용
      • ListModelMixin - 리스트 반환
      • CreateModelMixin - 객체를 생성
  • ViewSets
    • endpoint를 한 번에 관리
    • RESTful API에서 반복되는 구조를 더 편리하게 작성

Articles

models.py

  • 기존 Django 와 동일
class Article(models.Model):
    title = models.CharField(max_length=120)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title
    
class Comment(models.Model):
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name="comments"    
    )
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

serializers.py

  • 직렬화 도구 정의
  • read_only_fields = ("article",) : "article"은 참조만
  • to_representation : 보여지는 결과
    • .pop('article') : 본문 제거
  • ArticleDetailSerializer
    • ArticleSerializer 과 동일한 기능
    • comments 내용 포함
    • comments_count : 개수 계산
      • serializers.IntegerField
from rest_framework import serializers
from .models import Article, Comment

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = "__all__"
        read_only_fields = ("article",)
    
    def to_representation(self, instance):
        # 보여지는 결과
        ret = super().to_representation(instance)
        ret.pop('article')
        return ret

class ArticleSerializer(serializers.ModelSerializer):

    class Meta:
        model = Article
        fields = "__all__"


class ArticleDetailSerializer(ArticleSerializer):
    comments = CommentSerializer(many=True, read_only=True)
    comments_count = serializers.IntegerField(source="comments.count", read_only=True)

views.py

  • IsAuthenticated : 로그인 여부 확인
  • Response : 결과 전달 메서드
    • status 상태코드 설정 가능
from .models import Article
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from .serializers import (
  ArticleSerializer, 
  ArticleDetailSerializer,  
  CommentSerializer
  )
from rest_framework.permissions import IsAuthenticated

Articles

  • 기본 로직은 함수형과 동일
  • get_object : 공통적으로 사용하는 객체 불러오는 기능을 분리
  • partial=True : 일부만 수정하는 것 허용
  • raise_exception=True : 예외 처리 기능
class ArticleListAPIView(APIView):
    permission_classes = [
        IsAuthenticated,
    ]
    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        
class ArticleDetailAPIView(APIView):
    permission_classes = [
        IsAuthenticated,
    ]
    def get_object(self, pk):
        return get_object_or_404(Article, pk=pk)

    def get(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleDetailSerializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleDetailSerializer(article, data=request.data, partial=True)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data)

    def delete(self, request, pk):
        article = self.get_object(pk)
        article.delete()
        data = {"pk": f"{pk} is deleted."}
        return Response(data, status=status.HTTP_200_OK)

Comments

  • article 을 불러온 후 역참조하여 comments 호출
class CommentListAPIViews(APIView):
    permission_classes = [
        IsAuthenticated,
    ]
    def get(self, request, article_pk):
        article = get_object_or_404(Article, pk=article_pk)
        comments = article.comments.all()
        serializer = CommentSerializer(comments, many=True)
        return Response(serializer.data)
    
    def post(self, request, article_pk):
        article = get_object_or_404(Article, pk=article_pk)
        serializer = CommentSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save(article=article)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

from .models import Comment

class CommentDetailAPIView(APIView):
    permission_classes = [
        IsAuthenticated,
    ]
    def get_object(self, comment_pk):
        return get_object_or_404(Comment, pk=comment_pk)

    def delete(self, request, comment_pk):
        comment = self.get_object(comment_pk)
        comment.delete()
        data = {"pk": f"{comment_pk} is deleted."}
        return Response(data, status=status.HTTP_200_OK)

    def put(self, request, comment_pk):
        comment = self.get_object(comment_pk)
        serializer = CommentSerializer(comment, data=request.data, partial=True)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data)

urls.py

  • views.<APIView>.as_view() 형태로 사용
from django.contrib import admin
from django.urls import path
from . import views

app_name = "articles"

urlpatterns = [
    path("", views.ArticleListAPIView.as_view(), name="article_list"),
    path("<int:pk>/", views.ArticleDetailAPIView.as_view(), name="article_detail"),
    path(
        "<int:article_pk>/comments/",
        views.CommentListAPIViews.as_view(),
        name="comment_list",
    ),
    path(
        "comments/<int:comment_pk>/",
        views.CommentDetailAPIView.as_view(),
        name="comment_detail",
    ),
]

JSON Web Token

  • 일정한 규칙을 가지고 있고 간단한 서명을 더한 문자열
  • 토큰 자체에 유저에 대한 간단한 정보가 들어있는 형태입니다.
  • Session DB나 인증을 위한 여러가지 로직 처리가 불필요

처리방식

  • ID/PW를 서버로 전송
  • 서버에서 ID/PW를 검증
    • 유효한 경우 일정한 형식으로 서명 처리된 Token응답
  • 이후 클라이언트는 모든 요청 Header에 토큰을 담아 서버로 요청을 전송
  • 서버는 해당 토큰의 유효성을 검증하고 유저의 신원과 권한을 확인 후 요청을 처리

구조

  • HEADER
    • 토큰의 타입 또는 서명 부분의 생성에 어떤 알고리즘이 사용되었는지 등을 저장
  • PAYLOAD
    • 유저 정보: 토근 발급자, 토큰 대상자, 토큰 만료시간, 활성날짜, 발급시간 등
    • 민감한 정보 제외 최소의 정보만 저장 : User, PK
    • Claim : Key-Value 형태로 구성
  • SIGNATURE
    • HEADER + PAYLOAD + 서버의 비밀키 : HEADER에 명시된 암호 알고리즘 방식으로 생성한 값
    • 서명의 유효여부 + 유효기간 내의 토큰인지 확인하여 인증

Token

Access Token

  • 인증을 위해 헤더에 포함
  • 매 요청 포함 / 보안 취약
    • 짧은 만료기간 : 탈취되어도 만료되어 사용 불가

Refresh Token

  • 새로 Access Token을 발급받기 위한 Token
  • Access Token 보다 긴 유효기간
    • 주로 사용자의 기기에 저장
    • Refresh Token 만료 시 재인증
  • BlackList : 탈취를 보완하기 위해 DB 리소스를 사용

accounts

simplejwt

  • pip install djangorestframework-simplejwt

settings

  • ROTATE_REFRESH_TOKENS : 엑세스 토큰을 갱신 한 후 리프레시 토큰도 갱신
    • BLACKLIST_AFTER_ROTATION : 갱신 후 사용된 토큰은 블랙리스트로 관리하여 보안 강화
INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework_simplejwt.token_blacklist',
    ...
]

# Custom User Model
AUTH_USER_MODEL = 'accounts.User'

# JMT
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}

from datetime import timedelta

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
}

models.py

  • 커스텀 모델을 생성하여 기능 추가에 대비
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
  pass

urls.py

  • views.py 없이 작동
from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path("signin/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

테스트

  • python3 manage.py createsuperuser : 관리자 계정 생성
  • Postman 에 접속
    • json 형식으로 ID/PASSWORD 전달
    • Token 정보는 요청 Header에 담아서 전달
Tags: TIL Tools