在过去的几个月中,我一直在利用业余时间来构建Web应用程序。虽然我想保留我的应用程序的详细信息(暂时),但我确实想分享我在使用该应用程序时克服的一些困难。特别是在使用JSON Web令牌(JWT)进行用户管理和身份验证时。
科技栈
在深入研究代码之前,我想提供一些有关将要使用的技术的背景信息。在后端,我选择了Django和Django Rest Framework来开发RESTful API,这将是我应用程序的核心。在前端,我决定使用Reactjs来构建我的SPA来调用后端。我将使用Docker进行容器化,使用CircleCI构建我的CI / CD管道,并使用AWS作为基础设施服务。最后,我将使用Postman运行测试并监视API。本演练进展非常迅速,并假定您对Python,Django和Docker有所了解。
要求
开始之前,您应该安装以下python依赖项:
1 2 3 4 5 6 7 |
Django==2.1 django-rest-framework==0.1.0 djangorestframework==3.8.2 gunicorn==19.9.0 # Our WSGI server psycopg2==2.7.5 # Only if using Postgresql PyJWT==1.6.4 pytz==2018.5 |
由于我使用的是Docker,因此可以启动并运行以下Dockerfile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
FROM python:3.6-alpine MAINTAINER Sebastian Ojeda <sebastian@oddjobbox.com> WORKDIR /app COPY requirements.txt . RUN apk add --no-cache --virtual .build-deps \ build-base postgresql-dev jpeg-dev zlib-dev \ && pip install -r requirements.txt \ && find /usr/local \ \( -type d -a -name test -o -name tests \) \ -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \ -exec rm -rf '{}' + \ && runDeps="$( \ scanelf --needed --nobanner --recursive /usr/local \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u \ )" \ && apk add --virtual .rundeps $runDeps \ && apk del .build-deps COPY . . CMD ["python3", "manage.py", "runserver", "0:8000"] |
我仍在努力缩小此图片的文件大小(如果您有解决方案可以缩小此图片,请在评论中告知我)。 同时,这就足够了。 继续!
创建用户模型
Django用户模型非常简单。 我们将从AbstractBaseUser和PermissionsMixin类继承来创建模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
import jwt from datetime import datetime from datetime import timedelta from django.conf import settings from django.db import models from django.core import validators from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import PermissionsMixin class User(AbstractBaseUser, PermissionsMixin): """ Defines our custom user class. Username, email and password are required. """ username = models.CharField(db_index=True, max_length=255, unique=True) email = models.EmailField( validators=[validators.validate_email], unique=True, blank=False ) is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=True) # The `USERNAME_FIELD` property tells us which field we will use to log in. USERNAME_FIELD = 'email' REQUIRED_FIELDS = ('username',) # Tells Django that the UserManager class defined above should manage # objects of this type. objects = UserManager() def __str__(self): """ Returns a string representation of this `User`. This string is used when a `User` is printed in the console. """ return self.username @property def token(self): """ Allows us to get a user's token by calling `user.token` instead of `user.generate_jwt_token(). The `@property` decorator above makes this possible. `token` is called a "dynamic property". """ return self._generate_jwt_token() def get_full_name(self): """ This method is required by Django for things like handling emails. Typically this would be the user's first and last name. Since we do not store the user's real name, we return their username instead. """ return self.username def get_short_name(self): """ This method is required by Django for things like handling emails. Typically, this would be the user's first name. Since we do not store the user's real name, we return their username instead. """ return self.username def _generate_jwt_token(self): """ Generates a JSON Web Token that stores this user's ID and has an expiry date set to 60 days into the future. """ dt = datetime.now() + timedelta(days=60) token = jwt.encode({ 'id': self.pk, 'exp': int(dt.strftime('%s')) }, settings.SECRET_KEY, algorithm='HS256') return token.decode('utf-8') |
如您所见,令牌是使用@property装饰器动态创建的,有效期为60天。 创建新用户时将调用的UserManager如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
from django.contrib.auth.models import BaseUserManager class UserManager(BaseUserManager): """ Django requires that custom users define their own Manager class. By inheriting from `BaseUserManager`, we get a lot of the same code used by Django to create a `User`. All we have to do is override the `create_user` function which we will use to create `User` objects. """ def _create_user(self, username, email, password=None, **extra_fields): if not username: raise ValueError('The given username must be set') if not email: raise ValueError('The given email must be set') email = self.normalize_email(email) user = self.model(username=username, email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_user(self, username, email, password=None, **extra_fields): """ Create and return a `User` with an email, username and password. """ extra_fields.setdefault('is_staff', False) extra_fields.setdefault('is_superuser', False) return self._create_user(username, email, password, **extra_fields) def create_superuser(self, username, email, password, **extra_fields): """ Create and return a `User` with superuser (admin) permissions. """ extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) if extra_fields.get('is_staff') is not True: raise ValueError('Superuser must have is_staff=True.') if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') return self._create_user(username, email, password, **extra_fields) |
刚才有很多代码被遗弃了,但是我的目标是让您尽快起步并运行。 我建议逐行阅读代码,以确保您了解发生了什么(从Internet复制代码时,这通常是个好主意!)。 在Django中创建一个自定义用户只需要User类和UserManager! 只是不要忘记通过在settings.py文件中声明您的应用程序来让Django知道这些模型存在:
1 2 3 4 5 6 7 8 |
INSTALLED_APPS = [ ... 'rest_framework', 'authentication', # My'authentication` app ... ] AUTH_USER_MODEL = 'authentication.User' |
认证后端
默认情况下,Django不知道如何验证您的JWT。 要解决此问题,我们必须创建以下backends.py文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
import jwt from django.conf import settings from rest_framework import authentication, exceptions from .models import User class JWTAuthentication(authentication.BaseAuthentication): authentication_header_prefix = 'Bearer' def authenticate(self, request): """ The `authenticate` method is called on every request regardless of whether the endpoint requires authentication. `authenticate` has two possible return values: 1) `None` - We return `None` if we do not wish to authenticate. Usually this means we know authentication will fail. An example of this is when the request does not include a token in the headers. 2) `(user, token)` - We return a user/token combination when authentication is successful. If neither case is met, that means there's an error and we do not return anything. We simple raise the `AuthenticationFailed` exception and let Django REST Framework handle the rest. """ request.user = None # `auth_header` should be an array with two elements: 1) the name of # the authentication header (in this case, "Token") and 2) the JWT # that we should authenticate against. 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: # Invalid token header. No credentials provided. Do not attempt to # authenticate. return None elif len(auth_header) > 2: # Invalid token header. The Token string should not contain spaces. # Do not attempt to authenticate. return None # The JWT library we're using can't handle the `byte` type, which is # commonly used by standard libraries in Python 3. To get around this, # we simply have to decode `prefix` and `token`. This does not make for # clean code, but it is a good decision because we would get an error # if we didn't decode these values. prefix = auth_header[0].decode('utf-8') token = auth_header[1].decode('utf-8') if prefix.lower() != auth_header_prefix: # The auth header prefix is not what we expected. Do not attempt to # authenticate. return None # By now, we are sure there is a *chance* that authentication will # succeed. We delegate the actual credentials authentication to the # method below. return self._authenticate_credentials(request, token) def _authenticate_credentials(self, request, token): """ Try to authenticate the given credentials. If authentication is successful, return the user and token. If not, throw an error. """ try: payload = jwt.decode(token, settings.SECRET_KEY) except: msg = 'Invalid authentication. Could not decode token.' raise exceptions.AuthenticationFailed(msg) try: user = User.objects.get(pk=payload['id']) except User.DoesNotExist: msg = 'No user matching this token was found.' raise exceptions.AuthenticationFailed(msg) if not user.is_active: msg = 'This user has been deactivated.' raise exceptions.AuthenticationFailed(msg) return (user, token) |
同样,这是很多代码,但是如果您对Python和Django有一定的经验,我想认为这很简单。
我们还必须记得更新我们的settings.py文件,以告诉Django在哪里可以找到我们的自定义身份验证后端:
1 2 3 4 5 6 7 8 9 10 |
... REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'authentication.backends.JWTAuthentication', ) } ... |
至此,您已经创建了一个自定义的User模型和UserManager模型,并创建了一个自定义的JWTAuthentication类来对用户令牌进行身份验证。 最后遗漏的是设置用户视图以供DRF处理。
DRF Serializers
有几个视图需要序列化才能最终启动并运行。 第一个是RegistrationSerializer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
from rest_framework import serializers from .models import User class RegistrationSerializer(serializers.ModelSerializer): """ Creates a new user. Email, username, and password are required. Returns a JSON web token. """ # The password must be validated and should not be read by the client password = serializers.CharField( max_length=128, min_length=8, write_only=True, ) # The client should not be able to send a token along with a registration # request. Making `token` read-only handles that for us. token = serializers.CharField(max_length=255, read_only=True) class Meta: model = User fields = ('email', 'username', 'password', 'token',) def create(self, validated_data): return User.objects.create_user(**validated_data) |
该序列化程序将接收用户名,电子邮件和密码,并且如果身份验证成功,将返回用户令牌。 接下来,我们需要一种登录现有用户的方式。我们将为此创建一个LoginSerializer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class LoginSerializer(serializers.Serializer): """ Authenticates an existing user. Email and password are required. Returns a JSON web token. """ email = serializers.EmailField(write_only=True) password = serializers.CharField(max_length=128, write_only=True) # Ignore these fields if they are included in the request. username = serializers.CharField(max_length=255, read_only=True) token = serializers.CharField(max_length=255, read_only=True) def validate(self, data): """ Validates user data. """ 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.' ) user = authenticate(username=email, password=password) if user is None: raise serializers.ValidationError( 'A user with this email and password was not found.' ) if not user.is_active: raise serializers.ValidationError( 'This user has been deactivated.' ) return { 'token': user.token, } |
DRF视图
登录过程还将返回用户令牌,但前提是已经创建了该用户。 有了这两个序列化器之后,我们可以继续进行我们的view.py文件。 我们只需要包括一个用于注册和登录的视图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
from rest_framework import status from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView from .models import User from .serializers import LoginSerializer from .serializers import RegistrationSerializer class RegistrationAPIView(APIView): """ Registers a new user. """ permission_classes = [AllowAny] serializer_class = RegistrationSerializer def post(self, request): """ Creates a new User object. Username, email, and password are required. Returns a JSON web token. """ serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() return Response( { 'token': serializer.data.get('token', None), }, status=status.HTTP_201_CREATED, ) class LoginAPIView(APIView): """ Logs in an existing user. """ permission_classes = [AllowAny] serializer_class = LoginSerializer def post(self, request): """ Checks is user exists. Email and password are required. Returns a JSON web token. """ serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) return Response(serializer.data, status=status.HTTP_200_OK) |
最后一步是设置我们的urls.py文件,以将我们的视图映射到url。
1 2 3 4 5 6 7 8 9 |
from django.urls import re_path, include from .views import RegistrationAPIView from .views import LoginAPIView urlpatterns = [ re_path(r'^registration/?$', RegistrationAPIView.as_view(), name='user_registration'), re_path(r'^login/?$', LoginAPIView.as_view(), name='user_login'), ] |
最后
创建了所有这些文件后,我们现在可以使用我们的自定义Django模型注册和登录用户,并使用JSON Web令牌成功验证我们的用户。 尽管大多数信息已转储到此页面上,但我希望它对那些希望进行类似操作的人有所帮助。 继续骇客,朋友们!
原文:https://medium.com/@sebastianojeda/user-authentication-with-django-rest-framework-and-json-web-tokens-747ea4d84b9f