Django Rest Framework 사용자 정의 JWT 인증
This article is not a tutorial or a guide, it is more like a request for code review and validate the implementation from more experienced Django developers, so please don't use this code unless you are able to review and validate yourself
소개
웹 응용 프로그램의 안전성은 매우 중요하다. 생산 과정에서 이 일을 독립적으로 완성하는 것은 커다란 책임이다. 이것이 바로 내가 Django를 좋아하는 이유이다. 왜냐하면 그곳의 대부분의 보안 빈틈을 해결했기 때문이다. 그러나 백엔드를 모든 유형의 클라이언트 (SPA, 모바일, 데스크톱 등) 를 연결하기 위해restful API로 구축하고 싶을 때 나의 문제가 발생한다
Django Rest Framework(DRF)는 서로 다른 내장authentication classes, 영패 인증 또는JWT을 제공하는 것이 제 용례의 발전 방향이지만, 클라이언트에서 영패를 어떻게 저장해야 할지 걱정입니다.
모든 사람들이 영패를 로컬 저장소에 저장하지 말라고 한다. 왜냐하면XSS attacks, 영패를 httponly 쿠키에 저장하는 것이 가장 좋지만, 쿠키도 CSRF attack에 개방되고, DRF는 모든 APIVIew에 대해 CSRF 보호를 사용하지 않는다. 그렇다면 무엇이 가장 좋은 방법일까.
한동안 나는 그것을 사용해 왔다. 왜냐하면 모든 사람, 특히 이동 분야(내 반응은 로컬), 우리는 안전한 저장소를 가지고 있고, 만약 장치가 (루트 안드로이드나 탈옥 ios) 가 아니라면, 모든 응용 프로그램은 샌드박스이고, 영패는 대부분의 상황에서 안전하기 때문이다.
이 문제는 웹 클라이언트(SPA)에 여전히 존재하기 때문에 저는 유용한 실현 가능성을 제기했습니다. 저는 여기에 그것을 기록하여 더욱 경험이 많은 개발자로부터 피드백을 얻을 수 있도록 하겠습니다. 저는 제 실현을 다음과 같은 단계로 요약할 수 있습니다.
사용자는 사용자 이름과 비밀번호가 있는 POST 요청을 보내서 로그인하고 서버에서 세 가지 일을 합니다.
access_token
, 짧은 수명 jwt(5분 가능), 응답체에 전송refresh_token
, 장수명 jwt(일), httponly만 있는 쿠키에 보내기 때문에 클라이언트javascript클라이언트에서 개발자는 다음과 같이 주의해야 합니다.
access_token
을 발송합니다.Authorization
헤더에 CSRF 토큰X-CSRFTOKEN
이 필요할 때, 영패 단점을 새로 고치기 위해 POST 요청을 보내야 한다프로젝트 설정
python3 -m venv .venv
source .venv/bin/activate
pip install django django-cors-headers djangorestframework PyJWT
# create Django project with the venv activates
django-admin startproject project
# create an app
./manage.py start app accounts
프로젝트 설정에서 응용 프로그램을 사용하고 설정을 추가합니다INSTALLED_APPS = [
...
# 3rd party apps
'corsheaders',
'rest_framework',
# project apps
'accounts',
]
CORS_ALLOW_CREDENTIALS = True # to accept cookies via ajax request
CORS_ORIGIN_WHITELIST = [
'http://localhost:3000' # the domain for front-end app(you can add more than 1)
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', # make all endpoints private
)
}
첫 번째 이전을 적용하기 전에 official Django docs의 건의에 따라 사용자 정의 사용자 모델로 프로젝트를 시작하는 것을 좋아합니다. 설령 내가 지금 그것을 사용하지 않더라도access_token
에서 사용자 모델 정의from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
accounts.models
에서 사용자 모델 정의...
AUTH_USER_MODEL = 'accounts.User'
...
나중에 다음 방법 중 하나를 통해 사용자 모델을 참조할 수 있습니다from django.conf import settings
User = settings.AUTH_USER_MODEL
# OR
from django.contrib.auth import get_user_model
User = get_user_model()
project.settings.py
에서 관리 사이트에 새 사용자 모델 등록from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from accounts.models import User
admin.site.register(User, UserAdmin)
수퍼유저 만들기./manage.py createsuperuser
마지막으로, 우리가 상술한 설정에서 설명한 바와 같이, 기본적으로 모든 단점은 신분 검증이 필요하다. 우리는 일부 보기에서 이 점을 덮어쓸 수 있다. 잠시 후에 우리는 로그인에서 이렇게 할 것이다현재 인증된 사용자 객체를 JSON 형식으로 반환하는 사용자 프로필 끝점을 만듭니다. 이를 위해 사용자 서열화기를 만들어야 합니다.
# accounts.serializers
from rest_framework import serializers
from accounts.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email',
'first_name', 'last_name', 'is_active']
# project.urls
from accounts import urls as accounts_urls
urlpatterns = [
path('accounts/', include(accounts_urls)),
]
# accounts.urls
urlpatterns = [
path('profile', profile, name='profile'),
]
# accounts.views
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import UserSerializer
@api_view(['GET'])
def profile(request):
user = request.user
serialized_user = UserSerializer(user).data
return Response({'user': serialized_user })
현재, 이 노드에 접근하려면, 인용문에서 말한 바와 같이 403 오류를 받을 것입니다. 로그인한 다음, 요청 헤더에access\u 영패를 보내야 합니다.로그인 뷰
로그인 단점은post 요청으로 요청 본문
accounts.admin
과 username
이 있습니다.사용 권한 클래스 장식기
password
를 사용하여 로그인 보기를 공개하고 AllowAny
장식 보기를 사용합니다. 로그인에 성공하면 Django가 응답에서 CSRF 쿠키를 보내도록 합니다.로그인에 성공하면 다음과 같은 이점을 얻을 수 있습니다.
@ensure_csrf_cookie
.access_token
만 해당합니다.refreshtoken
를 사용하면 자바스크립트에서 읽을 수 있고 필요할 때 다시 보낼 수 있습니다# accounts.views
from django.contrib.auth import get_user_model
from rest_framework.response import Response
from rest_framework import exceptions
from rest_framework.permissions import AllowAny
from rest_framework.decorators import api_view, permission_classes
from django.views.decorators.csrf import ensure_csrf_cookie
from accounts.serializers import UserSerializer
from accounts.utils import generate_access_token, generate_refresh_token
@api_view(['POST'])
@permission_classes([AllowAny])
@ensure_csrf_cookie
def login_view(request):
User = get_user_model()
username = request.data.get('username')
password = request.data.get('password')
response = Response()
if (username is None) or (password is None):
raise exceptions.AuthenticationFailed(
'username and password required')
user = User.objects.filter(username=username).first()
if(user is None):
raise exceptions.AuthenticationFailed('user not found')
if (not user.check_password(password)):
raise exceptions.AuthenticationFailed('wrong password')
serialized_user = UserSerializer(user).data
access_token = generate_access_token(user)
refresh_token = generate_refresh_token(user)
response.set_cookie(key='refreshtoken', value=refresh_token, httponly=True)
response.data = {
'access_token': access_token,
'user': serialized_user,
}
return response
여기는 영패를 생성하는 함수입니다. 보안을 높이기 위해 다른 비밀을 사용하여 영패를 새로 고치는 것에 서명했습니다.# accounts.utils
import datetime
import jwt
from django.conf import settings
def generate_access_token(user):
access_token_payload = {
'user_id': user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, minutes=5),
'iat': datetime.datetime.utcnow(),
}
access_token = jwt.encode(access_token_payload,
settings.SECRET_KEY, algorithm='HS256').decode('utf-8')
return access_token
def generate_refresh_token(user):
refresh_token_payload = {
'user_id': user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),
'iat': datetime.datetime.utcnow()
}
refresh_token = jwt.encode(
refresh_token_payload, settings.REFRESH_TOKEN_SECRET, algorithm='HS256').decode('utf-8')
return refresh_token
DRF의 사용자 정의 인증 클래스
Django Rest 프레임워크는 사용자 정의 인증 방안을 만드는 것을 쉽게 합니다. official docs
다음 코드는 처음에 DRF 소스 코드에서 가져온 다음 필요에 따라 변경사항을 추가합니다.
DRF는 세션 인증에서만 CSRFrest_framework/authentication.py를 강제합니다.
# accounts.authentication
import jwt
from rest_framework.authentication import BaseAuthentication
from django.middleware.csrf import CsrfViewMiddleware
from rest_framework import exceptions
from django.conf import settings
from django.contrib.auth import get_user_model
class CSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class SafeJWTAuthentication(BaseAuthentication):
'''
custom authentication class for DRF and JWT
https://github.com/encode/django-rest-framework/blob/master/rest_framework/authentication.py
'''
def authenticate(self, request):
User = get_user_model()
authorization_heaader = request.headers.get('Authorization')
if not authorization_heaader:
return None
try:
# header = 'Token xxxxxxxxxxxxxxxxxxxxxxxx'
access_token = authorization_heaader.split(' ')[1]
payload = jwt.decode(
access_token, settings.SECRET_KEY, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed('access_token expired')
except IndexError:
raise exceptions.AuthenticationFailed('Token prefix missing')
user = User.objects.filter(id=payload['user_id']).first()
if user is None:
raise exceptions.AuthenticationFailed('User not found')
if not user.is_active:
raise exceptions.AuthenticationFailed('user is inactive')
self.enforce_csrf(request)
return (user, None)
def enforce_csrf(self, request):
"""
Enforce CSRF validation
"""
check = CSRFCheck()
# populates request.META['CSRF_COOKIE'], which is used in process_view()
check.process_request(request)
reason = check.process_view(request, None, (), {})
print(reason)
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
클래스를 만든 후 csrftoken
로 이동하여 project.settings
부분에서 이렇게 활성화합니다 REST_FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'accounts.authentication.SafeJWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
현재 우리는 appname.filename.classname
인증 방법을 설치했고 access_token
노드에 다시 접근할 수 있지만 이번에는 profile
헤더를 설정할 것이다영패 보기 새로 고침
언제든지 영패가 만료되거나 어떤 이유로 새로운 영패가 필요하면 우리는
Authorization
단점이 필요하다.이 뷰는
refresh_token
에 대한 라이센스가 필요하지만 다른 두 가지 측면에서 보호됩니다.AlloAny
.access_token
를 생성하여 다시 보냅니다.refresh_token
유효하지 않거나 만료되면 다시 로그인해야 합니다.import jwt
from django.conf import settings
from django.contrib.auth import get_user_model
from django.views.decorators.csrf import csrf_protect
from rest_framework import exceptions
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.decorators import api_view, permission_classes
from accounts.utils import generate_access_token
@api_view(['POST'])
@permission_classes([AllowAny])
@csrf_protect
def refresh_token_view(request):
'''
To obtain a new access_token this view expects 2 important things:
1. a cookie that contains a valid refresh_token
2. a header 'X-CSRFTOKEN' with a valid csrf token, client app can get it from cookies "csrftoken"
'''
User = get_user_model()
refresh_token = request.COOKIES.get('refreshtoken')
if refresh_token is None:
raise exceptions.AuthenticationFailed(
'Authentication credentials were not provided.')
try:
payload = jwt.decode(
refresh_token, settings.REFRESH_TOKEN_SECRET, algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed(
'expired refresh token, please login again.')
user = User.objects.filter(id=payload.get('user_id')).first()
if user is None:
raise exceptions.AuthenticationFailed('User not found')
if not user.is_active:
raise exceptions.AuthenticationFailed('user is inactive')
access_token = generate_access_token(user)
return Response({'access_token': access_token})
영패 취소
수수께끼의 마지막 부분은 영패 리셋을 취소하는 방법입니다. 영패 리셋의 생명 주기가 매우 길기 때문에 영패를 블랙리스트에 넣거나 영패에 uid를 분배하여 부하에 넣은 다음 사용자에게 연결시켜 데이터베이스에 저장할 수 있습니다. 취소나 로그아웃할 때 데이터베이스에 있는 uid를 변경하여 부하의 값과 일치하지 않게 하면 응용 프로그램에 적합한 내용을 선택할 수 있습니다.
Reference
이 문제에 관하여(Django Rest Framework 사용자 정의 JWT 인증), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/a_atalla/django-rest-framework-custom-jwt-authentication-5n5텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)