Pt. 1) Django JWT 설정하기
1인 프로젝트를 진행하는 데에 있어서 프론트엔드는 평소에 하고 있어서 문제가 없었지만 백엔드는 평소에 많이 만진 적이 없어서 로그인 기능을 구현하는 데부터 살짝 애를 먹었다. 일반적으로 개인이 진행하는 토이 프로젝트의 경우 그냥 DB에 username과 (암호화 된) password를 DB에 저장해서 lookup하는 정도로도 구현할 수 있지만, 로그인은 보안이 중시되는 작업인데 너무 허술해 보이기도 하고 회사에서 JWT로 인증 작업을 진행하던 걸 본 적이 있어서 JWT를 사용해서 본격적으로 해 보고자 했다.
JWT가 무엇인지, 특히 인증 단계에서 JWT를 왜 쓰는지도 시간이 된다면 정리해 보고 싶다.
이하에서 진행하는 프로젝트는 이미 프로젝트를 위한 가상 환경 설정과 해당 가상 환경에서의 Django 설치는 완료되었다고 가정하고 진행한다.
Django 백엔드 프로젝트 설정하기
일단 인증 튜토리얼이라는 폴더 안에 백엔드와 프론트엔드를 동시에 관리하고자 한다. 나의 경우 auth-tutorial 폴더 안에 backend 폴더를 만들고 내부에 api 프로젝트를 생성한다. 그리고 user의 정보를 담을 user 앱을 생성한다. DB 설정도 해 주도록 하자.
$ cd backend
$ django-admin startproject api .
$ python manage.py startapp user
$ python manage.py migrate
이후 Django 프로젝트를 관리할 환경에서 필요한 라이브러리를 설치한다. 우리가 설치하고자 하는 건 djangorestframework, djangorestframework-simplejwt, django-cors-headers이다.
$ pip install djangorestframework djangorestframework-simplejwt django-cors-headers
앞의 두 개는 Django에서 REST 통신을 하고 JWT auth를 가능하게 해 주며(원래는 djangorestframework-jwt로 가능했지만 지원이 중단되었다), django-cors-headers의 경우 프론트엔드와의 CORS 문제를 해결할 수 있게 해 준다.
settings.py 파일 설정하기
api 프로젝트 내부의 setting.py 파일을 설정하자.
INSTALLED_APPS
INSTALLED_APPS = [
...
"user",
"rest_framework",
"rest_framework_simplejwt",
"corsheaders",
]
user 앱과 설치한 라이브러리를 추가했다.
MIDDLEWARE
MIDDLEWARE = [
...
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
]
프로젝트에 사용할 middleware를 추가했다.
CORS_ORIGIN_WHITELIST (신규)
CORS_ORIGIN_WHITELIST = [
"http://localhost:3000"
]
CORS 문제를 해결하기 위해 django-cors-headers의 화이트리스트에 프론트엔드 주소를 추가한다.
REST_FRAMEWORK (신규)
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}
DEFAULT_PERMISSION_CLASSES는 접근 권한에 대한 설정이고, DEFAULT_AUTHENTICATION_CLASSES는 API 실행 시 인증에 사용할 클래스를 설정한다.
타임존 설정 (선택)
LANGUAGE_CODE = "ko-kr"
TIME_ZONE = "Asia/Seoul"
USE_I18N = True
USE_TZ = False
i18n과 타임존에 관한 설정을 위와 같이 설정해 준다. 이렇게 설정하지 않으면 나중에 Django에서 DB를 생성할 때 UTC 기준으로 생성된다. 정말 로그인만 구현하고 말 튜토리얼 프로젝트라면 굳이 해 줄 필요는 없다.
SIMPLE_JWT (신규)
from datetime import timedelta
...
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
}
SIMPLE_JWT 설정을 위와 같이 해준다. djangorestframework-simplejwt의 documentation에서 이 설정 값들의 기본값을 알 수 있으며, 해당 문서를 참고해서 입맛에 맞게 설정을 추가하거나 변경해 줘도 좋다.
참고로, 'VERIFYING_KEY': None 옵션은 추가하지 말자. 앞으로 진행하는 내용 중 JWT 갱신에 있어서 문제가 생길 수 있다.
유저 생성하기
길고 긴 설정이 끝났다. 이제 JWT를 통해 토큰 획득을 구현…하기 이전에, 유저부터 생성하자.
먼저, 우리는 Django에 내장된 기본 User 모델을 사용할 것이기 때문에 User 모델을 따로 구현해 주지 않는다. 우리가 user에 관해서 해야 할 것은 테스트를 위한 superuser의 생성이다.
$ python manage.py createsuperuser
위의 명령어를 실행해서 절차에 따라 임의의 관리자 계정을 설정해 보자. 아이디, 이메일, 패스워드를 설정하게 되는데 나는 아이디는 admin, 이메일은 공란으로 입력했고 패스워드 또한 admin으로 입력했다. 패스워드가 취약하다는 알림이 뜨지만 y를 눌러서 강제로 생성해 줘도 아무런 문제가 없다.
$ python manage.py runserver
이제 위의 명령어로 서버를 임시로 실행해 보자. 기본 설정으로는 localhost의 8000번 포트를 통해서 들어갈 수 있다. localhost:8000에 접속하면 Django의 기본 화면만 뜨게 된다. 어디로 들어가야 할까?
api 프로젝트 내부의 urls.py를 들여다 보자.
from django.contrib import admin
from django.urls import path
urlpatterns = [
path("admin/", admin.site.urls),
]
기본적으로 관리자 페이지가 admin이라는 경로로 설정되어 있는 것을 확인할 수 있다. 실제로 localhost:8000/admin에 접속하면 아래와 같은 페이지가 뜨는 것을 확인할 수 있을 것이다.
그러면 해당 로그인 창에서 이전에 생성한 관리자 계정으로 로그인이 됨을 확인할 수 있을 것이다.
토큰 획득 구현하기
이제 정말로 JWT를 통한 토큰 획득을 구현해 보자. 원래 JWT는 토큰을 갱신하거나 검증하는 과정도 존재하지만, 일단은 로그인 기능만 아주 간단하게 구현하는 것이 목표이므로 일단 토큰 획득만 구현하는 것을 목표로 삼자.
우리가 구현하고자 하는 건 클라이언트가 토큰을 획득할 수 있도록 하는 것이기 때문에 백엔드가 가지고 있는 정보를 프론트엔드가 알 수 있도록 바꿔서 제공해야 하는 기능을 구성하는 것이다. 작성/수정해야 할 파일은 총 세 가지이다.
- serializers.py
Django에서 REST 통신을 하기 위해서는 내부에 존재하는 모델에서 field를 생성하고, 이것을 REST로 통신 가능한 형태인 JSON 문자열로 만드는 과정이 필요하다. 이 과정을 serialize라고 한다.
- views.py
우리는 클라이언트에서 받은 request에 대해 특정 response를 보낼 필요가 있다. 받은 요청에 대해 로직을 수행해서 응답을 반환하는 곳이 Django의 view에 해당한다.
- urls.py
위에서 만든 view들을 적절한 url에 매핑하는 과정이 필요하다. 위에서 api 프로젝트의 urls.py를 봤으면 알겠지만, 이러한 과정을 urls.py에서 담당하게 된다.
사실, djangorestframework-simplejwt에서는 기본적으로 제공하는 view만 사용해도 해당 view 내부에 토큰을 기본적으로 serialize해서 제공해준다. 즉 정말 최소한의 기능만 구현하려면 serializers와 views를 구현할 필요도 없고 urls.py에 라이브러리의 view를 연결해 주기만 해도 되지만 이러면 너무 단순하면서도 실제로 추가적인 정보를 받아와야 할 필요가 있을 수 있으므로 기본적인 serializer와 view의 작성은 일부 커스텀을 진행하면서 알아보도록 하자.
user/serializers.py
class TokenObtainPairSerializer(TokenObtainSerializer):
token_class = RefreshToken
def validate(self, attrs):
data = super().validate(attrs)
refresh = self.get_token(self.user)
data["refresh"] = str(refresh)
data["access"] = str(refresh.access_token)
if api_settings.UPDATE_LAST_LOGIN:
update_last_login(None, self.user)
return data
위가 TokenObtainPairSerializer의 기본 구조이다. TokenObtainSerializer를 또 상속받는데, 너무 깊어지니 여기까지만 확인해 보도록 하자. 필요한 사람은 TokenObtainSerializer도 확인해 보는 것을 적극 추천한다.
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
...
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
# 에러 메시지를 바꿔 주자
# 에러 메시지는 TokenObtainSerializer까지 들어가면 확인할 수 있다
default_error_messages = {
"no_active_account": {
"message": "Username or password is incorrect!",
"success": False,
"status": 401,
}
}
def validate(self, attrs):
data = super().validate(attrs)
# username과 성공 여부를 함께 보내주자
# 기본적으로는 access token과 refresh token만 담긴다
data["username"] = self.user.username
data["success"] = True
return data
코드에 대한 내용은 주석으로 적었다. 이제, 이것을 view에 전달해 주어야 한다.
user/views.py
일부 블로그에서는 restframework의 APIView를 상속받아 새로운 view 클래스를 만드는 경우도 있었다. 이러한 블로그에서는 restframework-jwt를 사용하고 있었는데, 해당 라이브러리를 사용하면 APIView를 직접 상속받아야 해서 그렇게 구현한 건지는 잘 모르겠다. 아니면 좀 더 자유로운 구현을 위해서 APIView를 상속받은 거일 수도 있을 거라는 생각을 했다. 일단 명확한 점은 restframework의 APIView에서는 post와 같은 메소드를 제공하지 않는다는 점이다.
그러나 simplejwt에서는 restframework의 APIView를 상속받아서 생성된 view를 제공한다. 우리는 그것을 다시 상속받아서 커스텀하고자 한다. 이렇게 미리 만들어진 view에서는 기본적으로 post 함수가 제공된다.
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(serializer.validated_data, status=status.HTTP_200_OK)
TokenObtainPairView는 TokenViewBase를 상속하고, 위 post 함수는 TokenViewBase 내부에 선언된 post 함수이다. 해당 post 함수를 사용하면 우리가 위의 serializer에서 받은 데이터인 token들과 username, success 여부까지 전부 response로 보낼 수 있게 된다.
from rest_framework import permissions
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import MyTokenObtainPairSerializer
...
class MyTokenObtainPairView(TokenObtainPairView):
permission_classes = (permissions.AllowAny,)
serializer_class = MyTokenObtainPairSerializer
user/urls.py
from .views import MyTokenObtainPairView
...
urlpatterns = [
path("login/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"),
]
위에서 만들어 준 MyTokenObtainPairView를 view 형태로 urls.py에 작성하도록 하자. 이렇게만 작성하면 우리의 프로젝트인 api 프로젝트에서 접근할 수 없기 때문에, api 내부의 urls.py도 살짝 수정해 주어야 한다.
api/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("user/", include("user.urls")),
]
path(”user/”, include(”user.urls”))를 통해 user에서 생성된 urls을 전부 user/${url}/와 같이 user의 하위 url로 받게 된다.
이렇게 설정을 마치게 되면 백엔드 쪽의 설정은 끝났다.
작동 테스트
프론트엔드에 연결하기 전에 직접 테스트해 보자. 서버를 실행하고 localhost:8000/user/login에 들어가면 테스트할 수 있는 화면이 뜬다. GET method는 허용되지 않으므로 오류가 뜨지만 우리는 POST method만 테스트하면 되기 때문에 해당 오류는 무시하고 username과 password를 입력하여 POST request를 보내 보도록 하자.
위와 같이 POST request가 성공적으로 보내지고 response의 데이터에 refresh token, access token, username, success 여부가 담겨 되돌아오는 것을 볼 수 있다.
우리는 에러 메시지도 커스텀했기 때문에 존재하지 않는 유저도 보내 보자.
에러 메시지도 성공적으로 response로 받을 수 있음을 확인할 수 있다.
디렉토리 구조
.
├── backend
│ ├── api
│ │ ├── (etc)
│ │ ├── settings.py
│ │ └── urls.py
│ ├── db.sqlite3
│ ├── manage.py
│ └── user
│ ├── (etc)
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
└── env
우리가 직접 수정한 파일들만 추려서 디렉토리 구조를 만들어 봤다. 지금까지 따라왔다면 디렉토리는 위와 같이 구성되어 있을 것이다.
그럼, 다음에는 React를 이용하여 프론트엔드 환경을 구성하고, Django와 연결해서 직접 테스트해 보도록 하자.
'공부 > Django' 카테고리의 다른 글
Django + React로 로그인 시스템 구현하기 / Pt. 3) 토큰 저장하기 (0) | 2022.11.14 |
---|---|
Django 초기 설정 - 가상 환경부터 프로젝트 생성까지 (0) | 2022.10.13 |