Is providing apis without Djangos REST Framework (DRF) a sidestep?

Token based authentication mechanism is more and more used in the project, especially for pure backend only provides API without project web page, for example, we often speak of before and after the separation structure of pure back-end services, only provides the API to the front, the front data provided through the API to render the page display or add modification, etc., We know that HTTP is a stateless protocol, meaning that the back-end service does not know who sent the request, so how do we verify the validity of the request? This requires some way of authenticating the request

What is the difference between traditional login authentication and token-based authentication

Djangos account and password login is used as an example to illustrate how traditional authentication works. When you enter your account and password on the login page and submit a form, a request is sent to the server. The server authenticates the password. The user information is recorded in the server (djanGO_session table), and a sessionID is returned to the browser to uniquely identify the user. The browser saves the sessionID in the cookie, and then sends the sessionID to the server together with every request from the browser. The server compares the sessionid with the recorded information to authenticate the identity

The client logs in with its own account and password, and the server authenticates the authentication. Authentication returns the Token to the client by generating the Token. Then the client sends the Token in the header together with each request

The main purpose of a session is to add state keepsaws to the stateless HTTP protocol, which is generally common when the browser is the client. The main purpose of Token is authentication, and it does not need to consider CSRF protection and cross-domain issues. Therefore, it is more used to provide apis for third parties. Client requests can be well supported no matter they are initiated by browsers or other programs. Therefore, token-based authentication mechanism has almost become the authentication standard for front – end separation architecture or external API access, and is widely used

JSON Web Token (JWT) is one of the most popular implementations of Token authentication. There are a lot of articles about JWT on the Web, but we won’t go into details here. Django uses JWT to authenticate apis. If your project does not use the DRF framework and you do not want to introduce a large and complex DRF framework just for authentication apis, you can read on

My requirements are as follows:

  1. The same view function not only provides data to the front-end page, but also provides API services externally, which should meet the requirements of account password based authentication and JWT authentication
  2. The project uses Django’s default permission system, which validates permissions for both account and password logins and jWT-BASED requests

PyJWT introduction

To implement requirement 1, we first need to introduce the JWT module. Python has a PyJWT module that can be used directly. Let’s look at the simple usage of JWT

Install PyJWT
$ pip install pyjwt
Copy the code
Generate tokens using PyJWT
>>> import jwt
>>> encoded_jwt = jwt.encode({'username':Operation coffee Bar.'site':'https://ops-coffee.cn'},'secret_key',algorithm='HS256')
Copy the code

There are three parts that are passed to JWT,

The first part is a Json object called Payload, which is used to hold valid information, like the username, the expiration date, whatever you want to pass along

The second part is a secret key string, which is mainly used in the following Signature. The secret key is used by the server to verify the validity of the Token. The secret key is only known by the server and cannot be disclosed

The third part specifies the algorithm of Signature

View the generated Token
>>> print(encoded_jwt)
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL2 9wcy1jb2ZmZWUuY24ifQ.fIpSXy476r9F9i7GhdYFNkd-2Ndz8uKLgJPcd84BkJ4'
Copy the code

The Token generated by JWT is one with two dots (.) Long string split

Header Header, Payload Payload, and Signature Signature: header.paypay.signature

JWT is not encrypted, and anyone can read the information in it. The first Header and the second Payload are only base64 encoded for the original input information, and the third Signature is encrypted with Header, Payload and secret_key

You can use Base64 to decode Header and Payload to obtain corresponding information

>>> import base64
>>> base64.b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')
b'{"typ":"JWT","alg":"HS256"}'

>>> base64.b64decode('eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ==')
Base64 decodes an object whose length is not 2, requiring one or two = signs at the end of the argument
Copy the code

JWT does not encrypt the result, so do not store sensitive information in the Header or Payload. The server mainly relies on the last Signature to verify whether the Token is valid and whether it has been tampered

Decrypt the Token
>>> jwt.decode(encoded_jwt,'secret_key',algorithms=['HS256'])
{'username': Operation coffee Bar.'site': 'https://ops-coffee.cn'}
Copy the code

If the server has a secret key, it can directly decrypt the Token generated by JWT. If the decryption succeeds, the Token is correct and the data is not tampered with

Of course, JWT does not encrypt data. If you do not have the secret_key, you can directly obtain the data in the Payload. However, without the signature algorithm, you cannot verify whether the data is accurate

>>> jwt.decode(encoded_jwt, verify=False)
{'username': Operation coffee Bar.'site': 'https://ops-coffee.cn'}
Copy the code

Django case

Djangos need to support both session authentication and JWT authentication, and they need to share the same permission system. Here’s a Django solution: Decorators, such as login_required to check whether the user is logged in and permission_required to check whether the user has permissions, can be implemented by ourselves to check the user’s authentication mode and verify that the user has permissions after authentication

An auth_permission_required decorator is generated:

from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied

UserModel = get_user_model()


def auth_permission_required(perm):
    def decorator(view_func):
        def _wrapped_view(request, *args, **kwargs):
            # format permission
            perms = (perm,) if isinstance(perm, str) else perm

            if request.user.is_authenticated:
                Check whether the user has permission
                if not request.user.has_perms(perms):
                    raise PermissionDenied
            else:
                try:
                    auth = request.META.get('HTTP_AUTHORIZATION').split()
                except AttributeError:
                    return JsonResponse({"code": 401, "message": "No authenticate header"})

                # The user gets the data validation process through the API
                if auth[0].lower() == 'token':
                    try:
                        dict = jwt.decode(auth[1], settings.SECRET_KEY, algorithms=['HS256'])
                        username = dict.get('data').get('username')
                    except jwt.ExpiredSignatureError:
                        return JsonResponse({"status_code": 401, "message": "Token expired"})
                    except jwt.InvalidTokenError:
                        return JsonResponse({"status_code": 401, "message": "Invalid token"})
                    except Exception as e:
                        return JsonResponse({"status_code": 401, "message": "Can not get user object"})

                    try:
                        user = UserModel.objects.get(username=username)
                    except UserModel.DoesNotExist:
                        return JsonResponse({"status_code": 401, "message": "User Does not exist"})

                    if not user.is_active:
                        return JsonResponse({"status_code": 401, "message": "User inactive or deleted"})

                    # Token indicates whether the user has permission
                    if not user.has_perms(perms):
                        return JsonResponse({"status_code": 403, "message": "PermissionDenied"})
                else:
                    return JsonResponse({"status_code": 401, "message": "Not support auth type"})

            return view_func(request, *args, **kwargs)

        return _wrapped_view

    return decorator
Copy the code

This decorator can be used to replace the login_required and permission_required decorators when the view is used

@auth_permission_required('account.select_user')
def user(request):
    if request.method == 'GET':
        _jsondata = {
            "user": "ops-coffee"."site": "https://ops-coffee.cn"
        }
        
        return JsonResponse({"state": 1, "message": _jsondata})
    else:
        return JsonResponse({"state": 0."message": "Request method 'POST' not supported"})
Copy the code

We also need a method to generate User tokens, handled by adding a static method to the User model with a Token

class User(AbstractBaseUser, PermissionsMixin):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='Creation time')
    update_time = models.DateTimeField(auto_now=True, verbose_name='Update Time')
    username = models.EmailField(max_length=255, unique=True, verbose_name='Username')
    fullname = models.CharField(max_length=64, null=True, verbose_name='Chinese name')
    phonenumber = models.CharField(max_length=16, null=True, unique=True, verbose_name='phone')
    is_active = models.BooleanField(default=True, verbose_name='Active state')

    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.username

    @property
    def token(self):
        return self._generate_jwt_token()

    def _generate_jwt_token(self):
        token = jwt.encode({
            'exp': datetime.utcnow() + timedelta(days=1),
            'iat': datetime.utcnow(),
            'data': {
                'username': self.username
            }
        }, settings.SECRET_KEY, algorithm='HS256')

        return token.decode('utf-8')

    class Meta:
        default_permissions = ()

        permissions = (
            ("select_user"."View user"),
            ("change_user"."Modify user"),
            ("delete_user"."Delete user"),Copy the code

Tokens can be generated directly from user objects:

>>> from accounts.models import User
>>> u = User.objects.get(username='[email protected]')
>>> u.token
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDgyMjg3NzksImlhdCI6MTU0ODE0MjM3OSwiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluQD E2My5jb20ifX0.akZNU7t_z2kwPxDJjmc-QxtNdICK0yhnwWmKxqqXKLw'
Copy the code

After the generated Token is sent to the client, the client can use the Token for authentication

>>> import requests
>>> token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDgyMjg4MzgsImlhdCI6MTU0ODE0MjQzOCwiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluQD E2My5jb20ifX0.oKc0SafgksMT9ZIhTACupUlz49Q5kI4oJA-B8-GHqLA'
>>>
>>> r = requests.get('http://localhost/api/user', headers={'Authorization': 'Token '+token})
>>> r.json()
{'username': '[email protected]'.'fullname': Operation coffee Bar.'is_active': True}
Copy the code

The auth_permission_required method does all the work.


If you’re not enjoying your reading, read the following:

  • Django+Echarts drawing example
  • Djangos password management table