DRF: 게시판 만들기-사용자 관리
- 코드출처: 백엔드를 위한 Django REST Framework with 파이썬
1. 기능 구성
- 회원 관련 기능
- 회원 프로필 관리(닉네임, 관심사, 프로필 사진 등)
- 회원 가입 기능
- 로그인 기능
- 프로필 수정하기 기능
- 게시글 관련기능
- 게시글 생성
- 게시글 1개 가져오기
- 게시글 목록 가져오기(가져오는 개수 제한하기)
- 게시글 수정하기
- 게시글 삭제하기
- 게시글 좋아요 기능
- 게시글 필터링(좋아요 누른 글/내가 작성한 글)
- 게시글 각 기능마다 권한 설정
- 댓글 관련 기능
- 댓글 생성
- 댓글 1개 가져오기
- 댓글 목록 가져오기
- 댓글 수정하기
- 댓글 삭제하기
- 게시글을 가져올 때 댓글도 가져오게 만들기
2. Project Setting
가상환경 생성
python -m venv bbs cd bbs source ./bin/activate # ./Scripts/activate
DRF 프로젝트 생성
pip install django djangorestframework django-admin startproject board .
관리자 계정 생성
python manage.py createsuperuser
DRF 기본 설정
#//file: "board/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'
3. 모델 구성
User 모델
- Django 기본 User 모델
- 회원관리: 서비스를 위한 가장 기본적인 기능
- User: Django Framework에서 기본 제공하는 모델의 하나
- 회원관리를 위하여 User 모델을 새롭게 만들어도 되지만 현재 시점에서는 새로운 User 모델이 큰 의미가 없으므로 Django의 User 모델을 사용함
- django.contrib.auth.models 안에 정의되어 있음
- django.contrib.auth: 인증 기능을 위해 Django Framework가 미리 만들어둔 모듈
- settings.py에서 기본으로 등록되어 있음
- Django 기본 User 모델의 대표적인 필드
- username: 문자열
- 흔히 아는 ID가 들어가는 필드
- ID인만큼 다른 사용자와 겹치면 안됨
- 필수 항목
- first_name: 문자열
- 영문 이름에서 사용되는 이름의 개념
- 선택 항목
- last_name: 문자열
- 영문 이름에서 사용되는 성씨의 개념
- 선택 항목
- email: 문자열
- 회원 이메일 주소
- 선택 항목
- password: 문자열
- 비밀번호
- 필수 항목
- 실제로 입력한 비밀번호 문자열을 그대로 저장하지 않고 해시값을 저장함
- Django에서 비밀번호를 안전하게 보관하는 해시 알고리즘 사용(공식문서 참고할 것)
- 그외 기타 필드들
- username: 문자열
- Django 기본 User 모델
4. 회원관리 구현
4.1 회원 인증의 개념
- 회원 인증 개념 (1): ID/PW를 그대로 담아 보내기
- 가장 기본적인 인증 방법
- Django는 보안을 위하여 회원의 비밀번호를 해시값으로 저장하고 있지만
- 클라이언트의 입장에서는 그냥 비밀번호를 적어서 보내는 것일 뿐임
- 인증이 필요할 때마다 ID/PW를 전송해야 하며 중간에서 탈취당할 위험이 높음
- 보안이 매우 취약한 상태
- 회원 인증 개념 (2): 세션 & 쿠키의 사용
- 세션(Session): 서버쪽에 저장하는 정보
- 쿠키(Cookies): 클라이언트의 자체적인 저장소
- 쿠키는 데이터로 구성되긴 하지만 데이터라기보다는 데이터를 저장하는 임시저장소와 같은 역할을 수행함
- 한 번 로그인을 수행한 후, 로그인 정보를 이용하여 세션에서 발급하는 세션 ID를 보냄으로써 인증을 대체함
- 클라이언트에서는 세션 ID를 쿠키 저장소에 저장한 후 인증 요청이 있을때마다 세션 ID를 꺼내서 HTTP 헤더에 넣고 전송함
- 인증이 필요할 때마다 ID/PW를 전송할 필요가 없으므로 정보의 탈취 위험이 줄어들지만 세션 ID를 탈취당할 위험은 여전히 존재함
- 회원 인증 개념 (3): 토큰 & JWT
- 세션 & 쿠키 방식과 비슷함
- 회원 가입 시 서버는 유저에 매칭되는 토큰을 생성하여 클라이언트에 전달하고 클라이언트는 인증 요청 시마다 해당 토큰을 HTTP 헤더에 넣어서 전송함
- 토큰에는 사용자의 정보가 포함되어 있으므로 서버는 해당 정보를 이용하여 인증을 수행함
- 토큰은 암호화 방식이 적용되어 있으므로 전송 데이터 패킷을 중간에서 탈취당하더라도 정보가 노출될 가능성은 낮음
- 암호화에 사용되는 키: settings.py에 등록되어 있는 SECRET_KEY
- 그러나 토큰 자체를 탈취하여 사용자인척 하는 위험은 여전히 존재함
- 해결책: 토큰의 유효기간 설정
- 해결책: 토큰의 유효기간 설정
- JWT (JSON WEB Token)
- 사용자 인증을 위해 사용하는 Open Standard(RFC 7519)
- JSON 포맷을 이용하여 Self-Contained 방식으로 사용자에 대한 정보를 저장하는 Claim 기반의 WEB 토큰
- 기본 컨셉
- IdP (Identity Provider)가 사용자의 정보를 담은 내용에 서명하는 것을 통해서 토큰을 생성
- 유저가 서버에 요청할 때 이 토큰을 사용하며, 이 때 토큰의 무결성(integrity)과 인증성(authenticity)을 보장함
- 무결성: 정보가 원래의 내용으로 유지되는 것. 즉 정보가 변조되지 않았음을 보장하는 것
- 인증성: 보낸 사람과 받는 사람이 서로가 맞다고 확인할 수 있는 성질. 정보의 출처를 확인하고 신뢰할 수 있는지 판단하는 과정
- 유저가 전송하는 데이터를 숨기는 것보다 유저가 전송하는 데이터를 인증하는데 집중하는 방식
4.2 구현
users 앱 생성
python manage.py startapp users
설정 추가
#//file: "board/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', ] ... REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], }
- Model 구현
- Django User 모델에서 활용할 필드 목록
- username
- ID로 활용
- required=True
- email
- required=True
- password
- required=True
- username
- Django User 모델에서 활용할 필드 목록
- Serializer
- 회원가입 프로세스
- 사용자가 정해진 폼에 따라 데이터를 입력함
- username, email, password, password2
- 해당 데이터가 들어오면 ID가 중복되지 않는지, 비밀번호가 너무 짧거나 쉽지는 않은지 검사함
- 2단계를 통과했다면 회원을 생성함
- 회원 생성이 완료되면 해당 회원에 대한 토큰을 생성함
- 사용자가 정해진 폼에 따라 데이터를 입력함
- Serializer는 요청으로 들어온 데이터를 Django 데이터로 변환하여 저장하는 기능을 수행함
- 또한 Serializer는 검증(Validation) 기능을 수행하는 역할을 가지고 있음
이번 Serializer에서는 검증기능을 사용하도록 함
#//file: "users/serializers.py" from django.contrib.auth.models import User from django.contrib.auth.password_validation import validate_password 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): 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
- 회원가입 프로세스
- View 구현
- Serializer가 복잡해진 대신 View가 간단해짐
- 회원가입의 경우 회원 생성 기능만 있음(POST)
굳이 ViewSet을 사용해 다른 API 요청을 처리할 필요가 없음
#//file: "users/views.py" from django.contrib.auth.models import User from rest_framework import generics from .serializers import RegisterSerializer class RegisterView(generics.CreateAPIView): queryset = User.objects.all() serializer_class = RegisterSerializer
- URL 설정
클래스형 뷰를 사용할 것이므로 .as_view()를 사용함
#//file: "users/urls.py" from django.urls import path from .views import RegisterView urlpatterns = [ path('register/', RegisterView.as_view()), ]
#//file: "board/urls.py" from django.urls import path, include from django.contrib import admin urlpatterns = [ path('admin/', admin.site.urls), path('users/', include('users.urls')), ]
- Migration & Run Server
Migration
python manage.py makemigrations python manage.py migrate
Run Server
python manage.py runserver
기능 테스트 수행
- 관리자 계정을 통해 기능이 제대로 수행되었는지 확인
5. Login 구현
Serializer
#//file: "users/serializers.py" from django.contrib.auth import authenticate class LoginSerializer(serializers.Serializer): username = serializers.CharField(required=True) password = serializers.CharField(required=True, write_only=True) 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."})
View
#//file: "users/views.py" 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 return Response({"token": token.key}, status=status.HTTP_200_OK)
URL
#//file: "users/urls.py" from django.urls import path from .views import RegisterView, LoginView urlpatterns = [ path('register/', RegisterView.as_view()), path('login/', LoginView.as_view()), ]
실행
python manage.py runserver
6. User 모델의 확장
- 프로젝트에서 사용하는 회원 모델
- username: 아이디(CharField, primary=True)
- email: 이메일 주소(EmailField)
- password: 비밀번호(CharField)
- nickname: 닉네임(CharField)
- position: 직종(CharField)
- subjects: 관심사(CharField)
- image: 프로필 이미지(ImageField)
Profile 모델
#//file: "user/models.py" from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver # Create your models here. class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) nickname = models.CharField(max_length=128) position = models.CharField(max_length=128) subjects = models.CharField(max_length=128) image = models.ImageField(upload_to='profile/', default='default.png') @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, kwargs): if created: Profile.objects.create(user=instance)
Migration
python manage.py makemigrations python manage.py migrate python manage.py runserver
7. 사진 처리를 위한 설정 처리
라이브러리 설치
pip install Pillow
- 미디어 파일에 대한 경로 지정
- STATIC_URL: 정적 파일, 시스템에서 사용하는 리소스 파일의 저장 경로
- MEDIA_ROOT: 사용자가 업로드하는 파일의 저장경로(절대경로)
- MEDIA_URL: 사용자가 업로드하는 파일의 경로(읽기 위한 경로, 상대경로)
#//file: "board/settings.py" import os STATIC_URL = '/static/' MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - URL # myboard/urls.py from django.urls import path, include from django.contrib import admin from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('users/', include('users.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Serializer
#//file: "users/serializers.py" from .models import Profile class ProfileSerializer(serializers.ModelSerializer): class Meta: model = Profile fields = ("nickname", "position", "subjects") # extra_kwargs = {"image": {"required": False, "allow_null": True}}
- View + 기본 Permission
- 개발할 프로필 관련 기능
- 읽어오기, 수정하기 → generics.RetrieveUpdateAPIView를 이용하여 기능 구현 가능
- 요구되는 권한
- 프로필 조회: 모두
- 프로필 수정: 해당 프로필의 소유자만 가능 → permisstion_class 필드 설정을 통해 구현
- API마다 필요한 권한이 다른 경우
- 권한이 미리 조합된 클래스 활용
- 직접 권한 클래스를 만들어서 설정
- Django Rest Framework에서 제공하는 권한 종류의 예시
- AllowAny: 모든 요청을 통과시킴. 어떤 인증도 불필요함
- IsAuthenticated: 인증된 경우에만 통과시킴. 즉 우리가 선언한 인증 방법으로 인증을 통과한 요청만 가능한 권한
- IsAdminUser: 관리자인 경우에만 통과
#//file: "users/views.py" from .serializers import RegisterSerializer, LoginSerializer, ProfileSerializer from .models import Profile from rest_framework import generics class ProfileView(generics.RetrieveUpdateAPIView): queryset = Profile.objects.all() serializer_class = ProfileSerializer
#//file: "users/permissions.py" from rest_framework import permissions class CustomReadOnly(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True return obj.author == request.user
#//file: "users/urls.py" from django.urls import path from .views import RegisterView, LoginView, ProfileView urlpatterns = [ path('register/', RegisterView.as_view()), path('login/', LoginView.as_view()), path('profile/<int:pk>/', ProfileView.as_view()) ]
- 개발할 프로필 관련 기능
- Admin 페이지 등록
- User 모델만 관리자 페이지에 등록하게 되면 프로필 모델은 나타나지 않음
- 프로필 모델을 따로 등록하면 관리자 페이지에서는 볼 수 있지만 유저 테이블과 프로필 테이블이 분리되어 있으므로 불편함
- 아래와 같은 방법으로 두 모델이 같은 모델인 것처럼 함께 볼 수 있음
#//file: "user/admin.py" from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User from .models import Profile class ProfileInline(admin.StackedInline): model = Profile can_delete = False verbose_name_plural = "profile" class UserAdmin(BaseUserAdmin): inlines = (ProfileInline, ) admin.site.unregister(User) admin.site.register(User, UserAdmin)
Migration
python manage.py makemigrations python manage.py migrate python manage.py runserver