Este artículo es una recopilación-ensamblaje de varios artículos (basados en el primero), como resultado de mis estudios sobre el tema de la autenticación jwt en jang con todo lo que implica. Entonces no fue posible (al menos en runet) encontrar un artículo normal, que dice desde la etapa de creación de un proyecto, startproject, atornillando la autenticación jwt.
Habiendo investigado a fondo, lo someto al juicio humano.
Se adjuntan enlaces a artículos usados:
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
Configuración de la autenticación JWT
Django viene con un sistema de autenticación basado en sesiones y funciona de inmediato. Esto incluye todos los modelos, vistas y plantillas que pueda necesitar para crear y los usuarios que inicien sesión posteriormente. Pero aquí está el problema: el sistema de autenticación predeterminado de Django solo funciona con el bucle tradicional de solicitud y respuesta 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/), . , , .
Resumamos lo que hemos hecho en este artículo. Hemos creado un modelo de usuario flexible (en el futuro, puede expandirlo a su gusto, agregando diferentes campos, modelos adicionales, etc.), tres serializadores, cada uno de los cuales realiza su propia función claramente definida. Creó cuatro puntos finales que permiten a los usuarios registrarse, iniciar sesión, recibir y actualizar la información de su cuenta. En mi opinión, esta es una base muy agradable, una base sobre la cual puede construir un nuevo proyecto de lámpara a su discreción :) (sin embargo, hay un montón de áreas de las que puede hablar durante horas, por ejemplo, la siguiente etapa gira en el idioma sobre cómo envuelva todo en contenedores docker atornillando postgres, rábanos y apio).