JWT認證

  服務器不要存儲token,token交給每個客戶端本身存儲,服務器壓力小, 服務器存儲的是 簽發和校驗token 兩段算法,簽發認證的效率高,算法完成各集羣服務器同步成本低,路由項目完成集羣部署(適應高併發)git

格式github

  jwt token採用三段式:頭部.載荷.簽名算法

  每一部分都是一個json字典加密形參的字符串django

  頭部和載荷採用的是base64可逆加密(前臺後臺均可以解密)json

  簽名採用hash256不可逆加密(後臺校驗採用碰撞校驗)api

  各部分字典的內容:服務器

    頭部:基礎信息 - 公司信息、項目組信息、可逆加密採用的算法併發

    載荷:有用但非私密的信息 - 用戶可公開信息、過時時間ide

    簽名:頭部+載荷+祕鑰 不可逆加密後的結果高併發

    注:服務器jwt簽名加密祕鑰必定不能泄露

簽發token:固定的頭部信息加密.當前的登錄用戶與過時時間加密.頭部+載荷+祕鑰生成不可逆加密

校驗token:頭部可校驗也能夠不校驗,載荷校驗出用戶與過時時間,頭部+載荷+祕鑰完成碰撞檢測校驗token是否被篡改

drf-jwt插件:https://github.com/jpadilla/django-rest-framework-jwt

安裝: pip3 install djangorestframework-jwt

api/urls.py

from django.conf.urls import url
from . import views
from rest_framework_jwt.views import ObtainJSONWebToken,obtain_jwt_token
urlpatterns = [
    # url(r'^jogin', ObtainJSONWebToken.as_view()),
    url(r'^jogin', obtain_jwt_token),
    url(r'^user/detail/$', views.UserDetail.as_view()),
]
# 認證信息,必須在請求頭的Authorization 中攜帶'jwt'後臺簽發的'token'格式的認證字符串

認證 - 校驗token:全局或局部配置drf-jwt的認證類 JSONWebTokenAuthentication

from rest_framework.views import APIView
from utils.response import APIResponse
# 必須登陸後,才能訪問, 經過認證權限組件
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class UserDetail(APIView):
    authentication_classes = [JSONWebTokenAuthentication]  # jwt-token校驗request.user
    permission_classes = [IsAuthenticated]  # 結合權限組件篩選掉遊客
    def get(self, request, *args, **kwargs):
        return APIResponse(results={'username': request.user.username})

 簽發token

  前提:給一個局部禁用了全部 認證與權限 的視圖類發送用戶信息獲得token,其實就是登陸接口

  rest_framework_jwt.views.ObtainJSONWebToken 的 父類 JSONWebTokenAPIView 的 post 方法, 接受有username、password的post請求

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

核心源碼:rest_framework_jwt.serializer.JSONWebTokenSerializer的validate(self, attrs)方法

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)

手動簽發token邏輯
  經過username、password獲得user對象,經過user對象生成payload:jwt_payload_handler(user) => payload,   from rest_framework_jwt.serializers import jwt_payload_handler       經過payload簽發token:jwt_encode_handler(payload) => token        from rest_framework_jwt.serializers import jwt_encode_handler

校驗token

  前提:訪問一個配置了jwt認證規則的視圖類,就須要提交認證字符串token,在認證類中完成token的校驗, rest_framework_jwt.authentication.JSONWebTokenAuthentication 的 父類 BaseJSONWebTokenAuthentication 的 authenticate 方法, 請求頭拿認證信息jwt-token => 經過反爬小規則肯定有用的token => payload => user   

核心源碼: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)

手動校驗token邏輯

  從請求頭中獲取token,根據token解析出payload:jwt_decode_handler(token) => payloay, from rest_framework_jwt.authentication import jwt_decode_handler, 根據payload解析出user:self.authenticate_credentials(payload) => user,  繼承drf-jwt的BaseJSONWebTokenAuthentication,拿到父級的authenticate_credentials方法

實現多方式登錄簽發token

models.py

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

serializers.py

from rest_framework import serializers
from . import models
import re
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.serializers import jwt_encode_handler

# 前臺提交多種登陸信息都採用一個key,因此後臺能夠自定義反序列化字段進行對應
# 序列化要處理序列化與反序列化,要在fields中綁定的Model類全部使用到的字段
# 區分序列化字段與反序列化字段 read_only | write_only
# 在自定義校驗規則中,校驗數據是否合法,肯定登陸用戶,根據用戶簽發token
# 將登陸的用戶與簽發的token保存在序列化對象中

class UserModelSerializer(serializers.ModelSerializer):
    # 自定義反序列字段:必定要設置write_only,只參與反序列化,不會與model類字段映射
    usr = serializers.CharField(write_only=True)
    pwd = serializers.CharField(write_only=True)
    class Meta:
        model = models.User
        fields = ['usr', 'pwd', 'username', 'mobile', 'email']
        # 系統校驗規則
        extra_kwargs = {
            'username': {
                'read_only': True
            },
            'mobile': {
                'read_only': True
            },
            'email': {
                'read_only': True
            },
        }

    def validate(self, attrs):
        usr = attrs.get('usr')
        pwd = attrs.get('pwd')

        # 多方式登陸:各分支處理獲得該方式下對應的用戶
        if re.match(r'.+@.+', usr):
            user_query = models.User.objects.filter(email=usr)
        elif re.match(r'1[3-9][0-9]{9}', usr):
            user_query = models.User.objects.filter(mobile=usr)
        else:
            user_query = models.User.objects.filter(username=usr)
        user_obj = user_query.first()

        # 簽發:獲得登陸用戶,簽發token並存儲在實例化對象中
        if user_obj and user_obj.check_password(pwd):
            # 簽發token,將token存放到 實例化類對象的token 名字中
            payload = jwt_payload_handler(user_obj)
            token = jwt_encode_handler(payload)
            # 將當前用戶與簽發的token都保存在序列化對象中
            self.user = user_obj
            self.token = token
            return attrs

        raise serializers.ValidationError({'data': '數據有誤'})

views.py

from rest_framework.views import APIView

from utils.response import APIResponse
# 必須登陸後,才能訪問, 經過認證權限組件
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from .authentications import JWTAuthentication
class UserDetail(APIView):
    # authentication_classes = [JSONWebTokenAuthentication]
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticated]
    def get(self, request, *args, **kwargs):
        return APIResponse(results={'username': request.user.username})

自定義認證反爬規則的認證類

authentications.py

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:
            payload = jwt_decode_handler(token)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('token 已過時')
        except:
            raise AuthenticationFailed('非法用戶')
        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]

 views.py

from utils.response import APIResponse
# 必須登陸後,才能訪問, 經過認證權限組件
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from .authentications import JWTAuthentication
class UserDetail(APIView):
    # authentication_classes = [JSONWebTokenAuthentication]
    authentication_classes = [JWTAuthentication]
    permission_classes = [IsAuthenticated]
    def get(self, request, *args, **kwargs):
        return APIResponse(results={'username': request.user.username})

admin使用自定義User表:新增用戶密碼密文

from . import models
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)

羣查接口各類篩選組件

urls.py

url(r'^cars/$', views.CarListAPIView.as_view()),

models.py

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

admin.py

admin.site.register(models.Car)

serializers.py

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

views.py

from rest_framework.generics import ListAPIView

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

drf搜索過濾組件

 from rest_framework.generics import ListAPIView

# 第一步:drf的SearchFilter - 搜索過濾
from rest_framework.filters import SearchFilter

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

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

    # 第三步:SearchFilter過濾類依賴的過濾條件 => 接口:/cars/?search=...
    search_fields = ['name', 'price']
    # eg:/cars/?search=1,name和price中包含1的數據都會被查詢出

drf排序過濾組件

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升序

drf基礎分頁組件

pahenations.py

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

views.py

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

 drf偏移分頁組件

 pahenations.py

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

views.py

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

drf遊標分頁組件

pahenations.py

# 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.py

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

自定義過濾器

filters.py

# 自定義過濾器,接口:?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.py

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]

過濾器插件:django-filter

安裝  pip3 install django-filter

filters.py

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是自定義字段,須要本身自定義過濾條件

views.py

from django_filters.rest_framework import DjangoFilterBackend
from .filters import CarFilterSet

class CarListAPIView(ListAPIView):
    queryset = models.Car.objects.all()
    serializer_class = serializers.CarModelSerializer  # django-filter過濾器插件使用  filter_backends = [DjangoFilterBackend]
相關文章
相關標籤/搜索