Django를 사용해 API 서버 만들기 (TIL 14)

석형원·2024년 4월 11일

TIL

목록 보기
14/52

✏️ 오늘 학습한 내용

Users와 Authentication

  • User 추가
  • User 관리
  • Form을 사용하여 User 생성 ( 회원 가입 )
  • User 권한 관리
  • perform_create()
  • POSTMAN

🔎 Users와 Authentication

User 추가하기

  1. Field 추가 - owner
    polls/models.py

    # owner field 추가
    
    from django.db import models
    from django.utils import timezone
    import datetime
    from django.contrib import admin
    
    class Question(models.Model):
        question_text = models.CharField(max_length=200,verbose_name='질문')
        pub_date = models.DateTimeField(auto_now_add=True,verbose_name='생성일')
    
        # on_delete=models.CASCADE : owner가 삭제되면 Question도 삭제해라
        # null=True : field가 비어있어도 됨
        owner = models.ForeignKey('auth.User',  related_name='question', on_delete=models.CASCADE, null=True)
  2. Migration
    python manage.py makemigrations
    python manage.py migrate

  3. Django Shell에서 확인

    from django.contrib.auth.models import User
    
    # 유저 모델을 확인
    User._meta.get_fields()
    
    # 어느 유저가 있는지 Query Set으로 확인
    User.objects.all()
    
    from polls.models import *
    user = User.objects.first()
    
    # user가 갖고 있는 question을 모두 가져옴
    # where owner = user.id에 해당하는 question
    # related_name = 'questions'로 지정했기에 가능
    user.questions.all()
    
    # Choice의 경우,
    # 어느 모델도 related_name을 지정하지 않았기에 choice_set으로 자동 설정됨.
    question = Question.objects.first()
    question.choice_set.all()
    

User 관리하기

  • polls_api/serializers.py

    from rest_framework import serializers
    from polls.models import Question
    from django.contrib.auth.models import User
    
    ...
    
    # User Class 추가
    class UserSerializer(serializers.ModelSerializer):
        # PrimaryKeyRelatedField : User의 primarykey에 해당하는 여러개의 질문이 있다.
        # owner == user.id인 questions를 모두 불러옴
        questions = serializers.PrimaryKeyRelatedField(many=True, queryset = Question.objects.all())
    
        class Meta:
            model = User
            fields = ['id','username','questions']
    
  • polls_api/views.py

    from polls.models import Question
    from polls_api.serializers import QuestionSerializer, UserSerializer
    from rest_framework import generics
    # 장고 내 모델에 정의된 User를 불러옴
    from django.contrib.auth.models import User
    
    class QuestionList(generics.ListCreateAPIView):
        queryset = Question.objects.all()
        serializer_class = QuestionSerializer
    
    class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
        queryset = Question.objects.all()
        serializer_class = QuestionSerializer
    
    class UserList(generics.ListAPIView):
        queryset = User.objects.all()
        serializer_class = UserSerializer
    
    class UserDetail(generics.ListAPIView):
        queryset = User.objects.all()
        serializer_class = UserSerializer
  • polls_api/urls.py

    from django.urls import path
    from .views import *
    
    urlpatterns = [
        path('question/', QuestionList.as_view() , name='question-list'),
        path('question/<int:pk>/', QuestionDetail.as_view(),name="queston-detail"),
        # user 경로 추가
        path('users/',UserList.as_view()),
        path('users/<int:pk>',UserDetail.as_view()),
    ]

Form을 사용하여 User 생성 ( 회원 가입 )

1. Django에서 제공하는 기능으로 User 생성
( http://127.0.0.1:8000/polls/signup/ )

  • polls/views.py

    ...
    
    # django에서 가져온 generic, rest_framework에서 가져온 generics와 다름
    from django.views import generic
    from django.urls import reverse_lazy
    # Form 생성
    from django.contrib.auth.forms import UserCreationForm
    
    ...
    
    class SignupView(generic.CreateView):
        # UserCreationForm : '유저이름'과 '패스워드', '패스워드 확인' 필드를 제공하며,
        # 회원가입 폼을 자동으로 생성
        form_class = UserCreationForm
        
        # reverse_lazy : urls에서 정의한 name을 기반으로 url찾아서 전달해줌 ex) '/rest/users/'
        # 즉, 회원가입 성공시, user-list로 리디렉션 된다.
        success_url = reverse_lazy('user-list')
        template_name = 'registration/signup.html'
  • polls/urls.py

    from django.urls import path
    from . import views
    # class인 SignupView를 사용하기 위함
    from .views import *
    
    # 앞으로 템플릿에서 url 태그를 사용할 때, 이름 앞에 polls를 붙여줘야함
    # App마다 다른 detail url을 사용하기 위함
    app_name = 'polls'
    
    urlpatterns = [
        path('', views.index, name='index'),
        path('<int:question_id>/', views.detail, name='detail'),
        path('<int:question_id>/result', views.result, name='result'),
        path('<int:question_id>/vote', views.vote, name='vote'),
        path('signup/',SignupView.as_view(),),
    ]
  • polls/templates/registration/signup.html

    <h2>회원가입</h2>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">가입하기</button>
    </form>
    • 템플릿에서 사용한 {{ form.as_p }}formSignupView에서 전달한 form_class의 객체이다.

    • {{ form.as_p }} : from에 정의한 subject, content 속성에 해당하는 HTML 코드를 자동으로 생성

2. Serializer을 사용하여 User 생성
( http://127.0.0.1:8000/rest/register/ )

  • polls_api/serializers.py

    from rest_framework import serializers
    from polls.models import Question
    from django.contrib.auth.models import User
    # validators를 사용하기 위함
    from django.contrib.auth.password_validation import validate_password
    
    ...
    
    # 회원 가입
    class RegisterSerializer(serializers.ModelSerializer):
        password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
        password2 = serializers.CharField(write_only=True, required=True)
    
        # Serializer 안에 validate를 구현을 하면 원하는 내용에 대해서 validate를 구현할 수 있음
        # password 일치 여부 확인
        def validate(self, attrs) :
            if attrs['password'] != attrs['password2']:
                raise serializers.ValidationError({"password":"두 패스워드가 일치하지 않습니다."})
            return attrs
    
        # password2가 create에 들어가지 않게 create를 수정
        def create(self, validated_data):
            user = User.objects.create(username=validated_data['username'])
            user.set_password(validated_data['password'])
            user.save()
    
            return user
    
        class Meta:
            model = User
            fields = ['username','password','password2']
  • polls_api/views.py

    from polls.models import Question
    from polls_api.serializers import *
    from rest_framework import generics
    # 장고 내 모델에 정의된 User를 불러옴
    from django.contrib.auth.models import User
    
    # class 안에서 기능이 전부 구현이 되어있기 때문에, 이를 상속받아 사용
    class QuestionList(generics.ListCreateAPIView):
        queryset = Question.objects.all()
        serializer_class = QuestionSerializer
    
    class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
        queryset = Question.objects.all()
        serializer_class = QuestionSerializer
    
    class UserList(generics.ListAPIView):
        queryset = User.objects.all()
        serializer_class = UserSerializer
    
    class UserDetail(generics.ListAPIView):
        queryset = User.objects.all()
        serializer_class = UserSerializer
    
    # RegisterUser를 UserList에서 한번에 처리하지 않는 이유?
    # 별도의 Serializer class를 쓰기 위해서
    # 생성할 때 필요한 추가 기능을 Register Serializer에 쓰고 싶기 때문에
    class RegisterUser(generics.CreateAPIView):
        serializer_class = RegisterSerializer
  • polls_api/urls.py

    from django.urls import path
    from .views import *
    
    urlpatterns = [
        path('question/', QuestionList.as_view() , name='question-list'),
        # GenericAPIView에서 primary key를 pk로 받기 때문에, pk라 해줘야함
        path('question/<int:pk>/', QuestionDetail.as_view(),name="queston-detail"),
        path('users/',UserList.as_view()),
        path('users/<int:pk>',UserDetail.as_view()),
        path('register/', RegisterUser.as_view()),
    ]

User 권한 관리

( User가 만든 Question이라는 것을 기록하는 기능 구현 )

  • 로그인 기능 및 로그인 버튼 추가

    • polls_api/urls.py

      ...
      
      urlpatterns = [
          ...
          # 로그인 버튼 추가
          # django.contrib.auth의 login,logout view 사용한다는 의미
          path('api-auth/', include('rest_framework.urls'))
      ]
    • settings.py

      ...
      
      # 로그인 다음에 redirect되는 url을 정의
      import os
      from django.urls import reverse_lazy
      
      LOGIN_REDIRECT_URL = reverse_lazy('question-list')
      LOGOUT_REDIRECT_URL = reverse_lazy('question-list')
      
      ...

  • User(로그인 한 사람)가 만든 Question이라는 것을 기록

    • polls_api/serializers.py ( owner라는 컬럼 추가 )

      from rest_framework import serializers
      from polls.models import Question
      from django.contrib.auth.models import User
      from django.contrib.auth.password_validation import validate_password
      
      class QuestionSerializer(serializers.ModelSerializer):
          # readonly + 로그인된 사용자 이름이 바로 owner
          owner = serializers.ReadOnlyField(source='owner.username')
          class Meta:
              model = Question
              # 작성한 사용자가 누구인지 owner 추가
              fields = ['id','question_text','pub_date', 'owner']
    • polls_api/views.py ( 생성 시, owner 기록 )

      from polls.models import Question
      from polls_api.serializers import *
      from rest_framework import generics
      from django.contrib.auth.models import User
      
      class QuestionList(generics.ListCreateAPIView):
          queryset = Question.objects.all()
          serializer_class = QuestionSerializer
      
          # 생성될 때, 로그인한 사용자를 owner로 지정
          def perform_create(self, serializer):
              serializer.save(owner=self.request.user)
  • 로그인 시에만 생성, 수정, 삭제할 수 있도록 설정

    • polls_api/views.py

      from polls.models import Question
      from polls_api.serializers import *
      # permissions 추가
      from rest_framework import generics, permissions
      from django.contrib.auth.models import User
      
      class QuestionList(generics.ListCreateAPIView):
          queryset = Question.objects.all()
          serializer_class = QuestionSerializer
          # 로그인 된 상태에서만 Question을 만들 수 있게 지정
          # IsAuthenticated : 인증되지 않은 사용자에게 권한을 허용하지 않음
          # IsAuthenticatedOrReadOnly : get만 인증되지 않아도 허용해줌,
          # get->read, post-> create, put->update, del->delete
          # 즉, question 내용을 볼 수 있게 해준다, 단 생성, 수정, 삭제는 못한다.
          permission_classes = [permissions.IsAuthenticatedOrReadOnly]
      
          def perform_create(self, serializer):
              serializer.save(owner=self.request.user)
      
      class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
          queryset = Question.objects.all()
          serializer_class = QuestionSerializer
          # 이하 동일
          permission_classes = [permissions.IsAuthenticatedOrReadOnly]
      
      ...
      
  • User가 만든 질문만 수정할 수 있도록 권한 설정

    • polls_api/permissions.py

      from rest_framework import permissions
      
      class IsOwnerOrReadOnly(permissions.BasePermissions):
          def has_object_permission(self, request, view, obj):
              # SAFE_METHODS : GET/HEAD/METHOD는 허용, 즉 readonly는 항상 허용하겠다.
              if request.method in permissions.SAFE_METHODS:
                  return True
              # SAFE_METHODS가 아닐 때, 객체(question)의 owner가 로그인한 user가 맞는지 확인
              else :
                  return obj.owner == request.user
    • polls_api/views.py

      ...
      from .permissions import IsOwnerOrReadOnly
      
      ...
      class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
          queryset = Question.objects.all()
          serializer_class = QuestionSerializer
          # User가 만든 질문만 수정할 수 있도록 권한 추가
          permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

perform_create()

  • polls_api/views.py

    class QuestionList(generics.ListCreateAPIView):
        ...
        # 생성될 때, 로그인한 사용자를 owner로 지정
        def perform_create(self, serializer):
            serializer.save(owner=self.request.user)
    • perform_create()를 자세히 살펴보면

      처음에 url 요청이 들어오면, ->
      QuestionList로 연결이 되는데, .asview()에 의해서 ->
      ListCreateAPIView로 호출이 됨.

      여기서, ListCreateAPIView 내부에는 post 함수가 있는데,
      postcreate()를 return한다.
      또한, 이 create() 함수 내부에는 perform_create()가 정의되어 있기 때문에,

      perform_create() 오버라이드 한 것이다.

      정리하자면, QuestionListgenerics.ListCreateAPIView를 상속 받았고,
      ListCreateAPIViewmixins.CreateModeMixin을 상속 받았기 때문에, perform_create()를 오버라이드 할 수 있었다.

    • serializer.save(owner=self.request.user)

      serializer.save()에서 값을 지정하는 경우,
      readonly여도 값을 지정할 수 있다.
      심지어 id(pk)도 지정 가능하기 때문에,
      readonly인 owner에 값을 지정할 수 있는 것이다.

      즉, serialize를 할때는 readonly인 경우 값이 무시되지만,
      save()에 직접 선언하면 값 지정이 가능하다.

POSTMAN

  • POSTMAN을 사용하는 이유

    Django 서버로 열은 웹 사이트에서는 권한이 없어 question을 수정 요청할 수 없지만,
    이는 화면일 뿐이기 때문에,
    실제 API가 동작하는 서버의 환경에서는 얼마든지 수정 요청이 들어올 수 있다.

    이런 상황을 확인해보기 위해서 POSTMAN을 사용

  • 사용 예시 1
    ( 로그인 되지않은 상태에서는 권한이 없음을 확인 )

    • Header 설정

    • Body 설정

    • 결과

  • 사용 예시 2
    ( 로그인 시의 sessionid를 가져와 request 시도 )

    • sessionid, csrftoken (개발자도구)

    • Header 설정 및 결과

POSTMAN을 사용하면 여러 요청을 저장해뒀다가 쉽게 재현해서 사용할 수 있기에 API 서버 개발에 자주 사용됨.

profile
데이터 엔지니어를 꿈꾸는 거북이, 한걸음 한걸음

0개의 댓글