drf JWT認證模塊與自定製

JWT模塊

   在djangorestframework中,有一款擴展模塊可用於做JWT認證,使用如下命令進行安裝:

pip install djangorestframework-jwt

   現在,就讓我們開始使用它吧。

JWT配置

   該模塊的所有配置都會從settings.py中進行讀取,與drf一樣,它會先去讀取項目全局文件夾下的settings.py,再去讀取自身的settings.py,所以如果我們要對JWT進行配置,則在項目全局文件夾下的settings.py中進行配置即可:

import datetime
JWT_AUTH = {
    # 配置過期時間
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    # 配置請求頭中攜帶token的前綴
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

   如果你想了解更多配置,則可查看該模塊讀取的默認配置文件。

from rest_framework_jwt import settings

   在默認配置文件中,你可以看到如下代碼,它會先去全局中找配置,再到局部中找配置:

USER_SETTINGS = getattr(settings, 'JWT_AUTH', None)

auth組件

   下面將介紹如何使用auth組件與JWT配套使用,這當然非常方便,auth組件可以說是Django的核心。

   我打算這樣做,對內置的user表做擴展,添加頭像字段,只有用戶登錄後才能修改頭像,否則將會採用默認頭像。

準備工作

   首先我們需要對內置的auth_user表做擴展,如下所示:

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    avatar = models.FileField(upload_to="avatar",default="avatar/default.png")

   其次配置上傳文件的路徑,聲明media所在的位置,以及聲明我們對內置的auth_user表做了擴展:

MEDIA_ROOT = BASE_DIR / "media"
AUTH_USER_MODEL = "app01.User"

# python manage.py makemigrations
# python manage.py migrate

   最後打開資源暴露接口:

from django.contrib import admin
from django.urls import path,re_path
from django.views.static import serve
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r"^media/(?P<path>.*)", serve, {"document_root": settings.MEDIA_ROOT}),
]

註冊API

   現在,我們需要來做一個註冊的API接口,如下所示:

class Register(ViewSet):
    def register(self,request,*args,**kwargs):
        serializer = UserModelSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(data=serializer.data,status=status.HTTP_201_CREATED)
        return Response(data=serializer.errors,status=status.HTTP_401_UNAUTHORIZED)

   我們可以規定register這個方法必須是POST請求才能訪問,在url中進行配置(ViewSetViewSetMixin的子類,所以有actions參數):

path('register/', views.Register.as_view(actions={"post":"register"})),

   由於auth_user的密碼需要密文,所以我們重寫了模型序列化器的create方法。

from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from app01 import models

class UserModelSerializer(serializers.ModelSerializer):
    re_password = serializers.CharField(required=True, write_only=True)
    # 數據表中不存在該字段,我們自己寫一個

    class Meta:
        model = models.User
        fields = ("username","password","re_password","email")
        extra_kwargs = {
            "password":{"write_only":True}
        }

    def create(self, validated_data):
        password = validated_data.get("password")
        re_password = validated_data.get("re_password")
        email = validated_data.get("email")


        if re_password != password:
            raise ValidationError("兩次密碼輸入不一致")

        if models.User.objects.filter(email=email):
            raise ValidationError("郵箱已被註冊")


        validated_data.pop("re_password")  # 刪除即可,然後寫入
        user_obj = models.User.objects.create_user(**validated_data)  # 加密創建
        return user_obj

簽發token

   下面將實現登錄接口,如果你使用了auth組件作為擴展那麼登錄接口將十分的簡單。

   JWT模塊已經全部幫你完成了,你只需要向下面這麼做:

from rest_framework_jwt.views import obtain_jwt_token  # 導入視圖,它都寫好了的,並且會做驗證
from rest_framework_jwt.views import ObtainJSONWebToken  # 上面是一個變量,內部實際上是 obtain_jwt_token=ObtainJSONWebToken.as_view()

urlpatterns = [
	path('login/', obtain_jwt_token),
	# path('login/', ObtainJSONWebToken.as_view()),
]

   現在,當我們向該接口發送POST請求時,如果校驗全部通過,則會發送給我們一個JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTYyMzcyLCJlbWFpbCI6IjIzMjNAcXEuY29tIn0._SmZ0e0mj5QVOKUftAwI3xBX4_BOw1ZNjAi94_U3mXg

JWT認證

   下面我們來實現修改頭像,頭像必須先登錄才能修改,所以要添加JWT認證。

from rest_framework.permissions import IsAuthenticated  # 導入權限
from rest_framework_jwt.authentication import JSONWebTokenAuthentication  # 導入認證

class SetAvatar(ViewSet):

    authentication_classes = [JSONWebTokenAuthentication]  # 存儲到request.user,如果只配置這個,則不登陸也能訪問
    permission_classes = [IsAuthenticated]  # 必須已經登陸,即request.user不能是匿名用戶

    def set_avatar(self,request,*args,**kwargs):
        serializer = UserSetAvatar(instance=request.user,data=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(data="修改成功",status=status.HTTP_205_RESET_CONTENT)
        return Response(data="修改失敗",status=status.HTTP_401_UNAUTHORIZED)

   序列類如下:

class UserSetAvatar(serializers.ModelSerializer):
    class Meta:
        model = models.User
        fields = ("avatar",)
        extra_kwargs = {
            "avatar":{"write_only":True},
        }

   url配置:

path('setavatar/', views.SetAvatar.as_view(actions={"post":"set_avatar"})),

   現在,我們使用POSTMAN來發送請求,首先要先登錄,獲得JWT:

   image-20201105155846022

   然後需要在請求頭中添加JWT認證,並且在body體中添加新頭像:

   需要注意的是在添加JWT認證時,需要在VALUE處添加前綴JWT 隨機字符串,以這樣的格式提交,這是因為settings.py中設置了前綴。

   image-20201105155937569

   image-20201105155949700

   最後點擊send,將會提示我們修改成功。

全局使用

   要在全局使用JWT認證,方式如下,它將作用於所有視圖:

REST_FRAMEWORK = {
    # 認證模塊
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES':{
    	'rest_framework.permissions.IsAuthenticated',
    }
}

   如果你想取消某一個視圖的認證功能,則添加空列表即可:

authentication_classes = []  
permission_classes = [] 

   局部使用參見JWT認證中的書寫。

JWT認證通過返回信息定製

   在上面的示例中,我們可以看見在用戶登錄之後,返回信息只有一個JWT字符串,那麼我們可不可以將已登錄的用戶名字返回呢?也是可以的。

   jwt_response_payload_handler()這個函數就是控制返回格式的,我們可以覆寫它然後在settings.py中進行配置。

   如下所示:

def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'status': 0,
        'msg': 'ok',
        'data': {
            'token': token,
            'user': UserModelSerializers(user).data
        }
    }

   在settings.py中進行配置,該項配置是配置在REST_FRAMEWORK中,而不是JWT_AUTH中,一定要注意:

REST_FRAMEWORK = {
    # 配置自定義登錄成功後的返回信息
    'JWT_RESPONSE_PAYLOAD_HANDLER':"utils.jwt_response_payload_handler",
}

   最後登錄完成的結果如下:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTY0Njk3LCJlbWFpbCI6IjIzMjNAcXEuY29tIn0.cvmM6LvoVkSQETybss3fVVGZNXT099o8U21tzDvdFe4",
    "username": "yunya"
}

JWT認證流程源碼閱讀

   又開始愉快的讀源碼環節了,那麼JWT的源碼還是比較簡單的,下面一起看一看。

簽發流程

   首先,我們來分析一下為什麼只寫了下面一個登錄接口,甚至都沒寫視圖,就可以完成簽發。

from rest_framework_jwt.views import obtain_jwt_token  # 導入視圖,它都寫好了的,並且會做驗證
from rest_framework_jwt.views import ObtainJSONWebToken  # 上面是一個變量,內部實際上是 obtain_jwt_token=ObtainJSONWebToken.as_view()

urlpatterns = [
	path('login/', obtain_jwt_token),
]

   先看obtain_jwt_token,可以發現這個代碼:

obtain_jwt_token = ObtainJSONWebToken.as_view()

   我們發現了一個ObtainJSONWebToken這個類,它會執行as_view()方法,先不管,看看它繼承了誰:

   image-20201105165232378

   它繼承了JSONWebTokenAPIView,並且該類又繼承了APIView,那麼這個APIView的視圖的源碼已經閱讀過不下五次了,可以查看之前的文章。我們直接來看關於登錄的認證,JSONWebTOkenAPIView中實現了一個post方法:

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)  # 可以發現它有一個自帶的序列化器
        if serializer.is_valid():  # 直接進行驗證
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

   現在看到序列化器對吧,序列化器其實在這裡:

class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer

   這個是序列化器的源碼,在__init__方法中實現了字段:

class JSONWebTokenSerializer(Serializer):

    def __init__(self, *args, **kwargs):

        super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
        self.fields[self.username_field] = serializers.CharField()  # username字段
        self.fields['password'] = PasswordField(write_only=True)  # password字段

    @property
    def username_field(self):
        return get_username_field()

    def validate(self, attrs):
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            user = authenticate(**credentials)  # 這裡是執行認證。

            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user)  # 拿到荷載信息

                return {
                    'token': jwt_encode_handler(payload),   # 荷載信息放進去,來生成JWT的token字符串
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

   也就是說,在執行JSONWebTOkenAPIViewpost()方法時,會走一次數據庫查詢,根據提交的用戶名和密碼來拿到用戶對象。並且在序列化器中,會生成token()信息。他們都是配置好的一些函數:

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER

   (我們可不可以自己寫一個驗證類來覆蓋它,然後用於多端登錄呢?手機號,郵箱等都可以登錄)

   我們繼續來看post()方法:

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)  # 可以發現它有一個自帶的序列化器
        if serializer.is_valid():  # 直接進行驗證
            user = serializer.object.get('user') or request.user # request.user等於已經登錄的用戶
            token = serializer.object.get('token')  # token等於生成的jwt隨機字符串
            response_data = jwt_response_payload_handler(token, user, request)  # 設置返回信息
            response = Response(response_data) 
            if api_settings.JWT_AUTH_COOKIE:  # 未設置,不走
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response  # 直接返回

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)  # 沒驗證通過

   OK,自動簽發的流程已經走完了。

   大概的縷一縷,它自己有序列化器,並且在序列化器中完成了JWT字符串的拼接,最後進行返回。

驗證流程

   現在我們再來看一下,當用戶登錄後,再次訪問的驗證流程,最開始肯定走認證:

authentication_classes = [JSONWebTokenAuthentication] 

   我們都知道,在APIView中的dispatch()方法中的initial()方法中,會有下面這三條代碼:

self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)

# 詳細的這三個代碼的執行步驟,尤其是認證,可以查看之前的文章

   也就是先走認證,走認證時會統一執行一個叫做authenticators()的方法。我們直接找JSONWebTokenAuthentication中的authenticators()方法即可。

   這個類沒有authenticators()這個方法,所以它繼承類誰呢?

class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):

   所以我們要找的其實是BaseJSONWebTokenAuthentication中的authenticators()方法。

   終於,找到了:

    def authenticate(self, request):

        jwt_value = self.get_jwt_value(request)  # 獲取jwt字符串,進行解析。也就是[ JWT  字符串 ],這個字符串的內容,排除前綴,感興趣可以看看
        if jwt_value is None:
            return None  # 如果沒有JWT驗證字符串,則返回None

        try:  # 一系列的異常捕獲,均來自於內置的jwt模塊
            payload = jwt_decode_handler(jwt_value)  # 解碼荷載部分
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')  # 簽證已過期
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')  # 解碼簽證時出錯
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()  # 無效令牌

        user = self.authenticate_credentials(payload)  # 如果都沒出錯,則執行這裡

        return (user, jwt_value)  # 返回user對象,這個會賦值給reque.user,這個會jwt字符串會賦值給request.auth
        
=================Request.user中關於權限認證的地方

        for authenticator in self.authenticators:
            try:
                user_auth_tuple = authenticator.authenticate(self)  # 這裡會返回一個user
            except exceptions.APIException:
                self._not_authenticated()
                raise

            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple  # 進行賦值 self.user=user ,self.auth = jwt_value
                return

   我們來看看user是怎麼弄出來的。它是在BaseJSONWebTokenAuthentication類中定義的:

    def authenticate_credentials(self, payload):

        User = get_user_model() # 返回模型!!不是記錄對象 ,通過settings.py中的AUTH_USER_MODEL進行獲取
        username = jwt_get_username_from_payload(payload)  # 獲取payload中的用戶名

        if not username:  # 如果沒有用戶名就拋出異常
            msg = _('Invalid payload.')
            raise exceptions.AuthenticationFailed(msg)

        try:  # 根據荷載中的用戶名,在模型User中試圖獲取出用戶的記錄對象,也就是說這裡會走數據庫查詢。
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:  # 如果User這個模型不存在則拋出異常,說明
            msg = _('Invalid signature.')
            raise exceptions.AuthenticationFailed(msg)
 
        if not user.is_active:  # 判斷賬戶是否被禁用
            msg = _('User account is disabled.')
            raise exceptions.AuthenticationFailed(msg)

        return user  # 返回user

不用auth組件

   看完了上面的源碼分析後,我們再來想一想,如果不用auth模塊該怎麼辦?

   其實也很簡單,我們自己造一個jwt然後將token這個隨機字符串返回就好了。

   認證的時候我們重寫一下認證類就好了,反正都是那一套邏輯。

  

手動簽發

   我們的表中要有username以及password字段,使用JWT模塊中生成token的幾個函數,自己造就一個token

# views.py

from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
from users.models import User


class LoginView(APIView):  # 登錄的時候簽發token
    authentication_classes = []
    
    def post(self,request):
        username=request.data.get('username')
        password=request.data.get('password')
        user=User.objects.filter(username=username,password=password).first()
        if user: # 能查到,登陸成功,手動簽發
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            return CommonResponse('100','登陸成功',data={'token':token})
        else:
            return CommonResponse('101', '登陸失敗')

   如果你想實現多端登錄,手機、用戶名、郵箱等都能登錄,這裡也有一份代碼,只不過配合了序列化器使用,比較複雜:

# 使用用戶名,手機號,郵箱,都可以登錄#
# 前端需要傳的數據格式
{
	"username":"lqz/1332323223/[email protected]",  # 用戶名、或者手機號、或者郵箱
	"password":"lqz12345"
}


# 視圖
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin, ViewSet
from app02 import ser
class Login2View(ViewSet):  # 跟上面完全一樣
    def login(self, request, *args, **kwargs):
        # 1 需要 有個序列化的類
        login_ser = ser.LoginModelSerializer(data=request.data,context={'request':request})  # context參數類似與管道,與序列化器進行連接
        # 2 生成序列化類對象
        # 3 調用序列號對象的is_validad
        login_ser.is_valid(raise_exception=True)
        token=login_ser.context.get('token')  # 從管道中取出token並返回
        # 4 return
        return Response({'status':100,'msg':'登錄成功','token':token,'username':login_ser.context.get('username')})
    
    
    
# 序列化類
from rest_framework import serializers
from api import models
import re
from rest_framework.exceptions import ValidationError

from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler
class LoginModelSerializer(serializers.ModelSerializer):
    username=serializers.CharField()  # 重新覆蓋username字段,數據中它是unique,post,認為你保存數據,自己有校驗沒過
    class Meta:
        model=models.User
        fields=['username','password']

    def validate(self, attrs):

        print(self.context)

        # 在這寫邏輯
        username=attrs.get('username') # 用戶名有三種方式
        password=attrs.get('password')
        # 通過判斷,username數據不同,查詢字段不一樣
        # 正則匹配,如果是手機號
        if re.match('^1[3-9][0-9]{9}$',username):
            user=models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$',username):# 郵箱
            user=models.User.objects.filter(email=username).first()
        else:
            user=models.User.objects.filter(username=username).first()
        if user: # 存在用戶
            # 校驗密碼,因為是密文,要用check_password
            if user.check_password(password):
                # 簽發token
                payload = jwt_payload_handler(user)  # 把user傳入,得到payload
                token = jwt_encode_handler(payload)  # 把payload傳入,得到token
                self.context['token']=token
                self.context['username']=user.username
                return attrs
            else:
                raise ValidationError('密碼錯誤')
        else:
            raise ValidationError('用戶不存在')

JWT驗證

   驗證的時候,繼承BaseAuthentication類,重寫一下驗證方法:

# app_auth.py

from users.models import User

class MyJSONWebTokenAuthentication(BaseAuthentication):

    def authenticate(self, request):
        jwt_value = get_authorization_header(request)
        if not jwt_value:
            raise AuthenticationFailed('Authorization 字段是必須的')
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('簽名過期')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('非法用戶')
        username = jwt_get_username_from_payload(payload)
        print(username)
        user = User.objects.filter(username=username).first()
        print(user)

        return user, jwt_value

   然後你的某一個視圖必須登錄後才可以訪問,把他添加到認證中就OK了。

from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication
class OrderView(APIView):
    # authentication_classes = [JSONWebTokenAuthentication]  # 不用默認的了,用我們自己的
    authentication_classes = [MyJSONWebTokenAuthentication]  # 由於不是auth_user表,所以不需要判斷是否為匿名用戶。
    def get(self,request):
        print(request.user)
        return CommonResponse('100', '成功',{'數據':'測試'})

最後我想說

  1.注意前綴,JWT開頭,一個空格,後面是JWTtoken字符串

  2.如果你不用auth組件,則需要手動生成JWTtoken字符串與手動進行校驗。這很麻煩,所幸JWT這個模塊給我們很多方便之處。

  3.多閱讀源碼,有好處的

Tags: