나중에 서버를 구성해야 할 경우를 대비해서 DRF를 사용한 로그인, 회원가입 기능을 구현해보자.
[백엔드를 위한 Django REST Framework with 파이썬] 책을 따라해보며 글을 작성해보았다.
대표적인 회원 관련 기능
- 회원 프로필 관리 (닉네임, 관심사, 프로필 사진 등)
- 회원가입 기능
- 로그인 기능
- 프로필 수정하기 기능
먼저 장고 프로젝트를 생성해주자
가상환경을 활성화 하고, myboard 라는 이름의 장고 프로젝트를 생성해주었다.
settings.py에 DRF 앱을 등록해주고, TIME_ZONE 설정을 해준다.
[ Django 기본 User 모델 ]
장고에서는 기본적으로 구현된 User 모델이 있기 때문에 회원 모델을 별도로 만들어줄 필요는 없다.
admin 유저 생성 시 만들어지는 superuser도 장고의 기본 User모델로 만들어지는 것이다.
$ python manage.py createsuperuser
장고의 기본 User 모델은 django.contrib.auth.models 안에 있다.
django.contrib.auth는 settings.py에 등록된 기본 앱 중 하나이며, 장고에서 인증을 위해 만들어져있다.
나중에 User모델에 접근하고 싶다면 아래와 같이 불러오면 된다.
from django.contrib.auth.models import User
기본 User 모델 필드
기본 User모델에는 총 12개의 필드가 선언 되어있다.
대표적으로 아래와 같은 필드가 있고, 다른 필드들도 보고 싶다면 해당 링크를 참고하자 ( https://docs.djangoproject.com/en/4.1/ref/contrib/auth/ )
필드명 | 타입 | 설명 | 필수 여부 |
username | 문자열 | - 로그인 시 입력할 ID - 다른 사용자와 겹치면 안됨 |
O |
first_name | 문자열 | 이름 | X |
last_name | 문자열 | 성 | X |
문자열 | 이메일 | X | |
password | 문자열 | 비밀번호 | O |
이 중 password 필드는 입력된 비밀번호 문자열을 그대로 저장하지 않고, 보안을 위해 해시값으로 저장한다.
[ 회원 인증 방식 - 토큰&JWT ]
구현하게 될 회원 인증 방식은 토큰 방식이다.
기본적인 토큰 방식은 다음과 같다.
- 회원가입 시 유저에 매칭되는 토큰을 생성하여 저장
- 로그인 요청이 들어오면 해당하는 토큰을 응답으로 보내줌
- 클라이언트는 로그인 응답으로 받은 토큰을 가지고 있다가, 서버에 요청을 보낼 때 헤더에 토큰을 넣어 보내줌
토큰 자체에 사용자에 대한 정보가 있기 때문에 서버는 토큰만 가지고 어떤 사용자인지 구분해낼 수 있다는 점에서 토큰 방식은 세션&쿠키 방식보다 더 편리하고 효율적이라고 볼 수 있다.
토큰은 기본적으로 암호화 방식을 채택하여 사용하고, 이 암호화에 필요한 키는 settings.py에 정의되어 있는 SECRET_KEY이다. 따라서 이 SECRET_KEY만 노출되지 않는다면 토큰 자체로는 어떤 정보도 얻어낼 수 없기 때문에, 누군가 토큰을 탈취하여 유저에 대한 정보를 빼가는 상황을 막을 수 있다.
그렇기 때문에 깃허브 등에 배포 시 .gitignore에 SECRET_KEY값도 따로 빼주어야 한다.
SECRET_KEY까지 노출되었을 경우에 대한 해결책으로 토큰에 대해 유효기간을 설정해 줄 수 있다. 유효기간을 설정해 주면 누군가 토큰을 탈취해도 몇 분 뒤에는 쓸 수 없는 의미 없는 값이 되기 때문에 안전하다.
유효기간 설정 등은 여러 패키지를 통해 가능하지만 이번 구현에서는 유효기간을 따로 설정하지는 않는다.
토큰 인증 방식의 프로세스이다.
에이블스쿨 빅프로젝트 웹 개발 시에는 해당 방식을 통해 받아온 토큰을 브라우저의 localStorage 혹은 sessionStorage에 저장해주었다. 그리고 이후 서버에 요청을 보낼 때는 로컬or세션 스토리지에 저장한 토큰 값을 헤더에 넣어주었다.
[ 회원가입 구현하기 ]
앱 생성
회원가입, 로그인 등의 기능을 구현하기 위해 users라는 이름의 앱을 생성해준다.
$ python manage.py startapp users
앱 등록
그 다음 settings.py에 users 앱을 등록해준다.
기본 토큰 인증 방식을 사용하기 위해 rest_framework.authtoken 앱도 추가해주도록 하자.
프로젝트의 인증 방식으로 토큰 방식을 사용한다는 것을 정의해주기 위해 settings.py의 적당한 위치에 REST_FRAMEWORK라는 옵션을 만들어서 다음과 같이 작성해준다.
이제 모델 >> 시리얼라이저 >> 뷰 >> URL 순서로 기능을 개발해보자.
회원가입 프로세스는 다음과 같다.
- 사용자가 정해진 폼에 맞춰 데이터를 입력 (username, email, password, password2)
- 해당 데이터가 들어오면 ID 중복 여부, 비밀번호의 길이와 형식 등을 검사
- 회원 생성
- 생성된 회원에 대한 토큰 생성
1. 모델
장고의 기본 User모델을 사용할 것이기 때문에 따로 모델을 만들 필요 없다.
따라서 users/models.py에는 따로 내용을 작성할 것은 없는데, 아래의 필드들을 다음과 같이 사용한다는 것을 미리 정의하고 넘어가겠다.
[ 활용할 필드 목록 ]
- username : ID로 사용, required=True
- email : required=True
- password : required=True
사용자는 회원가입 시에 위의 내용들을 필수로 입력해주어야 한다. 여기에 추가적으로 password2를 입력받도록 함으로써 password와 비교하여 비밀번호 확인 과정도 거치도록 해 줄 것이다.
2. 시리얼라이저
시리얼라이저는 서버로부터 응답으로 받은 Django 데이터를 json으로 변환해서 클라이언트에 전달해주거나, 클라이언트가 json 형식으로 서버로 보낸 요청을 Django 데이터로 변환해서 서버에 전달해준다.
그 밖에도 시리얼라이저는 검증(Validation) 기능도 수행 가능하다. 따라서 사용자의 회원가입 요청이 적절한지 검증하는 과정을 시리얼라이저에 작성해보도록 하겠다.
users 앱 폴더 내에 serializers.py 파일을 생성하고, 다음과 같이 작성해주었다.
회원가입 과정에 대한 기능은 시리얼라이저에 대부분 구현해주었다.
# users/serializers.py
from django.contrib.auth.models import User # User 모델
from django.contrib.auth.password_validation import validate_password # Django의 기본 pw 검증 도구
from rest_framework import serializers
from rest_framework.authtoken.models import Token # 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', 'email', 'password', 'password2')
def validate(self, data): # password과 password2의 일치 여부 확인
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
3. 뷰
회원가입 대부분의 기능을 시리얼라이저에 구현했기 때문에 views.py에는 많이 적을 내용은 없다.
회원가입은 POST(회원 생성) 기능 1개만 있기 때문에 굳이 ViewSet을 사용해 다른 API 요청을 처리해 줄 필요가 없다. 그렇기 때문에 generics의 CreateAPIView를 사용해 회원가입 기능을 작성해주었다.
# 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
4. URL
회원가입 API의 URL을 register/ 로 등록해준다.
회원가입 View는 클래스형 뷰로 만들었기 때문에 .as_view()로 urls.py에 등록해준다.
# 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')),
]
5. 마이그레이션
이제 마이그레이션을 해 준 다음, 프로젝트를 실행해보자.
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver
회원가입 API URL인 localhost:8000/users/register/ 로 접속해보면 아래와 같은 화면이 나온다.
회원가입 시 입력받도록 지정해준 4개의 필드에 대한 폼이 구성되어 있고, POST 요청을 보낼 수 있다.
이제 Postman으로 회원가입 API를 테스트해보자.
간단한 비밀번호 입력 시에는 장고의 ValidatePassword가 회원가입을 거부한다.
그래서 더 어려운 비밀번호로 바꿔주면 회원가입이 정상적으로 이루어지는 것을 볼 수 있다.
이제 가입된 회원에 대해 토큰이 잘 생성되었는지 확인해보기 위해 관리자 계정을 만들어서 관리자 페이지로 접속해보겠다.
$ python manage.py createsuperuser
http://localhost:8000/admin/ 를 통해 관리자 페이지로 접속하면 Tokens 라는 모델이 있다.
Tokens 모델을 눌러서 확인해보면 가입한 유저의 토큰이 생성된 것을 볼 수 있다.
[ 로그인 구현하기 ]
로그인은 모델과 아예 관련이 없다고 봐도 된다.
여기서 정의한 로그인 프로세스에서는 사용자가 ID/PW를 적어서 요청하면, 서버에서는 이를 확인 후 해당하는 토큰만 응답해주면 된다.
1. 시리얼라이저
# users/serializers.py
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=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."}
)
2. 뷰
특별한 제네릭을 따로 사용하지 않고, 기본 GenericAPIView를 사용하여 간단히 구현하였다.
로그인은 POST 요청으로 처리하고, 시리얼라이저를 통과하여 얻어온 토큰을 그대로 응답해 주는 방식으로 구현해준다. 검증 과정은 시리얼라이저에서 모두 처리되기 때문에 이번에도 views.py에 작성할 내용은 많지 않다.
# 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 # validate()의 리턴값인 token을 받아온다.
return Response({"token": token.key}, status=status.HTTP_200_OK)
3. URL
회원가입과 동일하게 URL 연결해준다. ( login/ )
# users/urls.py
from django.urls import path
from .views import RegisterView, LoginView
urlpatterns = [
path('register/', RegisterView.as_view()),
path('login/', LoginView.as_view()),
]
Postman을 통해 로그인 요청을 테스트해보자.
로그인에 성공하면서 토큰을 응답받는 것을 확인해볼 수 있다.
잘못된 비밀번호 입력 시에는 에러 메세지와 함께 로그인 되지 않는 것도 확인해볼 수 있다.
[ User 모델 확장하기 - 프로필 조회, 수정 구현 ]
장고의 기본 유저 모델은 기본적인 기능들을 제공해주기 때문에 편리하게 사용 가능하다. 하지만 회원의 프로필 사진이나, 회원의 닉네임 등이 필요할 수도 있고, 이메일을 로그인 시 ID로 활용하고 싶을 수도 있다.
사실 장고의 기본 유저 모델만 사용해서 회원가입 기능을 구현할 일은 극히 드물 것이다.
다른 필드들을 추가하기 위해서 User 모델을 확장할 필요가 있다.
기존의 회원 모델에 더해 다음의 필드들을 추가해보자
- username : 아이디
- email : 이메일
- password : 비밀번호
- nickname : 닉네임
- position : 직종
- subjects : 관심사
- image : 프로필 이미지
User 모델을 확장할 수 있는 방법은 총 4가지가 있다.
- Proxy Model
- 1:1 (One-to-One) Model - 사용
- AbstractBaseUser
- AbstractUser
Proxy Model은 기본 User 모델을 그대로 상속받아 기능을 추가하거나 동작을 변경하기 위해 사용하는 방법이다. 가장 간단한 방법이지만 기존 User모델의 스키마를 변경하지 않기 때문에 정작 우리에게 필요한 필드 추가는 할 수 없다.
1:1 Model은 기본 User 모델에 1대1로 연결되는 새로운 모델을 하나 만들어 주는 방법이다. DB의 1대1 관계 개념을 활용하는 것으로 User 모델을 직접 건들지 않으면서 필드를 추가할 수 있는 방법이다. 여기서는 이 방법을 채택하여 사용해줄 예정이다.
좋은 방법이긴 하지만 두 개의 모델을 연결하여 사용하기 때문에 모델 1개를 사용하는 것보다 느릴 수밖에 없다. 어렵진 않지만 효율적인 방법도 아니긴 하다.
AbstractBaseUser은 가장 정석적인 방법 중 하나이다. User모델을 추상화시킨 AbstractBaseUser 모델을 상속받아와서 아예 새로운 User 모델을 만드는 방식이다. 밑에서 설명할 AbstractUser와 다른 점은 AbstractBaseUser은 말 그대로 기본 요소만 있는 버전이기 때문에 많은 내용을 우리가 일일이 구현해야 한다.
즉, 가장 자유도가 높지만 어려운 방식이다.
AbstractUser는 AbstractBaseUser보다 어느 정도 현실에 타협한 방식이다. AbstractBaseUser는 많은 내용이 이미 구현된 AbstractUser를 상속받아 새로운 유저 모델을 만드는 방식으로, 사실상 기본 User 모델을 그대로 가져와 필요한 내용만 수정하거나 추가할 수 있는 방식이라고 볼 수 있다.
가장 많이 쓰이고, 편리한 방법이다.
1. Profile 모델로 User 확장하기
쉽고 직관적인 1:1 Model을 통해 구현해보도록 하자.
Profile이라는 모델을 만들어주고, One-To-One으로 User 모델과 연결해준다.
# users/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
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
# primary_key를 User의 pk로 설정하여 통합적으로 관리
nickname = models.CharField(max_length=50)
position = models.CharField(max_length=50)
subjects = models.CharField(max_length=50)
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)
@receiver 에서는 User 모델이 post_save 이벤트를 발생시켰을 때 해당 이벤트가 일어났다는 사실을 받아서, 해당 유저 인스턴스와 연결되는 Profile 데이터를 생성해준다. 이 덕분에 프로필을 생성해주는 코드를 직접 작성하지 않아도 알아서 User 생성 이벤트를 감지해 프로필을 자동으로 생성할 수 있다.
마이그레이션을 하기 전에 image 필드의 이미지가 저장될 MEDIA 파일 경로를 지정해주어야 한다.
settings.py 파일에 아래와 같이 작성해준다. /media/ 디렉토리를 미디어 파일들의 루트로 설정하여 이후 미디어 파일들에 대한 경로를 지정할 때 상대 경로로 간편하게 작성할 수 있다.
아래와 같이 프로젝트 루트 폴더에 media 폴더를 생성해주고, 그 안에 Profile 모델의 image 필드 이미지 파일이 저장될 경로로 profile 폴더를 만들어준다.
사진을 선택하지 않을 때 디폴트 이미지로 사용해줄 default.png 파일도 media 폴더 안에 넣어준다.
마지막으로 미디어 파일 경로를 myboard/urls.py에 매칭시켜준다.
# myboard/urls.py
from django.contrib import admin
from django.urls import path, include
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)
2. 시리얼라이저
이제 users/serializers.py에 다음과 같이 작성해준다.
# users/serializers.py
from .models import Profile
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ("nickname", "position", "subjects", "image")
3. 뷰 + 기본 퍼미션
프로필 관련해서 구현되어야 할 기능은 프로필 불러오기와 프로필 수정 2가지이다.
따라서 불러오기와 수정 2개의 기능이 구현된 generics.RetrieveUpdateAPIView을 사용하여 코드를 간단하게 작성해줄 수 있다.
# users/views.py
from .serializers import RegisterSerializer, LoginSerializer, ProfileSerializer
from .models import Profile
from .permissions import CustomReadOnly
class ProfileView(generics.RetrieveUpdateAPIView):
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
permission_classes = [CustomReadOnly]
하지만 이렇게 작성하면 프로필 수정이 누구나 가능해진다. 프로필 조회는 누구나 할 수 있지만, 프로필 수정은 프로필의 주인(로그인한 유저)만 할 수 있어야 한다.
이렇게 어떤 API에 특정한 권한이 필요한 상황은 permission_class 필드를 설정해 주는 것으로 구현할 수 있다.
안전한 메소드(GET) 외의 요청에는 프로필을 수정하려는 해당 유저만 허가해주도록 권한을 만들어보자.
4. 커스텀 권한 만들기
users 앱 디렉토리에 permissions.py 파일을 생성해주고, 아래처럼 내용을 작성해준다.
# users/permissions.py
from rest_framework import permissions
class CustomReadOnly(permissions.BasePermission):
# GET : 누구나 / PUT,PATCH : 해당 유저만
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: # 안전한 메소드(GET)면
return True
return obj.user == request.user
커스텀 권한 클래스는 permissions.BasePermission을 상속받아와서 작성한다.
조회, 수정 시에 모두 프로필 전체에 대한 요청은 없고 각 객체에 대한 요청만 있으므로 has_object_permission 메소드를 가져와서 작성해준다.
permissions.SAFE_METHODS는 데이터에 영향을 미치지 않는 메소드를 의미한다. 이런 요청은 True로 반환하여 통과시키고, PUT/PATCH 등의 요청에는 요청으로 들어온 user와 객체의 user를 비교해서 같으면 True를 반환하여 통과시켜준다.
5. URL
url은 다음과 같이 세팅해준다.
# 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()),
]
6. 어드민 페이지에서 User + Profile 모델 같이 보도록 설정해주기
Profile 모델을 admin에 등록하면 관리자 페이지에서 확인은 가능하지만, User 과 따로 떨어져 있다 보니 불편해진다.
User와 Profile 두 모델이 같은 모델인 것처럼 함께 보기 위해 admin.py에 다음과 같이 작성해주도록 하자.
# users/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)
이렇게 관리자 페이지에서 Users 모델 내부에 Profile 내용이 같이 조회되는 것을 볼 수 있다.
7. 마이그레이션 및 테스트
이제 마이그레이션을 해주고, 기능을 테스트해보자.
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver
유저를 더 생성해주고 확인해보면, 따로 토큰 없이 GET 요청만 보내도 프로필 조회가 가능한 것을 볼 수 있다.
하지만 프로필 수정 같은 경우는 해당 사용자의 토큰이 있어야 가능하다.
pk 3번 계정에 대해 pk 1번 계정의 토큰을 넣어주고 다음과 같이 닉네임을 수정해주는 요청을 보내보았다.
아래처럼 프로필 수정에 실패하는 것을 볼 수 있다.
하지만 pk 1번 계정에 대해 요청을 보내게 되면
수정이 되는 것을 볼 수 있다.
'Django' 카테고리의 다른 글
[ Django ] MAC OS에 Django 설치하기 (0) | 2023.01.13 |
---|---|
[ Django ] 장고에서 API KEY 숨기기 (secrets.json) (0) | 2023.01.03 |