[DRF] 게시판 프로젝트 시작 & 회원 관련 기능 구현하기

mia·2024년 8월 7일
0

DRF 기반으로 게시판 서비스를 만들고, React.js와 연동해보자❗

필요한 기능

회원 관련 기능

  • 회원 프로필 관리(닉네임, 소개, 프로필 사진 등)
  • 회원가입
  • 로그인
  • 프로필 수정

게시글 관련 기능

  • 게시글 생성
  • 게시글 1개 가져오기/게시글 목록 가져오기(개수 제한)
  • 게시글 수정
  • 게시글 삭제
  • 게시글에 좋아요 기능
  • 게시글 필터링(내가 작성한 글/좋아요 누른 글)
  • 게시글 각 기능마다 권한 설정

댓글 관련 기능

  • 댓글 생성
  • 댓글 1개 가져오기/댓글 목록 가져오기
  • 댓글 수정
  • 댓글 삭제
  • 게시글을 가져올 때 댓글도 가져오기

프로젝트 생성

$ py -m venv myvenv
$ source myvenv/Scripts/activate
$ pip install django
$ pip install djangorestframework
$ django-admin startproject myboard .

settings.py에서 세부 설정

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
]

TIME_ZONE = 'Asia/Seoul'

회원 관련 기능 구현

Django의 기본 User 모델을 사용한다.
기본 User 모델은 django.contrib.auth.models 안에 있으며, auth(인증)을 위해 Django가 미리 만들어놓은 앱인 django.contrib.auth는 자동으로 settings.py에 등록되어 있다.
나중에 User 모델에 접근하기 위해서는 아래와 같이 불러온다.

from django.contrib.auth.models import User

User 모델의 대표적인 필드는 다음과 같다.

필드명타입설명
username문자열ID가 들어가는 필드로, 다른 사용자와 겹칠 수 없으며 필수로 요구되는 필드
password문자열비밀번호로, 필수 필드이며 비밀번호 문자열 그대로 저장하지 않고 해시값을 저장함
first_name문자열이름 개념으로, 선택적으로 사용할 수 있는 필드
last_name문자열성 개념으로, 선택적으로 사용할 수 있는 필드
email문자열회원의 이메일 주소로, 선택적으로 사용할 수 있는 필드

회원 인증의 개념

클라이언트는 서버로 요청을 보낼 때 HTTP 메시지에 자신이 누구인지 적어서 보낸다. 이때 서버에게 유저라는 사실을 확인받는 과정을 인증이라고 한다.

인증 방식의 종류를 몇 가지 알아보자.

ID와 PW를 그대로 담아 보내기

인증을 위해 Django에게 클라이언트가 자신의 ID와 PW를 그대로 보내면 Django가 확인하는 방법이다.
가장 간단하지만 취약한 방법으로, Django(서버)가 비밀번호를 해시값으로 저장하며 보안에 신경쓰더라도 누군가가 요청을 가로채면 유저의 ID와 PW를 그대로 볼 수 있다.

세션 & 쿠키

세션은 서버 쪽에서 저장하는 정보,
쿠키는 클라이언트의 자체적인 임시 저장소이다.

이 방식은 로그인을 하고 난 후에는 매 요청마다 ID와 PW를 보내지 않고, 로그인 후 발급되는 세션 ID를 보내는 것으로 인증을 대체한다.

처음 로그인을 하면 서버 측에서 해당 로그인 정보로 세션을 생성하고 클라이언트에게 세션의 ID를 응답으로 보낸다. 클라이언트는 응답으로 받은 세션 ID를 쿠키에 보관했다가, 요청을 보낼 때마다 세션 ID를 쿠키에서 꺼내와 HTTP 헤더에 넣어 보낸다. 서버는 세션 ID를 바탕으로 저장된 세션을 탐색해 요청을 보낸 유저가 맞는지 확인하고 인증한다.

세션 ID 자체에는 의미있는 정보가 없지만, 세션 ID 또한 노출될 수 있고 그것만 가지고도 유저인 척할 수 있기 때문에 안전하지 않다.

토큰 & JWT

기본적인 토큰 방식은 회원가입 시 유저에 매칭되는 토큰을 생성하고 저장한다.
로그인 요청이 들어오면, 서버는 해당 토큰을 응답으로 보내주고 클라이언트는 토큰을 저장했다가 요청을 보낼 때 헤더에 넣어서 같이 보낸다. 서버는 요청으로 들어온 토큰이 있는지 확인하고 인증한다.

토큰이 세션 & 쿠키와 다른 점은 토큰 자체에 사용자에 대한 정보가 있어서 서버가 토큰만을 가지고도 유저를 구분할 수 있다는 것이다.
누군가 토큰을 탈취해도, 토큰은 settings.pySECRET_KEY로 암호화되어 있으므로 이 키만 노출되지 않는다면 토큰 자체로는 어떤 정보도 얻을 수 없다.

그러나 여전히 노출될 수 있다는 점에서 세션 & 쿠키와 동일한 문제점을 갖는다.


각 토큰/세션에 대한 유효기간을 설정하면, 누군가가 토큰/세션을 탈취해도 몇 분 지나면 쓸 수 없는 값이 되므로 문제를 어느 정도 방지할 수 있다.

유효기간이 있는 토큰을 직접 구현하기엔 아직 어려우므로 이 프로젝트는 기본 토큰 인증 방식을 사용한다.



회원가입 기능

이제 회원가입 기능을 직접 구현해보자.
회원 관련 기능을 모아놓을 users 앱을 생성한다.

$ py manage.py startapp users

settings.py에 새로 만든 앱과 기본 토큰 방식을 사용하기 위한 앱을 등록한다.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',
    'users',
]

또한 프로젝트의 인증 방식으로 토큰 방식을 사용한다는 것을 정의해두어야 한다.
settings.py의 (LANGUAGE_CODE 위 정도의) 적당한 위치에 REST_FRAMEWORK라는 옵션을 작성한다.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

이제부터는 그동안 해왔던 개발 순서대로 모델 ➡️ 시리얼라이저 ➡️ 뷰 ➡️ URL 순서대로 구현하면 된다.

모델

Django의 기본 User 모델을 사용할 예정이므로, 따로 모델을 만들지 않는다. 따라서 models.py에는 작성할 내용이 없다.
User 모델 필드 중에는 아래 필드들을 사용할 것이다.

  • username : ID로 활용 / required=True
  • password : required=True
  • email : required=True

추가적으로, password2를 입력하게 하여 password와 일치하는지 다시 확인하는 과정을 거칠 예정이다.

시리얼라이저

시리얼라이저를 작성하기 앞서, 회원가입 프로세스를 정리하면 다음과 같다.

  1. 사용자가 정해진 폼에 따라 데이터를 입력
    (username, password, password2, email)
  2. 데이터가 들어오면 ID가 중복되지는 않는지, 및 비밀번호가 너무 짧거나 쉽지는 않은지 검사
  3. 2단계를 통과했으면 회원 생성
  4. 회원 생성이 완료되면 해당 회원에 대한 토큰 생성

뷰에서 다루어도 되지만, 이번 프로젝트에서는 시리얼라이저의 Validation(검증) 기능을 사용하여 회원가입 요청이 기준에 맞는지 검사하는 것을 시리얼라이저에서 처리하도록 해보자.
시리얼라이저의 create() 메소드를 활용하여 회원과 토큰을 생성하는 3, 4단계 부분까지 작성한다.

users/serializers.py 파일을 생성한다.

from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
# Django의 기본 패스워드 검증도구

from rest_framework import serializers
from rest_framework.authtoken.models import Token           # 토큰 모델
from rest_framework.validators import UniqueValidator       # 이메일 중복 방지용 검증도구

class RegisterSerializer(serializers.ModelSerializer):      # 회원가입 시리얼라이저
    email = serializers.EmailField(
        required=True,
        validators=[UniqueValidator(queryset=User.objects.all())],      # 이메일 중복 검증
    )
    password = serializers.CharField(
        write_only=True,
        required=True,
        validators=[validate_password],     # 비밀번호 검증
    )
    password2 = serializers.CharField(write_only=True, required=True)

    class Meta:
        model = User
        fields = ('username', 'password', 'password2', 'email')
    
    def validate(self, data):       # 비밀번호 일치 여부 확인
        if data['password'] != data['password2']:
            raise serializers.ValidationError(
                {"password": "Password fields didn't match."}
            )
        return data
    
    def create(self, validated_data):
    # CREATE 요청에 대해 create 메소드를 오버라이딩, 유저를 생성하고 토큰을 생성하게 함
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data['email'],
        )
        user.set_password(validated_data['password'])
        user.save()
        token = Token.objects.create(user=user)
        return user

시리얼라이저에서 회원가입 관련 기능을 대부분 구현했으므로 뷰는 간단하게 작성할 수 있다.

회원가입의 경우 회원 생성 기능(POST 요청)만 있으므로 굳이 Viewset을 사용해 다른 API 요청을 처리할 필요가 없다.
generics.CreateAPIView를 사용해 작성한다.

from django.contrib.auth.models import User
from rest_framework import generics

from .serializers import RegisterSerializer

# Create your views here.
class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = RegisterSerializer

URL

users/urls.py 파일을 생성한다.

from django.urls import path
from .views import RegisterView

urlpatterns = [
    path('register/', RegisterView.as_view()),
]

myboard/urls.py 파일에 연결한다.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
]

마이그레이션 & 프로젝트 실행

$ py manage.py makemigrations
$ py manage.py migrate
$ py manage.py runserver

http://127.0.0.1:8000/users/register/ 주소로 접속하면 다음과 같은 화면이 나온다.

API 테스트를 위해 Insomnia에서 POST 요청으로 JSON 문자열을 보내보자.

비밀번호로 password를 넣었더니 너무 흔하다고 까였다.
좀 어려운 문자열 dywjdeh?(요정도?)로 바꿔줬다.

201 Created 상태 코드와 함께 잘 생성되었다는 것을 알 수 있다.
같은 username과 email로 POST 요청을 보내봤다.

중복되는 username과 email의 경우를 시리얼라이저의 UniqueValidator가 잘 걸러준다.
이번에는 다른 username으로, password와 password2의 값을 다르게 보내보자.

패스워드 필드가 매치하지 않는다는 메시지와 함께 시리얼라이저의 validate() 함수가 잘 동작한다는 것을 알 수 있다.
password와 password2의 값을 일치시켜 보내면 가입에 성공한다.

이제 가입한 유저에 대해 토큰이 잘 생성되었는지 확인해보자.
관리자 계정을 생성하고 관리자 페이지로 접속한다.

$ py manage.py createsuperuser


Tokens 모델이 있다.

KEY 옆에 각 유저가 있는 것을 확인할 수 있다.
토큰까지 생성 완료!



로그인 기능

앞서 구현한 로그인 프로세스에 의하면, 서버는 사용자가 보낸 ID/PW를 확인하여 해당하는 토큰을 응답한다.

모델과는 아예 관련이 없으므로, 시리얼라이저도 ModelSerializer를 사용할 필요가 없다.

시리얼라이저

serializers.Serializer를 상속받아 작성한다.

from django.contrib.auth import authenticate
# Django의 기본 authenticate 함수로, 우리가 설정한 DefaultAuthBackend인 TokenAuth 방식으로 유저를 인증해줌

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField(required=True)
    password = serializers.CharField(required=True, write_only=True)
    # write_only 옵션을 통해 클라이언트->서버 방향의 역직렬화는 가능, 서버->클라이언트 방향의 직렬화는 불가능
    def validate(self, data):
        user = authenticate(**data)
        if user:
            token = Token.objects.get(user=user)
            return token
        raise serializers.ValidationError(
            {"error": "Unable to log in with provided credentials."}
        )
  • password는 비밀번호를 받는 필드로, write_only=True 옵션을 사용해 클라이언트에서 서버로만 전송될 수 있고 서버에서 클라이언트로는 반환되지 않는다.
  • validate 메소드: 시리얼라이저에서 입력된 데이터를 검증하는 메소드로, authenticate(**data) 함수를 사용해 주어진 사용자 이름과 비밀번호로 사용자를 인증하고 토큰을 반환한다.

로그인은 모델에 영향을 주지 않기 때문에, 어떤 특별한 제네릭을 사용할 필요 없이 기본 GenericAPIView를 사용하면 된다.
로그인 요청은 POST 요청으로 처리한다.
시리얼라이저를 통과하여 얻은 토큰을 그대로 응답해 주는 방식으로 구현한다.

from django.contrib.auth.models import User
from rest_framework import generics, status
from rest_framework.response import Response

from .serializers import RegisterSerializer, LoginSerializer

class LoginView(generics.GenericAPIView):
    serializer_class = LoginSerializer
    def post(self, request):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        token = serializer.validated_data       # validate()의 리턴값인 Token을 받아옴
        return Response({"token": token.key}, status=status.HTTP_200_OK)

URL

from django.urls import path
from .views import RegisterView, LoginView

urlpatterns = [
    path('register/', RegisterView.as_view()),
    path('login/', LoginView.as_view()),
]

실행


앞서 만들었던 계정으로 로그인 요청을 보내면 응답으로 토큰이 주어진다.

잘못된 비밀번호를 입력하면 에러 메시지가 발생한다.
로그인 기능까지 구현 완료!

profile
바보

0개의 댓글

관련 채용 정보