DRF - JWT 로그인 authentication

.·2021년 8월 7일
4

DRF를 기반으로 jwt로 로그인 기능 구현

이전 포스트에서 구현했었던 장고 기본 User를
Custom 한 Custom User Model을 통해 로그인을 구현한다.

JWT (Json Web Token )

프로젝트 세팅

패키지 설치

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 하기
}
  • rest_framework_jwt.authentication.JSONWebTokenAuthentication

위 코드를 통해 장고 기본 인증방식을 JWT 토큰 인증으로 할 수 있다.

  • DEFAULT_PERMISSION_CLASSES [...,]

위 코드를 통해 권한을 지정할 수 있다.


[app].serializers.py : 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}

[app].views.py

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가 간다.


[app].urls

# urls.py (app)
from django.urls import path
from django.conf.urls import url

from . import views

urlpatterns = [
    path('login', views.Login.as_view()),
] 

로컬에서 테스트

/users/login

get 요청을 받는 함수가 없어서, 'detail':"Method GET ..." 메세지가 반환되었다.

성공적으로 로그인을 성공했을 때

/users/login : POST

로그인에 실패했을 때

/users/login : POST


IsAuthenticated 메소드를 추가해, 유효한 Token을 가진 유저만 접근 테스트

[app].views 에 추가

...
@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)
  • @permission_classes([IsAuthenticated])
    앞써 settings.py 에서 선언한 DEFAULT_PERMISSION_CLASSES [] 을 통해 인증된 유저만 접근이 가능한 에너테이션을 선언한다.

만약 인증되지 않은 유저가 들어온다면 토큰이 없어서 접근이 불가하다는 메세지를 반환한다.

인증 성공

header 값에 앞 써 로그인 요청해서 받은 token 을 넣어준다.
jwt 공식문서에 보면
key : Authorization
value : JWT [토큰값]
으로 넣으라고 명시되어있다.

그대로 넣고 요청을 보내면 아래 그림과 같이 성공적으로 응답이 온다.

인증실패

하지만 token 을 넣지않고 IsAuthenticated 에 접근한다면 아래 그림과 같이 에러 메세지가 반환된다.


다음 번 포스트는 로그인 10회 오류시 lock out을 거는 logic을 포스팅할 예정이다.
캐시를 통해 구현해 많은 공부가 될것이다.

profile
.

0개의 댓글