DRF를 기반으로 jwt로 로그인 기능 구현
이전 포스트에서 구현했었던 장고 기본 User를
Custom 한 Custom User Model을 통해 로그인을 구현한다.
패키지 설치
pip install django-allauth
pip install django-rest-auth
pip install djangorestframework-jwt
settings.py
# Application definition
INSTALLED_APPS = [
...,
'users', # Startapp 을 통해 만든 APP
'rest_auth.registration',
'rest_framework.authtoken',
'rest_auth',
'allauth',
'allauth.account',
'allauth.socialaccount',
'django.contrib.sites',
...,
]
## DRF
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', # 인증된 사용자만 접근 가능
'rest_framework.permissions.IsAdminUser', # 관리자만 접근 가능
'rest_framework.permissions.AllowAny', # 누구나 접근 가능
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
# 'rest_framework.authentication.TokenAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
# 'rest_framework.authentication.BasicAuthentication',
),
}
# 추가적인 JWT_AUTH 설젇
JWT_AUTH = {
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_ALGORITHM': 'HS256', # 암호화 알고리즘
'JWT_ALLOW_REFRESH': True, # refresh 사용 여부
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 유효기간 설정
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28), # JWT 토큰 갱신 유효기간
# import datetime 상단에 import 하기
}
위 코드를 통해 장고 기본 인증방식을 JWT 토큰 인증으로 할 수 있다.
위 코드를 통해 권한을 지정할 수 있다.
from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.contrib.auth.models import update_last_login
from django.contrib.auth import authenticate
from rest_framework_jwt.settings import api_settings
User = get_user_model()
# JWT 사용을 위한 설정
JWT_PAYLOAD_HANDLER = api_settings.JWT_PAYLOAD_HANDLER
JWT_ENCODE_HANDLER = api_settings.JWT_ENCODE_HANDLER
class UserLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=30)
password = serializers.CharField(max_length=128, write_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
username = data.get("username")
password = data.get("password", None)
# 사용자 아이디와 비밀번호로 로그인 구현(<-> 사용자 아이디 대신 이메일로도 가능)
user = authenticate(username=username, password=password)
if user is None:
return {'id': 'None','username':username}
try:
payload = JWT_PAYLOAD_HANDLER(user) # payload 생성
jwt_token = JWT_ENCODE_HANDLER(payload) # jwt token 생성
update_last_login(None, user)
except User.DoesNotExist:
raise serializers.ValidationError(
'User with given username and password does not exist'
)
return {
'id':user.id,
'token': jwt_token
}
# 사용자 정보 추출
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id',)
현재 프로젝트에서는 front-end 팀과 로그인 성공시 id 값만 return 해주기로 회의해서 id값만 return 해준다.
username, email 과 같은 다른 fields 들도 함께 Response 한다면,
UserSerializer 의 fields 값에 더 추가해주면 된다.
공식문서를 통해 작성
https://jpadilla.github.io/django-rest-framework-jwt/
다음에 나올 lock out 때문에 username 을 return 해준다.
lock out 을 처리하지 않는다면, 굳이 username 을 return 하지 않아도 된다.
if user is None:
return {'id': 'None','username':username}
from django.core import cache
from rest_framework.response import Response
from rest_framework import status, mixins
from rest_framework import generics # generics class-based view 사용할 계획
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
from rest_framework.decorators import permission_classes, authentication_classes
# JWT 사용을 위해 필요
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer
from .serializers import *
from .models import *
from .cache import InvalidLoginAttemptsCache
import arrow
@permission_classes([AllowAny]) #모든 사용자 접근가능
class Login(generics.GenericAPIView):
serializer_class = UserLoginSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid(raise_exception=True):
return Response({"message": "Request Body Error."}, status=status.HTTP_409_CONFLICT)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
if user['id'] == "None":
return Response({"message": "fail"}, status=status.HTTP_401_UNAUTHORIZED)
return Response(
{
"id": UserSerializer(
user,context=self.get_serializer_context()
).data.get('id'),
"token": user['token']
}
)
"user": UserSerializer(user, context=self.get_serializer_context()).data 가 눈에 잘 안들어올 수도 있는데, 이를 가시적으로 작성해보면 아래처럼 됩니다. 해당 내용 역시 공식 홈페이지에 잘 나와 있습니다.
serializer = UserSerializer(user, context=self.get_serializer.context())
serializer.data
# {'id': 1}
나는 Front에 Response를 {'id':value, 'token':value} 로 보내주기로 사전에 약속해 아래와 같이 괴상한 코드가 나왔다.
UserSerializer(user,context=self.get_serializer_context()).data.get('id')
만약 .get('id')를 붙여주지 않는다면, { user : { 'id' : value },'token' : value } 형식으로 Response가 간다.
# urls.py (app)
from django.urls import path
from django.conf.urls import url
from . import views
urlpatterns = [
path('login', views.Login.as_view()),
]
get 요청을 받는 함수가 없어서, 'detail':"Method GET ..." 메세지가 반환되었다.
...
@permission_classes([IsAuthenticated])
class Test(generics.GenericAPIView):
serializer_class = UserSerializer
def get(self, request, *args, **kwargs):
return Response({'message':'good'},status=status.HTTP_200_OK)
만약 인증되지 않은 유저가 들어온다면 토큰이 없어서 접근이 불가하다는 메세지를 반환한다.
header 값에 앞 써 로그인 요청해서 받은 token 을 넣어준다.
jwt 공식문서에 보면
key : Authorization
value : JWT [토큰값]
으로 넣으라고 명시되어있다.
그대로 넣고 요청을 보내면 아래 그림과 같이 성공적으로 응답이 온다.
하지만 token 을 넣지않고 IsAuthenticated 에 접근한다면 아래 그림과 같이 에러 메세지가 반환된다.
다음 번 포스트는 로그인 10회 오류시 lock out을 거는 logic을 포스팅할 예정이다.
캐시를 통해 구현해 많은 공부가 될것이다.