DRF之JWT簽發,認證,羣查

一:簽發Token

(1)源碼分析

(1)源碼入口git

rest_framework_jwt.views.ObtainJSONWebToken 的 父類 JSONWebTokenAPIView 的 post 方法
PS:只有post方法 接受username 與 password請求

(2)校驗方式django

post方法將請求數據交給 rest_framework_jwt.serializer.JSONWebTokenSerializer 處理
PS 完成數據的校驗,會走序列化類的 全局鉤子校驗規則,校驗獲得登陸用戶並簽發token存儲在序列化對象中

(3)核心源碼後端

def validate(self, attrs):
    # 帳號密碼字典
    credentials = {
        self.username_field: attrs.get(self.username_field),
        'password': attrs.get('password')
    }
    if all(credentials.values()):
        # 簽發token第1步:用帳號密碼獲得user對象
        user = authenticate(**credentials)
        if user:
            if not user.is_active:
                msg = _('User account is disabled.')
                raise serializers.ValidationError(msg)
            # 簽發token第2步:經過user獲得payload,payload包含着用戶信息與過時時間
            payload = jwt_payload_handler(user)
            # 在視圖類中,能夠經過 序列化對象.object.get('user'或者'token') 拿到user和token 
            return {
                # 簽發token第3步:經過payload簽發出token
                'token': jwt_encode_handler(payload),
                '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)

(2)手動簽發token

(1)經過username password 生成user對象api

(2)經過user對象生成載荷(payload)ide

jwt_payload_handler(user) => payload
from rest_framework_jwt.serializers import jwt_payload_handler

(3)經過載荷簽發token源碼分析

jwt_encode_handler(payload) => token
from rest_framework_jwt.serializers import jwt_encode_handler

二:檢驗token

(1)源碼入口

(1)前提:一個配置JWT認證的視圖類 就必須提供token進行認證 在認證的過程當中完成token的校驗post

rest_framework_jwt.authentication.JSONWebTokenAuthentication 的 父類 BaseJSONWebTokenAuthentication 的 authenticate 方法
# 請求頭拿認證信息jwt-token => 經過反爬小規則肯定有用的token => payload => user

(2)核心源碼測試

rest_framework_jwt.authentication.BaseJSONWebTokenAuthentication的authenticate(self, request)方法
def authenticate(self, request):
    """
    Returns a two-tuple of `User` and token if a valid signature has been
    supplied using JWT-based authentication.  Otherwise returns `None`.
    """
    # 帶有反爬小規則的獲取token:前臺必須按 "jwt token字符串" 方式提交
    # 校驗user第1步:從請求頭 HTTP_AUTHORIZATION 中拿token,並提取
    jwt_value = self.get_jwt_value(request)
    # 遊客
    if jwt_value is None:
        return None
    # 校驗
    try:
        # 校驗user第2步:token => payload
        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第3步:token => payload
    user = self.authenticate_credentials(payload)

    return (user, jwt_value)

(3)檢驗tokenurl

  (1)從請求頭中獲取tokenspa

  (2)根據token解析出payload(載荷)

jwt_decode_handler(token) => payloay
from rest_framework_jwt.authentication import jwt_decode_handler

  (3)根據載荷解析出用戶

self.authenticate_credentials(payload) => user
 繼承drf-jwt的BaseJSONWebTokenAuthentication,拿到父級的authenticate_credentials方法

三:案例展現

(1):多方式實現登陸簽發token

(1)model層

# 模型層
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    mobile = models.CharField(max_length=11, unique=True)

    class Meta:
        db_table = 'api_user'
        verbose_name = '用戶表'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

(2)api/serializers序列化層

import re
from rest_framework_jwt.serializers import jwt_payload_handler  # 生成載荷
from rest_framework_jwt.serializers import jwt_encode_handler  # 解析user
from . import models  # 導入模型層

from rest_framework import serializers  # 序列化組件

class UserModelSerializer(serializers.ModelSerializer):  # 定義校驗類

    # 自定義反序列字段:必定要設置write_only,只參與反序列化,不會與model類字段映射
user = serializers.CharField(write_only=True) pwd = serializers.CharField(write_only=True) class Meta: model = models.User fields = ['user', 'pwd', 'username', 'mobile', 'email'] extra_kwargs = { 'username': { 'read_only': True }, 'mobile': { 'read_only': True }, 'email': { 'read_only': True }, } def validate(self, attrs): # 設置校驗的數據 user = attrs.get('user') pwd = attrs.get('pwd') # 設置校驗的方式 if re.match(r'.*@.*', user): # 匹配郵箱號 進行郵箱登陸 user_obj = models.User.objects.filter(email=user).first() elif re.match(r'1[3-9][0-9]{9}', user): # 匹配13-19開頭的手機號 進行手機登陸 user_obj = models.User.objects.filter(mobile=user).first() # 用戶名登陸 else: user_obj = models.User.objects.filter(username=user).first() if user_obj and user_obj.check_password(pwd): # 判斷是否有用戶與傳入的密碼是否正確 payload = jwt_payload_handler(user_obj) # 生成載荷 print(payload) # {'user_id': 1, 'username': 'admin', 'exp': datetime.datetime(2019, 10, 23, 12, 37, 52, 240300), 'email': '1234@qq.com'} token = jwt_encode_handler(payload) # 生成token print(token) # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTcxODM0MjcyLCJlbWFpbCI6IjEyMzRAcXEuY29tIn0.zLeCBgT6j6zi2g59iTIGIB5ELSmiWmgf8IdMk1mZors self.user = user_obj # 將用戶傳給後端 self.token = token # 將token傳入給後端 return attrs      # 上述條件 都不知足 直接拋出異常 raise serializers.ValidationError( { 'msg': "數據錯誤" } )
'''
1:設置自定義反序列化字段
2:設置全局校驗鉤子
3:經過前臺輸入的數據 對數據進行匹配生成user_obj
4:將生成的user_obj 生成載荷
5:調用生成的載荷生成token
6:將生成user_obj 與 token傳入後端
'''

(3)路由層

    url(r'^login/$', views.Login.as_view()),

(4)視圖層

from . import serializers

class Login(APIView):
    permission_classes = []  # 不進行權限校驗

    authentication_classes = []  # 不進行認證校驗

    def post(self, request, *args, **kwargs):

        user_ser = serializers.UserModelSerializer(data=request.data)  # 拿到傳入的數據進行反序列化校驗
  
user_ser.is_valid(raise_exception=True)   # 序列化類校驗獲得登陸用戶與token存放在序列化對象中 token = user_ser.token user_data = serializers.UserModelSerializer(user_ser.user).data # 獲取傳入的數據 進行序列化 return Response( { 'status': 0, 'msg': "測試成功", 'results': user_data, 'token': token } )
'''
1:獲取前臺傳入的數據 進行反序列化 拿到user_ser
2:判斷數據是否有效 若是無效直接拋出異常
3:在將前臺傳入的數據進行序列化 傳入給前臺展現
'''

 (2)自定義反爬認證規則

(1)api/authentications

import jwt
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework.exceptions import AuthenticationFailed
class JWTAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        jwt_token = request.META.get('HTTP_AUTHORIZATION')

        # 自定義校驗規則:auth token jwt
        token = self.parse_jwt_token(jwt_token)

        if token is None:
            return None

        try:
            # token => payload
            payload = jwt_decode_handler(token)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('token已過時')
        except:
            raise AuthenticationFailed('非法用戶')
        # payload => user
        user = self.authenticate_credentials(payload)

        return (user, token)

    # 自定義校驗規則:auth token jwt,auth爲前鹽,jwt爲後鹽
    def parse_jwt_token(self, jwt_token):
        tokens = jwt_token.split()
        if len(tokens) != 3 or tokens[0].lower() != 'auth' or tokens[2].lower() != 'jwt':
            return None
        return tokens[1]

(2)視圖層

from rest_framework.views import APIView
from utils.response import APIResponse
# 必須登陸後才能訪問 - 經過了認證權限組件
from rest_framework.permissions import IsAuthenticated
# 自定義jwt校驗規則
from .authentications import JWTAuthentication
class UserDetail(APIView):
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticated]
    def get(self, request, *args, **kwargs):
        return APIResponse(results={'username': request.user.username})

四:admin後臺管理密碼密文輸入

from django.contrib import admin
from . import models

# 自定義User表,admin後臺管理,採用密文密碼
from django.contrib.auth.admin import UserAdmin

class MyUserAdmin(UserAdmin):
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('username', 'password1', 'password2', 'mobile', 'email'),
        }),
    )

admin.site.register(models.User, MyUserAdmin)

五:DRF羣查數據配置

(1)模型層

class Car(models.Model):
    name = models.CharField(max_length=16, unique=True, verbose_name='車名')
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='價格')
    brand = models.CharField(max_length=16, verbose_name='品牌')

    class Meta:
        db_table = 'api_car'
        verbose_name = '汽車表'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name

(2)admin註冊

admin.site.register(models.Car)

(3)序列化層api/serializers

class CarModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Car
        fields = ['name', 'price', 'brand']

六:DRF羣查組件

(1)搜索過濾組件

from rest_framework.generics import ListAPIView  # 進行羣查
from rest_framework.filters import SearchFilter   # 導入搜索組件
from . import models

class CarListAPIView(ListAPIView):
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer

    filter_backends = [SearchFilter]  # 局部過濾

    search_fields = ['name','price']   # 設置查找的字段

(2)排序過濾組件

from rest_framework.generics import ListAPIView

# 第一步:drf的OrderingFilter - 排序過濾
from rest_framework.filters import OrderingFilter

class CarListAPIView(ListAPIView):
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer

    # 第二步:局部配置 過濾類 們(全局配置用DEFAULT_FILTER_BACKENDS)
    filter_backends = [OrderingFilter]

    # 第三步:OrderingFilter過濾類依賴的過濾條件 => 接口:/cars/?ordering=...
    ordering_fields = ['pk', 'price']
    # eg:/cars/?ordering=-price,pk,先按price降序,若是出現price相同,再按pk升序

(3)分頁組件

api/pahenations

from rest_framework.pagination import PageNumberPagination

class MyPageNumberPagination(PageNumberPagination):
    # ?page=頁碼
    page_query_param = 'page'
    # ?page=頁面 下默認一頁顯示的條數
    page_size = 3
    # ?page=頁面&page_size=條數 用戶自定義一頁顯示的條數
    page_size_query_param = 'page_size'
    # 用戶自定義一頁顯示的條數最大限制:數值超過5也只顯示5條
    max_page_size = 5

視圖層

from rest_framework.generics import ListAPIView

class CarListAPIView(ListAPIView):
    # 若是queryset沒有過濾條件,就必須 .all(),否則分頁會出問題
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer
    
    # 分頁組件 - 給視圖類配置分頁類便可 - 分頁類須要自定義,繼承drf提供的分頁類便可
    pagination_class = pagenations.MyPageNumberPagination

 (4)偏移分頁組件

api/pahenations

from rest_framework.pagination import LimitOffsetPagination
class MyLimitOffsetPagination(LimitOffsetPagination):
    # ?offset=從頭偏移的條數&limit=要顯示的條數
    limit_query_param = 'limit'
    offset_query_param = 'offset'
    # ?不傳offset和limit默認顯示前3條,只設置offset就是從偏移位日後再顯示3條
    default_limit = 3
    # ?limit能夠自定義一頁顯示的最大條數
    max_limit = 5

    # 只使用limit結合ordering能夠實現排行前幾或後幾
    # ?ordering=-price&limit=2  => 價格前2

views

from rest_framework.generics import ListAPIView

class CarListAPIView(ListAPIView):
    # 若是queryset沒有過濾條件,就必須 .all(),否則分頁會出問題
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer
    
    # 分頁組件 - 給視圖類配置分頁類便可 - 分頁類須要自定義,繼承drf提供的分頁類便可
    pagination_class = pagenations.MyLimitOffsetPagination

(5)遊標組件

api/pahenations

# 注:必須基於排序規則下進行分頁
# 1)若是接口配置了OrderingFilter過濾器,那麼url中必須傳ordering
# 1)若是接口沒有配置OrderingFilter過濾器,必定要在分頁類中聲明ordering按某個字段進行默認排序
from rest_framework.pagination import CursorPagination
class MyCursorPagination(CursorPagination):
    cursor_query_param = 'cursor'
    page_size = 3
    page_size_query_param = 'page_size'
    max_page_size = 5
    ordering = '-pk'

views

from rest_framework.generics import ListAPIView

class CarListAPIView(ListAPIView):
    # 若是queryset沒有過濾條件,就必須 .all(),否則分頁會出問題
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer
    
    # 分頁組件 - 給視圖類配置分頁類便可 - 分頁類須要自定義,繼承drf提供的分頁類便可
    pagination_class = pagenations.MyCursorPagination

七:自定義過濾器

api/filters

# 自定義過濾器,接口:?limit=顯示的條數
class LimitFilter:
    def filter_queryset(self, request, queryset, view):
        # 前臺固定用 ?limit=... 傳遞過濾參數
        limit = request.query_params.get('limit')
        if limit:
            limit = int(limit)
            return queryset[:limit]
        return queryset

views

from rest_framework.generics import ListAPIView

class CarListAPIView(ListAPIView):
    # 若是queryset沒有過濾條件,就必須 .all(),否則分頁會出問題
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer
    
    # 局部配置 過濾類 們(全局配置用DEFAULT_FILTER_BACKENDS)
    filter_backends = [LimitFilter]

八:過濾器插件

(1)安裝

pip3 install django-filter

(2)api/filter

# django-filter插件過濾器類
from django_filters.rest_framework.filterset import FilterSet
from . import models

# 自定義過濾字段
from django_filters import filters
class CarFilterSet(FilterSet):
    min_price = filters.NumberFilter(field_name='price', lookup_expr='gte')
    max_price = filters.NumberFilter(field_name='price', lookup_expr='lte')
    class Meta:
        model = models.Car
        fields = ['brand', 'min_price', 'max_price']
        # brand是model中存在的字段,通常都是能夠用於分組的字段
        # min_price、max_price是自定義字段,須要本身自定義過濾條件

(3)視圖層

# django-filter插件過濾器
from django_filters.rest_framework import DjangoFilterBackend
from .filters import CarFilterSet

class CarListAPIView(ListAPIView):
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer
    
    # 局部配置 過濾類 們(全局配置用DEFAULT_FILTER_BACKENDS)
    filter_backends = [DjangoFilterBackend]
    
    # django-filter過濾器插件使用
    filter_class = CarFilterSet
    # 接口:?brand=...&min_price=...&max_price=...
    # eg:?brand=寶馬&min_price=5&max_price=10 => 5~10間的寶馬牌汽車
相關文章
相關標籤/搜索