使用djangorestframework和simplejwt进行基于角色的身份验证和授权

Authentication and Authorization are two difficult concepts in programming. Lucky for us, Django makes our lives super easy and handles those things for us.

身份验证和授权是编程中的两个困难概念。 对于我们来说幸运的是,Django使我们的生活变得异常轻松,并为我们处理了这些事情。

Recently, I have been working on a Django REST API that has around seven roles in it, all have different permissions for both the back-end API and the web app, which displays different views based on roles. This created a problem, as Session Authorization is great until we start working with mobile apps, which is the next step in this application. A JWT token was needed for authentication, but how was I to handle authorization? Let’s take a look at how to add role-based authorization to your Django REST apps.

最近,我一直在研究Django REST API,其中包含大约七个角色,它们都对后端API和Web应用程序具有不同的权限,这些Web应用程序根据角色显示不同的视图。 这就产生了一个问题,因为在我们开始使用移动应用程序之前,会话授权非常有用,这是此应用程序的下一步。 身份验证需要JWT令牌,但是如何处理授权? 让我们看一下如何向Django REST应用程序添加基于角色的授权。

项目设置 (Project Setup)

You should have Python already installed on your system. Let’s create our virtual environment and get Django installed.

您应该已经在系统上安装了Python。 让我们创建我们的虚拟环境并安装Django。

First, create the directory for your Django project and cd into it:

首先,为您的Django项目创建目录并cd进入该目录:

mkdir my-django-app && cd "$_"

Next, we need to create our virtual environment:

接下来,我们需要创建我们的虚拟环境:

// macOS and Linux
python3 -m venv venv// Windows
python -m venv venv

Active your environment with:

通过以下方式激活您的环境:

// macOS and Linux
source venv/bin/activate// Windows
.\venv\Scripts\activate

And finally, install your dependencies:

最后,安装依赖项:

// macOS and Linux
pip3 install django // Windows
pip install django

Freeze your requirements with:

通过以下方式冻结您的需求:

// macOS and Linux
pip3 freeze > requirements.txt// Windows
pip freeze > requirements.txt

Now we can start writing some code!

现在我们可以开始编写一些代码了!

创建您的Django应用 (Creating your Django App)

Now we are ready to start our Django project. Start with:

现在我们准备开始我们的Django项目。 从...开始:

django-admin startproject my_django_app

This will create a new folder in your current directory with a folder of the same name and a file named manage.py inside of it. You need to move those files to your current directory. Once you do that, you can remove the original folder that was created. The folder structure should like like the following:

这将在您的当前目录中创建一个新文件夹,其中包含相同名称的文件夹以及其中的一个名为manage.py的文件。 您需要将这些文件移动到当前目录。 完成此操作后,您可以删除创建的原始文件夹。 文件夹结构应如下所示:

使用djangorestframework和simplejwt进行基于角色的身份验证和授权_第1张图片

Next, let’s create an app. Run the following command to start a project:

接下来,让我们创建一个应用程序。 运行以下命令以启动项目:

// macOS and Linux
python3 manage.py startapp api// Windows
python manage.py startapp api

创建模型和经理 (Create Your Model and Manager)

We are going to create a custom user for our authentication needs. Add in the following code below to create our user:

我们将为身份验证需求创建一个自定义用户。 在下面添加以下代码以创建我们的用户:

import uuid


from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils import timezone


from .managers import CustomUserManager


# Create your models here.
class User(AbstractBaseUser, PermissionsMixin):


    # These fields tie to the roles!
    ADMIN = 1
    MANAGER = 2
    EMPLOYEE = 3


    ROLE_CHOICES = (
        (ADMIN, 'Admin'),
        (MANAGER, 'Manager'),
        (EMPLOYEE, 'Employee')
    )
    
    class Meta:
        verbose_name = 'user'
        verbose_name_plural = 'users'

Then, we can add fields to our user:

然后,我们可以向用户添加字段:

import uuid


from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils import timezone


from .managers import CustomUserManager


# Create your models here.
class User(AbstractBaseUser, PermissionsMixin):


  # Roles created here
  uid = models.UUIDField(unique=True, editable=False, default=uuid.uuid4, verbose_name='Public identifier')
  email = models.EmailField(unique=True)
  first_name = models.CharField(max_length=30, blank=True)
  last_name = models.CharField(max_length=50, blank=True)
  role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, blank=True, null=True, default=3)
  date_joined = models.DateTimeField(auto_now_add=True)
  is_active = models.BooleanField(default=True)
  is_deleted = models.BooleanField(default=False)
  created_date = models.DateTimeField(default=timezone.now)
  modified_date = models.DateTimeField(default=timezone.now)
  created_by = models.EmailField()
  modified_by = models.EmailField()


  USERNAME_FIELD = 'email'
  REQUIRED_FIELDS = []


    objects = CustomUserManager()


    def __str__(self):
        return self.email

The code above is just normal Django code. I replaced the default username field with an email address and I also removed is_staff and is_superuser. These fields are fine to have if you have a use for them. In this example, we don’t have any. We are also setting the default role of a ‘Employee’ for all new users in our application.

上面的代码只是普通的Django代码。 我用电子邮件地址替换了默认用户名字段,并且还删除了is_staff和is_superuser。 如果您需要使用这些字段,则很好。 在这个例子中,我们没有任何东西。 我们还为应用程序中的所有新用户设置了“雇员”的默认角色。

You may have noticed I also added an extra UUID field called uid to the model, but not at the primary key. I like to use a UUID as a public identifier that we can render in a URL. I only put it on resources that will be displayed on their own, not internal or nested tables. This way, I still have an integer field as my PK that I can easily do queries on in the database and I have a safer identifier to use in my URL’s.

您可能已经注意到,我还向模型添加了一个名为uid的额外UUID字段,但未在主键上添加。 我喜欢使用UUID作为我们可以在URL中呈现的公共标识符。 我只将其放在将显示在自己的资源上,而不放在内部或嵌套表上。 这样,我仍然有一个整数字段作为PK,可以轻松地在数据库中进行查询,并且可以在URL中使用更安全的标识符。

A user manager class allows you to write your own logic for creating users and superusers. Add a managers.py file to your api app and add the following code to it:

用户管理器类使您可以编写自己的逻辑来创建用户和超级用户。 将manager.py文件添加到您的api应用中,并向其中添加以下代码:

from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _




class CustomUserManager(BaseUserManager):
    """
    Custom user model where the email address is the unique identifier
    and has an is_admin field to allow access to the admin app 
    """
    def create_user(self, email, password, **extra_fields):
        if not email:
            raise ValueError(_("The email must be set"))
        if not password:
            raise ValueError(_("The password must be set"))
        email = self.normalize_email(email)


        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user


    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_active', True)
        extra_fields.setdefault('role', 1)


        if extra_fields.get('role') != 1:
            raise ValueError('Superuser must have role of Global Admin')
        return self.create_user(email, password, **extra_fields)

Notice here when I create my superuser, I am setting my role to 1, or ‘Admin’. In this case, I want to make a super user when I create an admin, but you can set this field to whatever role you want. Again, this is just standard Django code.

请注意,当我创建我的超级用户时,我将角色设置为1,即“管理员”。 在这种情况下,我想在创建管理员时成为超级用户,但是您可以将此字段设置为所需的任何角色。 同样,这只是标准的Django代码。

更新设置并进行迁移 (Update settings and Make Migrations)

Now we need to go into our settings.py file and add a couple of fields. We first need to add our new app to the INSTALLED_APPS and setup our settings for authentication and for the JWT.

现在,我们需要进入settings.py文件,并添加几个字段。 首先,我们需要将新应用添加到INSTALLED_APPS,并设置身份验证和JWT的设置。

# Other settings not shown
# Set your auth user to the new user you have created
AUTH_USER_MODEL = 'api.User'


# We need to add our api app and the rest framework to INSTALLED_APPS
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api.apps.ApiConfig'
]


# App the REST framework url conf
ROOT_URLCONF = 'django_rest_role_jwt.urls'


# REST framework settings
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated'
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication'
    ),
}


# Configure the JWT settings
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': False,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUTH_HEADER_TYPES': ('JWT',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
}

These settings tell Django to use the JWT token as the default authentication schema. The settings for the JWT token are the default settings from the SimpleJWT docs.

这些设置告诉Django使用JWT令牌作为默认身份验证架构。 JWT令牌的设置是SimpleJWT文档中的默认设置。

Next, we need to run our database migrations. Use the following command to run your migrations:

接下来,我们需要运行数据库迁移。 使用以下命令运行迁移:

// macOS and Linux
python3 manage.py makemigrations// Windows
python manage.py makemigrations

Then to apply the migrations:

然后应用迁移:

// macOS and Linux
python3 manage.py migrate// Windows
python manage.py migrate

We are now ready to start building our serializers.

现在,我们准备开始构建序列化器。

创建序列化器 (Creating Serializers)

In Django Rest Framework, a serialzier is used to take complex data and put it into a standard Python data form to be used as JSON, XML, or other content types. Let’s create a serializer for our registration process.

在Django Rest Framework中,serialzier用于获取复杂数据并将其放入标准Python数据格式中,以用作JSON,XML或其他内容类型。 让我们为我们的注册过程创建一个序列化器。

from .models import User




class UserRegistrationSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = (
            'email',
            'password'
        )


    def create(self, validated_data):
        auth_user = User.objects.create_user(**validated_data)
        return auth_user

All we need to register a user is their email address and a password.

我们只需要注册一个用户,便是他们的电子邮件地址和密码。

Now, we need a serializer to handle our logins. Here, we need to authenticate a user based on a given email and password and return the JWT back to the user.

现在,我们需要一个序列化程序来处理我们的登录。 在这里,我们需要根据给定的电子邮件和密码对用户进行身份验证,然后将JWT返回给用户。

from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken




class UserLoginSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField(max_length=128, write_only=True)
    access = serializers.CharField(read_only=True)
    refresh = serializers.CharField(read_only=True)
    role = serializers.CharField(read_only=True)


    def create(self, validated_date):
        pass


    def update(self, instance, validated_data):
        pass


    def validate(self, data):
        email = data['email']
        password = data['password']
        user = authenticate(email=email, password=password)


        if user is None:
            raise serializers.ValidationError("Invalid login credentials")


        try:
            refresh = RefreshToken.for_user(user)
            refresh_token = str(refresh)
            access_token = str(refresh.access_token)


            update_last_login(None, user)


            validation = {
                'access': access_token,
                'refresh': refresh_token,
                'email': user.email,
                'role': user.role,
            }


            return validation
        except AuthUser.DoesNotExist:
            raise serializers.ValidationError("Invalid login credentials")

The login serializer is pretty straightforward. We are defining all required fields for handling login requests. Note that password is write_only. This is to prevent a password in plain text from being returned back to the user as apart of the response. We also have an access and refresh token. The access token lets the user access resources and expires after five minutes by default. When it expires, we send the refresh token back to the server and have it return another access token. More on that in the SimpleJWT docs.

登录序列化器非常简单。 我们正在定义用于处理登录请求的所有必填字段。 请注意,密码为write_only。 这是为了防止将纯文本密码作为响应的一部分返回给用户。 我们也有一个访问和刷新令牌。 访问令牌使用户可以访问资源,并且默认情况下五分钟后过期。 过期后,我们会将刷新令牌发送回服务器,并让它返回另一个访问令牌。 有关更多内容,请参见SimpleJWT文档。

Finally, we need one more serializer to handle returning all users data. This object can only be accessed by Admins. This is the route we will use our role authorization for.

最后,我们还需要一个串行器来处理返回所有用户数据。 该对象只能由管理员访问。 这是我们将使用角色授权的路线。

class UserListSerializer(serializers.ModelSerializer):
    class Meta:
        model = AuthUser
        fields = (
            'email',
            'role'
        )

This serializer is really basic. We just want to get the email addresses and user roles back from the API.

这个序列化器真的很基础。 我们只想从API获取电子邮件地址和用户角色。

创建视图,更新URL和运行测试 (Creating Views, Update URLs, and Run Tests)

We need to create three views. One for registration, one for login, and one for accessing all the users.

我们需要创建三个视图。 一种用于注册,一种用于登录,另一种用于访问所有用户。

Let’s first create a registration view:

首先创建一个注册视图:

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated


from .serializers import (
    UserRegistrationSerializer,
    UserLoginSerializer,
    UserListSerializer
)


from .models import User




class AuthUserRegistrationView(APIView):
    serializer_class = UserRegistrationSerializer
    permission_classes = (AllowAny, )


    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        valid = serializer.is_valid(raise_exception=True)


        if valid:
            serializer.save()
            status_code = status.HTTP_201_CREATED


            response = {
                'success': True,
                'statusCode': status_code,
                'message': 'User successfully registered!',
                'user': serializer.data
            }


            return Response(response, status=status_code)

Now, we need to create a view for logins:

现在,我们需要为登录创建一个视图:

class AuthUserLoginView(APIView):
    serializer_class = UserLoginSerializer
    permission_classes = (AllowAny, )


    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        valid = serializer.is_valid(raise_exception=True)


        if valid:
            status_code = status.HTTP_200_OK


            response = {
                'success': True,
                'statusCode': status_code,
                'message': 'User logged in successfully',
                'access': serializer.data['access'],
                'refresh': serializer.data['refresh'],
                'authenticatedUser': {
                    'email': serializer.data['email'],
                    'role': serializer.data['role']
                }
            }


            return Response(response, status=status_code)

Finally, we need a view for our list of users:

最后,我们需要一个用户列表视图:

class UserListView(APIView):
    serializer_class = UserListSerializer
    permission_classes = (IsAuthenticated,)


    def get(self, request):
        user = request.user
        if user.role != 1:
            response = {
                'success': False,
                'status_code': status.HTTP_403_FORBIDDEN,
                'message': 'You are not authorized to perform this action'
            }
            return Response(response, status.HTTP_403_FORBIDDEN)
        else:
            users = AuthUser.objects.all()
            serializer = self.serializer_class(users, many=True)
            response = {
                'success': True,
                'status_code': status.HTTP_200_OK,
                'message': 'Successfully fetched users',
                'users': serializer.data


            }
            return Response(response, status=status.HTTP_200_OK)

Here, we are setting the permissions classes to IsAuthenticated. This stops users who aren’t logged from accessing this information. Next, we need to check the users role. We can get that from request.user.role. If the role is 1 or is an admin, then they can access the requested resource.

在这里,我们将权限类设置为IsAuthenticated。 这将阻止未登录的用户访问此信息。 接下来,我们需要检查用户角色。 我们可以从request.user.role获得它。 如果角色是1或是管理员,则他们可以访问请求的资源。

Now we need to update our URLs to be able to hit our endpoints. Create a urls.py file and add the following code:

现在,我们需要更新我们的URL,以便能够访问我们的端点。 创建一个urls.py文件并添加以下代码:

from django.urls import path
from rest_framework_simplejwt import views as jwt_views


from .views import (
    UserRegistrationView,
    UserLoginView,
    UserListView
)


urlpatterns = [
    path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'),
    path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
    path('register', UserRegistrationView.as_view(), name='register'),
    path('login', UserLoginView.as_view(), name='login'),
    path('users', UserListView.as_view(), name='users')
]

We now need to import them to our main urls.py file:

现在,我们需要将它们导入到我们的主要urls.py文件中:

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/auth/', include('api.urls'))
]

Let’s add some tests to see if it works. Open up test.py and add the following code.

让我们添加一些测试以查看其是否有效。 打开test.py并添加以下代码。

import json
from django.urls import include, path, reverse
from rest_framework import status
from rest_framework.test import APITestCase, APIClient, URLPatternsTestCase


from .models import User


# Create your tests here.
class UserTest(APITestCase, URLPatternsTestCase):
    """ Test module for User """


    urlpatterns = [
        path('api/auth/', include('api.urls')),
    ]


    def setUp(self):
        self.user1 = User.objects.create_user(
            email='[email protected]',
            password='test',
        )


        self.admin = User.objects.create_superuser(
            email='[email protected]',
            password='admin',
        )


    def test_login(self):
        """ Test if a user can login and get a JWT response token """
        url = reverse('login')
        data = {
            'email': '[email protected]',
            'password': 'admin'
        }
        response = self.client.post(url, data)
        response_data = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response_data['success'], True)
        self.assertTrue('access' in response_data)


    def test_user_registration(self):
        """ Test if a user can register """
        url = reverse('register')
        data = {
            'email': '[email protected]',
            'password': 'test',
        }
        response = self.client.post(url, data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)


    def test_list_all_users_as_admin(self):
        """ Test fetching all users. Restricted to admins """
        # Setup the token
        url = reverse('login')
        data = {'email': '[email protected]', 'password': 'admin'}
        response = self.client.post(url, data)
        login_response_data = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertTrue('access' in login_response_data)
        token = login_response_data['access']


        # Test the endpoint
        client = APIClient()
        client.credentials(HTTP_AUTHORIZATION='JWT ' + token)
        response = client.get(reverse('users'))
        response_data = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(User.objects.count(), len(response_data['users']))


    def test_access_denied_all_users(self):
        """ Test fetching all users. Restricted to admins """
        # Setup the token
        url = reverse('login')
        data = {'email': '[email protected]', 'password': 'test'}
        response = self.client.post(url, data)
        login_response_data = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertTrue('access' in login_response_data)
        token = login_response_data['access']


        # Test the endpoint
        client = APIClient()
        client.credentials(HTTP_AUTHORIZATION='JWT ' + token)
        response = client.get(reverse('users'))
        response_data = json.loads(response.content)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
        self.assertFalse(response_data['success'])

All the tests should run successfully.

所有测试均应成功运行。

In this guide, we looked at how to implement role based authorization in Django REST views with JWT authentication. We created a custom user object and tied a role to it, as well as secured our routes. We also wrote unit tests to test our views.

在本指南中,我们研究了如何通过JWT身份验证在Django REST视图中实现基于角色的授权。 我们创建了一个自定义用户对象并为其绑定了角色,并保护了路由。 我们还编写了单元测试来测试我们的观点。

One thing that could be expanded on is if there is a need for multiple roles, you could create a separate role table and create a many to many relationship between that and your custom user object. This would need a little more detailed serializer, but as a whole, is completely doable. It all just depends on what you need in your application.

可以扩展的一件事是,如果需要多个角色,则可以创建一个单独的角色表,并在该表和自定义用户对象之间创建多对多关系。 这将需要一些更详细的序列化器,但总体而言是完全可行的。 这完全取决于您在应用程序中需要什么。

Full project can be found here: https://github.com/BrockHerion/django-rest-role-jwt

完整的项目可以在这里找到: https : //github.com/BrockHerion/django-rest-role-jwt

翻译自: https://medium.com/@brockjosephherion/role-based-authentication-and-authorization-with-djangorestframework-and-simplejwt-d9614d79995c

你可能感兴趣的:(使用djangorestframework和simplejwt进行基于角色的身份验证和授权)