이번에는 DRF의 JWT(JSON Web Token)의 인증방식을 통해 로그인과 회원가입 , (참조URL)
user의 모델정보에 profile을 확장해서 추가하는법 , (참조URL)
profile을 확장하는 중 ImageField사용법과 front에서 FormData()의 활용 , (참조URL)
전체적인 backend의 세팅이 끝났다면 front로 가져오는 useEffect의 활용 , (참조URL)
또는 backend로 데이터를 보내는법 fech의 활용 , (참조URL)
그리고 선택사항으로 oauth의 react-google-login을 활용한 소셜로그인까지 (참조URL)
전체적인 코드를 많이 참고한 git의 (참조URL)
1장에서 생성한 리액트폴더인 frontend와 나란하게 우리는 backend라는 폴더를 만들자
그 후 안에 python가상환경을 실행할건데(python이 깔려있다는 전제)
python3 -m venv env(가상환경이름)
(가상환경이름)env\Scripts\activate
을 통한 가상환경 설치 및 실행까지 시켰다면 Django를 설치하고 필요한 프레임워크, 라이브러리를 빠르게 설치해보겠다.
python -m pip install --upgrade pip
pip install django
pip install djangorestframework
pip install djangorestframework-jwt
pip install django-cors-headers
pip install Pillow
각각 토큰발행을 위한 DRF의 djangorestframework-jwt,
리액트의 http://localhost:3000/ Django의 http://localhost:8000/ 각각의 다른 서버를 연결하는 django-cors-headers,
ImageField를 쓰기위한 Pillow 까지
설치를 마쳤다면 startproject ,startapp 을 통해 api와 api로 호출시킬 app을 각각 만들겠다.
django-admin startproject api .
python manage.py startapp user
그리고 따로 DB를 설정하지 않았기 때문에 sqlite파일을 생성하도록 아래 명령어를 실행하겠다.
python manage.py migrate
여기까지 잘 따라왔다면 backend의 폴더 구성은 아래와 같다 !
이제 추가해준 항목들을 api/setting.py 파일을 사용할 수 있게 수정과 추가를 해보겠다.
"""
Django settings for api project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
from pathlib import Path
import datetime #56에서 시작되는 JWT_AUTH 의 토큰 유효기간을 설정하기 위한 datetime import
import os # 아래에 작성한 image가 추가될 때 경로를 설정해주기 위한 os import
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') #개발자가 관리하는 파일들
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') #사용자가 업로드한 파일 관리
# Build paths inside the project like this: BASE_DIR / 'subdir'.
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xt!de@_*w0!y*@x!bqsb6%zr*tmhh+z2qjoaqwru4vq1@-d-fa'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
APPEND_SLASH = False # 추가 안해줄시 기본값이 True인데 그 경우 urls.py에서 경로설정시 주소 끝에 /를 붙이고
#해당경로로 /를 붙이지 않고 접속시 페이지를 찾을 수 없기때문에 리다이렉트를 시켜 자동으로 /를 붙여서 경로를 찾는다.
#이 경우 문제가 될 수 있기때문에 false로 값을 지정해줬다.
CORS_ORIGIN_WHITELIST = ['http://localhost:3000'] #아까 설치한 corsheaders로 해당 서버와 연결할 서버의 url을 작성해준모습
# Application definition
REST_FRAMEWORK = { # 추가
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', #인증된 회원만 액세스 허용
'rest_framework.permissions.AllowAny', #모든 회원 액세스 허용
],
'DEFAULT_AUTHENTICATION_CLASSES': ( #api가 실행됬을 때 인증할 클래스를 정의해주는데 우리는 JWT를 쓰기로 했으니
'rest_framework_jwt.authentication.JSONWebTokenAuthentication', #이와 같이 추가해준 모습이다.
),
}
JWT_AUTH = { # 추가
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY_EXPIRATION' : True, #토큰검증
'JWT_ALLOW_REFRESH': True, #유효기간이 지나면 새로운 토큰반환의 refresh
'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=30), # Access Token의 만료 시간
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=3), # Refresh Token의 만료 시간
'JWT_RESPONSE_PAYLOAD_HANDLER': 'api.custom_responses.my_jwt_response_handler'
}
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user', # 추가
'rest_framework', # 추가
'rest_framework_jwt', # 추가
'corsheaders', # 추가
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 추가
'django.middleware.common.CommonMiddleware', # 추가
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'api.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'api.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Seoul' # 후에 작성한 게시물에 날짜를 표현하기 위해서
USE_TZ = False #Use_TZ를 false로 설정해서 우리나라 시간을 가져온 모습이다.
USE_I18N = True
USE_L10N = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
중간중간 추가해준 사항에 대한 주석을 달아놨으니 해당사항은 주석을 통해 확인하면 좋겠다.
여기까지 완료하고 runserver를 해주어 문제없이 실행되는 모습이 나와야만 얼추 setting에 관한 추가사항은 끝이라고해도 무방하다 !
python manage.py runserver
문제없이 실행되어 서버에 방문한 모습
이제 우리는 아까 만든 app인 user를 통해 token을 발급받는 user의 모델과 그 모델인 user의 profile을 확장하는 모델을 backend/user/models.py를 통해 정의해보도록 하겠다.
from django.db import models
# Create your models here.
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)
user_pk = models.IntegerField(blank=True)
email = models.EmailField(max_length=500, blank=True)
mygit = models.CharField(max_length=50, blank=True)
nickname = models.CharField(max_length=200, blank=True)
photo = models.ImageField(upload_to="profile/image", default='red.jpg')
myInfo = models.CharField(max_length=150, blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance, user_pk=instance.id)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.profile.save()
조금만 보아도 우리는 user의 profile을 설정하는 class Profile을 볼 수는 있지만 user의 모델을 username, password 등으로 정의한 model은 찾아볼 수 없다 !
이는 user의 모델의 경우
from django.contrib.auth.models import User
즉 django의 내장 auth model로 부터 정의된 User를 Import 시켰기 때문에 따로 작성안해준 이유이다 !
그리고 User를 Ctrl + 마우스클릭 을통해 User의 정의를 들어가보면
class User(AbstractUser):
Class User를 확인할 수 있는데 여기서 또 Abstract를 확인할 수 있다. 전에 Java에서 상속개념을 배울 때 추상의 Abstract를 배웠는데
추상 클래스는 상속을 강제하기 위한 것이다. 즉 부모 클래스에는 메소드의 시그니처만 정의해놓고 그 메소드의 실제 동작 방법은 이 메소드를 상속 받은 하위 클래스의 책임으로 위임하고 있다.
-생활코딩-
즉 User를 정의해놨지만 이를 상속받을 user Class를 니 맛대로 다양하게 활용은 니가해라 ~
라는 개념으로 이해하면 편할 듯 하다.
모델을 정의할 때 쓰는 Field의 경우 많은 종류가 있는데 이는 이곳에서 참조하고 더 필요한 경우 구글링을 권장한다.
특이점의 경우 OneToOneField가 있는데 이처럼 1대1 방식이 있는 반면 다른 다양한 방식도 존재하더라.
또 ImageField의 경우 default='red.jpg' 으로 설정해준것은 user를 생성할 때 profile에 기본이미지로 red.jpg를 선택한 것 이고 해당 이미지의 경로는 backend/media/red.jpg로 이미지를 해당경로에 위치시킨다면 user를 생성할 때 기본이미지가 적용되는 것을 확인 할 수 있을 것이다.
(이미지를 back에서 업로드 또는 front에서 업로드 해주면 media폴더가 자동으로 생성되는데 아직 아무것도 안했기때문에 직접 생성해 주었다.)
이제 모델을 정의했으면 우리는 DB에 table을 만들고 적용을 시켜야한다.
이 때 makemigrations, migrate가 등장한다.
python manage.py makemigrations
python manage.py migrate
정확한 표현은 아니지만 table을 만드는 과정을 makemigrations, 만든것을 적용하는 과정을 migrate 라고 이해하면 편할 듯 하다.
정확한 의미가 궁금하다면 늘 검색해보길 권장한다.
명령어를 입력하고나면 폴더의 구성은 아래와 같다
따로 만들지 않아도 user/migrations/0001_initial.py 가 생성된 것을 확인할 수 있으며 model정의시 기본키를 따로 주지 않았기때문에 id가 추가된것을 확인 할 수 있다!
항상 모델을 설정한 뒤 즉시 makemigrations, migrate를 해주는게 정신건강에 이로울 것이다
여기까지 하고 admin.py를 설정해주면 우리가 지금껏 한 것들을 눈으로 확인 할 수 있다!!
from django.contrib import admin
# Register your models here.
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)
이렇게만 설정해도 우리는 backend/api/urls.py의 기본적으로 amdin페이지의 url을 정의해 놓은
urlpatterns = [
path('admin/', admin.site.urls),
]
덕분에 http://localhost:8000/admin/ 접속시 관리자페이지로 접속할 수 있다 !
username과 password는
python manage.py createsuperuser
라는 실행명령어를 통해 username, email , password를 정하고 superuser를 생성할 수 있다
이제 다시
python manage.py runserver
을통해 서버를 실행시키고 admin페이지로 이동한 뒤 설정한 superuser의 username과 password를 입력하면 우리는 관리자같은 아래와 같은 페이지로 접속이 가능하다 !
admin 접속시 우리가 생성한 superuser의 1대1방식으로 생성한 profile까지 함께 볼 수 있는 수정도 가능한 db의 workbench같은 GUI를 확인할 수 있다.
이제 우리는 back과 front를 연결시킬 준비를 해야한다. 때문에 아직 세팅해줘야 할 파일이 더 남았으니
이렇게 생성된 model을 front와 주고받을 때 서로 다른 언어를 쓰기때문에 서로가 이해할 수 있는 언어로 변환을 해줘야한다. 이 때 쓰이는것이 바로 serializers.py 사전적의미로는 직렬변환기이다.
그리고 그것이 해당하는 페이지에서 어떤식으로 보여지는 views.py 마지막으로 views의 url을 정의하는것이 바로 urls.py
여기까지하면 기본적인 django가 front와 주고받을 준비를 마친것이라고 봐도 무방하다 !
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
from django.contrib.auth.models import User
from .models import Profile
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'email', 'id')
class UserSerializerWithToken(serializers.ModelSerializer):
token = serializers.SerializerMethodField()
password = serializers.CharField(write_only=True)
def get_token(self, obj):
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(obj)
token = jwt_encode_handler(payload)
return token
def create(self, validated_data):
password = validated_data.pop('password', None)
instance = self.Meta.model(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
class Meta:
model = User
fields = ('token', 'username', 'first_name', 'last_name', 'email', 'password')
#profile
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('id', 'user_pk', 'email', 'nickname', 'user', 'photo', 'mygit', 'myInfo')
따로 이코드에 대해서는 공부하지 않았는데 좀 읽다보면 대략적인 그림은 get_token할 때 jwt이벤트 헨들러같은것을 정의해준거같고 매개변수로는 front에서 get_token할 수 있도록 self에는 get할 수 있는 해당접속url의 경로, obj는 토큰을 얻을 유저의 정보(username이나 password)라고 분석했다. 또 create_token을 할 때 validated_data는 username이나 password를 통해 확인된 user에게 token을 만들어 줘야하기때문에 validated_data라는 객체를 매개변수로 받는다는 듯 했다.
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib.auth.models import User
from rest_framework import permissions, status, generics
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer, UserSerializerWithToken, ProfileSerializer
from .models import Profile
# from google.oauth2 import id_token
# from google.auth.transport import requests
@api_view(['GET'])
def current_user(request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
class UserList(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request, format=None):
serializer = UserSerializerWithToken(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ProfileUpdateAPI(generics.UpdateAPIView):
lookup_field = "user_pk"
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
여지껏 많은 DRF의 import가 있었지만 views.py가 압도적으로 DRF의 혜택을 많이 받는 녀석이라는 사실이 코드에서 드러난다.
from rest_framework import permissions, status, generics
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
등등 만약 DRF가 없었다면 우리는 create, get, post, delete 등등 serializer부터 쭉 하나하나 class, method들을 정의해줬어야 할 텐데 엄청난 특권을 누리고있다는 생각이 새삼 든다..
아직 back과 front사이에서 확실한 선택을 못했지만 back으로 전향하게된다면 나는 DRF없이 고생해서 작성하는 serializer와 view들을 작성해보는 경험(사서고생하는)들을 충분히 해보고싶다는 생각도 있다 !
from django.urls import path
from .views import current_user, UserList, ProfileUpdateAPI
urlpatterns = [
path('', UserList.as_view()),
path('current/', current_user),
path("auth/profile/<int:user_pk>/update/", ProfileUpdateAPI.as_view()),
]
from django.contrib import admin
from django.urls import path, include
from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token
from .views import validate_jwt_token
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path('validate/', validate_jwt_token),
path('login/', obtain_jwt_token),
path('verify/', verify_jwt_token),
path('refresh/', refresh_jwt_token),
path('user/', include('user.urls')),
]
urlpatterns += \
static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
과거에는 path경로를 쓰지않고 api/urls.py에 url을 직접 전부 입력하는 방식을 쓰더라. 지금은 path에서 include('user.urls')을 통해 user라는 app안에서 urls.py를 다시 작성함으로써 user가아닌 다른 app을 생성할 시 api/urls.py를 훨씬 직관적으로 볼 수 있다는 사실을 확인할 수 있다 !
url 코드가 궁금하다면 찾아보길 바란다 !
아 그리고 작성완료시 터미널창에서
ImportError: Could not import 'api.custom_responses.my_jwt_response_handler' for API setting 'JWT_RESPONSE_PAYLOAD_HANDLER'. ModuleNotFoundError: No module named 'api.custom_responses'.
와같은 에러를 뿜어내는데 여기서
from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view
# from ./user/serializers import UserSerializer
@api_view(['GET'])
def validate_jwt_token(request):
try:
token = request.META['HTTP_AUTHORIZATION']
data = {'token': token.split()[1]}
valid_data = VerifyJSONWebTokenSerializer().validate(data)
except Exception as e:
return Response(e)
return Response(status=status.HTTP_200_OK)
from user.serializers import UserSerializer
def my_jwt_response_handler(token, user=None, request=None):
return {
'token': token,
'user': UserSerializer(user, context={'request': request}).data
}
위 코드들을 추가해준다면 아까 터미널에서 No module named 'api.custom_responses' 하던 에러가 사라지는것을 볼 수 있다.(정확히 따라했는데도 에러가난다면 가끔 적용이 느리게 될 때도 있으니 우선 서버를 종료시켰다가 다시 실행시켜보는 것을 권장한다!)
서버가 에러없이 잘 실행된다면 back에서 로그인과 회원가입을 할 준비를 완벽하게 끝냈다.
마지막으로 우리가 지정해준 url로 들어가보자.
당황하지말자. 우리는 http://localhost:8000 , api/urls.py에서 ""빈문자열에 해당하는 url을 정의한적이 없기때문에 페이지를 찾을 수 없다는 404페이지가 당연하다 !
아래 api/urls.py에서 정의한 url 패턴을 보여주는데 그중 우리는 user라는 페이지에 접속해보자.
user list를 볼 수 있는 /user/페이지에 들어왔지만 유저목록이 보이지않는다. 빨간색으로 된 줄을 읽어보면
"detail": "Method \"GET\" not allowed."
즉 user list들을 볼 수있는 get메서드가 허용되지 않는다는 내용이다.
Allow: POST
허용된 메서드는 POST, 즉 유저리스트에 유저를 추가시키는 것은 허용된다는 소리다 !
어찌보면 당연한 소리다. 관리자를 제외하고는 user list를 볼 수 없는게 맞기때문에 아예 get 메서드를 허용하지 않았고 이는 우리가 아까 작성한 djnago의 AbstractUser, 거기에 token을 발급, 소멸, 재발급 의 기능을 가진 DRF jwt의 기능을 가져다 쓰면서 이런 설정이 가능하게 된것이다.
그럼 우리가 만든 프로필은 볼 수 있을까 ? 프로필에 해당하는 url로 접속해보겠다 !
superuser에 해당하는 user_pk가 1임을 우리는 admin페이지에서 확인할 수 있었고 user_pk에 해당하는 1을 넣어서 접속해 보았다.
superuser에 해당하는 user_pk가 1임을 우리는 admin페이지에서 확인할 수 있었고 user_pk에 해당하는 1을 넣어서 접속해 보았다.
profile을 보려고 접속했지만 권한을 확인할 수 없다는 문구가 나온다.
superuser의 프로필이라서 안되는 것인가 ? 그렇다면 amdin으로 접속해서 user를 add해서 test1이라는 일반유저를 생성해 보고 user_pk가 id값과 일치하는 2를 확인하고 해당경로로 다시 접속해보겠다.
역시 권한이 없다고 나오는데 jwt를 사용할때 우리는 토큰을 인증받아야 자신의 user정보를 열람할 수 있는 권한을 부여한다고 설정했었다. 그래서 토큰이 없는 지금은 어떤 유저의 정보도 열람할 수 없는것이고 1대1로 생성된 userProfile또한 열람할 수 없는 것이다 !
이 권한을 views.py에서 권한없이도 열람할 수 있는 코드를 추가할수도 있지만 이는 3~4장에서 다룰 예정이니까 참고바란다 !
마지막으로 모든 과정을 따라했다면 backend의 폴더 구성은 다음과 같다.
여기까지가 2장의, DRF jwt를 이용한 back의 세팅이었고 다음 3장에서는 데이터 요청과 데이터를 보내는 2장의 처음에서 참조URL에서 다룬 useEffect, fetch, then 등 react의 문법이 주가 될것이고, 선택사항으로 소셜로그인기능인 react-google-login까지 알아볼 예정이다 !
만약 에러가 난다면 에러메세지와 404, 401, 500페이지 에러등을 잘 읽고 구글링한다면 충분히 무난하게 여기까지 올 수 있을것이라 생각한다 !