この記事は、jangでのjwt認証のトピックに関する私の研究の結果として、いくつかの(最初の)記事をまとめたものです。そのため、(少なくともrunetでは)通常の記事を見つけることができませんでした。この記事は、プロジェクトの作成段階から、startproject、ねじ込みjwt認証を示しています。
徹底的に調査した上で、人間の判断に委ねます。
中古記事へのリンクが添付されています:
https://thinkster.io/tutorials/django-json-api/authentication
-
https://www.django-rest-framework.org/api-guide/authentication/
https://medium.com/django-rest/django-rest-framework-jwt-authentication-94bee36f2af8
JWT認証の構成
Djangoにはセッションベースの認証システムが付属しており、すぐに使用できます。これには、作成して後でユーザーにログインする必要がある可能性のあるすべてのモデル、ビュー、およびテンプレートが含まれます。ただし、ここに問題があります。Djangoのデフォルトの認証システムは、従来のHTML要求-応答ループでのみ機能します。
« '-' HTML»? , - (, ), . , «», - , - , , HTML , . , , « ».
, Django '-' HTML? , API, . , , JSON, HTML. JSON, , , . '-' JSON, , ( '-' HTML), . .
, Django , . , , . , . , , Django, , , .
, , :
User, Django
JSON HTML
HTML, Django
,
, Django . , , , , JSON Web Token Authentication (JWT ), .
Django (cookie). , (middlewares) , , . request.user
. , request.user
User
. , request.user
AnonymousUser
. , , request.user .
? , , , , request.user.isauthenticated()
, True
False
. request.user
AnonymousUser
, request.user.isauthenticated()
False. ( :) )
if request.user is not None and request.user.isauthenticated():
if request.user.isauthenticated():
, - !
, . , http://localhost:3000, http://localhost:5000. , , http://www.server.com http://www.clent.com. cookie, , , .
, cookie, (Cross-Origin Resource Sharing, CORS) (Cross-Site Request Forgery, CSRF), :
,
/ .. . . . ( , , , , , , ""). , , . , . , , . , , . , , .
(ID) . , . , . - , . , .
JSON Web Tokens
JSON Web Token (. JWT) - (RFC 7519) , . JWT .
, , ? JWT , .
JSON Web Tokes ?
JWT :
JWT - . , JWT , . , , .
JWT , .
. , « » , .
,
, . , django-admin startproject json_auth_project
( , pip3 install django
).
. cd jsonauthproject
, python3 -m venv venv
. , venv
, . , . ./venv/bin/activate
. requirements.txt
, ( django
pip3 install django
). pip3 freeze > requirements.txt
( , / ). ./manage.py migrate
. , , . - ./manage.py runserver
. localhost
8000. http://localhost:8000. "The install worked successfully! Congratulations!" - :)
, (app) authentication
: ./manage.py startapp authentication
. apps/authentication/models.py
, . , .
User
UserManager
, :
import jwt
from datetime import datetime, timedelta
from django.conf import settings from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin
)
from django.db import models
Django Manager : createuser()
createsuperuser()
. Django, https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#substituting-a-custom-user-model
UserManager
apps/authentication/models.py
( : https://docs.djangoproject.com/en/3.1/topics/db/managers/):
class UserManager(BaseUserManager):
"""
Django ,
Manager. BaseUserManager,
, Django User ( ).
"""
def create_user(self, username, email, password=None):
""" , . """
if username is None:
raise TypeError('Users must have a username.')
if email is None:
raise TypeError('Users must have an email address.')
user = self.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save()
return user
def create_superuser(self, username, email, password):
""" . """
if password is None:
raise TypeError('Superusers must have a password.')
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save()
return user
, , , :
class User(AbstractBaseUser, PermissionsMixin):
# ,
# User
# .
# .
username = models.CharField(db_index=True, max_length=255, unique=True)
# ,
# .
# ,
# ,
# ( ).
email = models.EmailField(db_index=True, unique=True)
# ,
# . ,
# , :)
# .
# , ,
# .
is_active = models.BooleanField(default=True)
# ,
# . .
is_staff = models.BooleanField(default=False)
# .
created_at = models.DateTimeField(auto_now_add=True)
# .
updated_at = models.DateTimeField(auto_now=True)
# , Django
# .
# USERNAME_FIELD ,
# . .
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
# Django, UserManager
# .
objects = UserManager()
def __str__(self):
""" ( ) """
return self.email
@property
def token(self):
"""
user.token,
user._generate_jwt_token(). @property
. token " ".
"""
return self._generate_jwt_token()
def get_full_name(self):
"""
Django ,
. ,
, username.
"""
return self.username
def get_short_name(self):
""" get_full_name(). """
return self.username
def _generate_jwt_token(self):
"""
- JSON,
, 1
"""
dt = datetime.now() + timedelta(days=1)
token = jwt.encode({
'id': self.pk,
'exp': int(dt.strftime('%s'))
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
, :
models.CustomUser
- , DjangoUser
https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.CustomUser
models.AbstractBaseUser
models.PermissionsMixin
- https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.AbstractBaseUser https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.PermissionsMixin
models.BaseUserManager
-UserManager
https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#django.contrib.auth.models.BaseUserManager
, Django, , (,
db_index
unique
) https://docs.djangoproject.com/en/3.1/ref/models/fields/
AUTH_USER_MODEL
-, Django , - django.contrib.auth.models.User
. , . User
, , Django User
, .
Django: https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#substituting-a-custom-user-model
, User
, .
Django User
, AUTH_USER_MODEL
project/settings.py
. , project/settings.py
:
# Django .
# authentication.User Django, User
# authentication. INSTALLED_APPS.
AUTH_USER_MODEL = 'authentication.User'
, , . - , Django , , - . , User.
:
./manage.py makemigrations
./manage.py migrate
, , . SQLite , . Django ,AUTH_USER_MODEL
, .
. . , :
./manage.py makemigrations
Django. , . , , .
authenticate
,
./manage.py makemigrations authentication
authentication
. . ,
./manage.py makemigrations
:
./manage.py migrate
makemigrations
, migrate
.
, User, , . , User. , .
:
./manage.py createsuperuser
Django - , . , . ! :)
, Django, :
./manage.py shell_plus
( ./manage.py shell
)
shell_plus django-extensions, (pip3 install django-extensions), shell_plus. , , INSTALLED_APPS. , .
, :
user = User.objects.first() user.username user.token
, username
token
.
, . .
RegistrationSerializer
apps/authentication/serializers.py
:
from rest_framework import serializers
from .models import User
class RegistrationSerializer(serializers.ModelSerializer):
""" . """
# , 8 , 128,
#
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
#
# . .
token = serializers.CharField(max_length=255, read_only=True)
class Meta:
model = User
# ,
# , , .
fields = ['email', 'username', 'password', 'token']
def create(self, validated_data):
# create_user,
# , .
return User.objects.create_user(**validated_data)
, , .
ModelSerializer
RegistrationSerializer
, serializers.ModelSerializer
. serializers.ModelSerializer
- serializers.Serializer,
Django REST Framework (DFR). ModelSerializer
, Django. , : . create()
User.objects.create_user()
, . DRF .
RegistrationAPIView
. , (views) (endpoint), URL .
apps/authentication/views.py
:
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import RegistrationSerializer
class RegistrationAPIView(APIView):
"""
( ) .
"""
permission_classes = (AllowAny,)
serializer_class = RegistrationSerializer
def post(self, request):
user = request.data.get('user', {})
# , -
# , .
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
:
permission_classes
- , , . .. ..
, , post - , . .
Django REST Framework (DRF) Permissions https://www.django-rest-framework.org/api-guide/permissions/
. Django 1.x => 2.x URL (path). URL- URL's, . , Django , .
apps/authentication/urls.py
:
from django.urls import path
from .views import RegistrationAPIView
app_name = 'authentication'
urlpatterns = [
path('users/', RegistrationAPIView.as_view()),
]
Django . . . app_name = 'authentication'
, (including) . URL-.
project/urls.py
:
from django.urls import path
, , include()
django.urls
from django.urls import path, include
include()
, , .
:
urlpatterns = [
path('admin/', admin.site.urls),
]
, urls.py
:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('apps.authentication.urls', namespace='authentication')),
]
Postman
, User , , , . ( ) Postman ( https://learning.postman.com/docs/getting-started/introduction/).
POST localhost:8000/api/users/ :
{
"user": {
"username": "user1",
"email": "user1@user.user",
"password": "qweasdzxc"
}
}
. ! , , . , "user". ( , ), DRF (renderer).
User
apps/authentication/renderers.py
:
import json
from rest_framework.renderers import JSONRenderer
class UserJSONRenderer(JSONRenderer):
charset = 'utf-8'
def render(self, data, media_type=None, renderer_context=None):
# token ,
# . ,
# User.
token = data.get('token', None)
if token is not None and isinstance(token, bytes):
# , token bytes.
data['token'] = token.decode('utf-8')
# , 'user'.
return json.dumps({
'user': data
})
, .
, apps/auhentication/views.py
UserJSONRenderer
, :
from .renderers import UserJSONRenderer
, renderer_classes
RegistrationAPIView
:
renderer_classes = (UserJSONRenderer,)
, UserJSONRenderer
, Postman'e . , "user".
, . , , . , API .
LoginSerializer
apps/authentication/serializers.py
:
from django.contrib.auth import authenticate
, :
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255)
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
# validate ,
# LoginSerializer valid.
# ,
# , .
email = data.get('email', None)
password = data.get('password', None)
# , .
if email is None:
raise serializers.ValidationError(
'An email address is required to log in.'
)
# , .
if password is None:
raise serializers.ValidationError(
'A password is required to log in.'
)
# authenticate Django ,
# -
# . email username,
# USERNAME_FIELD = email.
user = authenticate(username=email, password=password)
# / , authenticate
# None. .
if user is None:
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
# Django is_active User.
# , .
# , True.
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
)
# validate .
# , .. create update.
return {
'email': user.email,
'username': user.username,
'token': user.token
}
, .
LoginAPIView
apps/authentication/views.py
:
from .serializers import LoginSerializer, RegistrationSerializer
, :
class LoginAPIView(APIView):
permission_classes = (AllowAny,)
renderer_classes = (UserJSONRenderer,)
serializer_class = LoginSerializer
def post(self, request):
user = request.data.get('user', {})
# , save() ,
# . ,
# . , validate() .
serializer = self.serializer_class(data=user)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
, apps/authentication/urls.py
:
from .views import LoginAPIView, RegistrationAPIView
urlpatterns
:
urlpatterns = [
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
]
Postman
, , . :) Postman, http://localhost:8000/api/users/login/
, . , :
{
"user": {
"email": "email@email.email",
"username": "admin",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjA1MTE3MjkwfQ.W8B6RY-jGO9PYDTzDWxhrkSHsTe1p3jlzq1BL7Tbwcs"
}
}
, token
, , .
-, . , , . - . -, non_field_errors
. , . , , , validate_email
, Django REST Framework , . , nonfield_errors
, , . -, , JSON ( ). , Django REST Framework.
EXCEPTION_HANDLER NON_FIELD_ERRORS_KEY
DRF EXCEPTION_HANDLER
. , , EXCEPTION_HANDLER
. NON_FIELD_ERRORS_KEY
, .
project/exceptions.py
, :
from rest_framework.views import exception_handler
def core_exception_handler(exc, context):
# , ,
# -,
# DRF. , ,
# DRF - .
response = exception_handler(exc, context)
handlers = {
'ValidationError': _handle_generic_error
}
# . ,
# , DRF.
exception_class = exc.__class__.__name__
if exception_class in handlers:
# - :)
# ,
return handlers[exception_class](exc, context, response)
return response
def _handle_generic_error(exc, context, response):
# , .
# DRF 'errors'.
response.data = {
'errors': response.data
}
return response
, project/settings.py
REST_FRAMEWORK
:
REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'project.exceptions.core_exception_handler', 'NON_FIELD_ERRORS_KEY': 'error', }
DFR. , , , . ( / ) Postman - .
UserJSONRenderer
, / , . , "error", "user", . UserJSONRenderer
"error" . apps/authenticate/renderers.py
:
import json
from rest_framework.renderers import JSONRenderer
class UserJSONRenderer(JSONRenderer):
charset = 'utf-8'
def render(self, data, media_type=None, renderer_context=None):
# (,
# ), data error. ,
# JSONRenderer ,
# .
errors = data.get('errors', None)
# token ,
# . ,
# User.
token = data.get('token', None)
if errors is not None:
# JSONRenderer .
return super(UserJSONRenderer, self).render(data)
if token is not None and isinstance(token, bytes):
# , token bytes.
data['token'] = token.decode('utf-8')
# , 'user'.
return json.dumps({
'user': data
})
, ( /) Postman - .
.
, . . .
UserSerializer
. , .
apps/authentication/serializers.py
:
class UserSerializer(serializers.ModelSerializer):
""" User. """
# 8 128 . .
# -,
# , , .
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
# read_only_fields
# read_only = True, .
# , 'read_only_fields'
# , .
# min_length max_length,
# .
read_only_fields = ('token',)
def update(self, instance, validated_data):
""" User. """
# ,
# setattr. Django ,
# ''. ,
# 'validated_data' .
password = validated_data.pop('password', None)
for key, value in validated_data.items():
# , validated_data
# User .
setattr(instance, key, value)
if password is not None:
# 'set_password()' ,
# , .
instance.set_password(password)
# , ,
# User. , set_password() .
instance.save()
return instance
, create , DRF serializers.ModelSerializer
. , , RegistrationSerializer
.
UserRetrieveUpdateAPIView
apps/authentication/views.py
:
from rest_framework import status
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer,
)
, UserRetrieveUpdateView
:
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
# . ,
# User -,
# json .
serializer = self.serializer_class(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, *args, **kwargs):
serializer_data = request.data.get('user', {})
# , - ,
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
apps/authentication/urls.py
, UserRetrieveUpdateView
:
from .views import (
LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView
)
urlpatterns
:
urlpatterns = [
path('user', UserRetrieveUpdateAPIView.as_view()),
path('users/', RegistrationAPIView.as_view()),
path('users/login/', LoginAPIView.as_view()),
]
Postman (GET localhost:8000/api/user/). , :
{
"user": {
"detail": "Authentication credentials were not provided."
}
}
Django . , - , , , . JWT, Django, Django REST Framework (DRF).
apps/authentication/backends.py
:
import jwt
from django.conf import settings
from rest_framework import authentication, exceptions
from .models import User
class JWTAuthentication(authentication.BaseAuthentication):
authentication_header_prefix = 'Token'
def authenticate(self, request):
"""
authenticate , ,
. 'authenticate'
:
1) None - None .
, , .
, , ,
.
2) (user, token) - /
, .
, , ,
.
AuthenticationFailed DRF .
"""
request.user = None
# 'auth_header' :
# 1) (Token )
# 2) JWT,
auth_header = authentication.get_authorization_header(request).split()
auth_header_prefix = self.authentication_header_prefix.lower()
if not auth_header:
return None
if len(auth_header) == 1:
# ,
return None
elif len(auth_header) > 2:
# , -
return None
# JWT ,
# bytes,
# Python3 (HINT: PyJWT). ,
# prefix token. ,
# , , .
prefix = auth_header[0].decode('utf-8')
token = auth_header[1].decode('utf-8')
if prefix.lower() != auth_header_prefix:
# , - .
return None
# "", .
# .
return self._authenticate_credentials(request, token)
def _authenticate_credentials(self, request, token):
"""
. -
, - .
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY)
except Exception:
msg = ' . '
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get(pk=payload['id'])
except User.DoesNotExist:
msg = ' .'
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = ' .'
raise exceptions.AuthenticationFailed(msg)
return (user, token)
, . , , , , , , - .
DRF
Django REST Framework, , , Django .
project/settings.py
REST_FRAMEWORK
:
REST_FRAMEWORK = { ... 'DEFAULT_AUTHENTICATION_CLASSES': ( 'apps.authentication.backends.JWTAuthentication', ), }
Postman
, , , , . , Postman (GET localhost:8000/api/user/). , . , ? . (PATCH localhost:8000/api/user/), . , , .
この記事で行ったことを要約しましょう。柔軟なユーザーモデル(将来的には、必要に応じて拡張したり、さまざまなフィールドを追加したり、モデルを追加したりできます)、3つのシリアライザーを作成しました。各シリアライザーは、明確に定義された独自の機能を実行します。ユーザーが自分のアカウントに関する情報を登録、ログイン、受信、更新できるようにする4つのエンドポイントを作成しました。私の意見では、これは非常に快適な基盤であり、あなたの裁量でいくつかの新しいランププロジェクトを構築するための基礎です:)(ただし、何時間も話すことができる領域がたくさんあります。たとえば、次の段階はどのように言語で回転していますかpostgres、radishes、celeryをねじ込んで、すべてをドッカーコンテナに包みます。