DjangoRestFramework,認證組件、許可權組件、頻率組件、url註冊器、響應器、分頁組件
- 2019 年 12 月 20 日
- 筆記
一 認證組件
1. 局部認證組件
我們知道,我們不管路由怎麼寫的,對應的視圖類怎麼寫的,都會走到dispatch方法,進行分發,
在咱們看的APIView類中的dispatch方法的源碼中,有個self.initial(request, *args, **kwargs),那麼認證、許可權、頻率這三個默認組件都在這個方法裡面了,如果我們自己沒有做這三個組件的配置,那麼會使用源碼中默認的一些配置。進源碼去看看你就會看到下面三個東西:
# Ensure that the incoming request is permitted #實現認證 self.perform_authentication(request) #許可權判斷 self.check_permissions(request) #控制訪問頻率 elf.check_throttles(request)
目前為止大家知道的認證機制是不是有cookie、session啊,session更安全一些,但是你會發現session的資訊都存到咱們的伺服器上了,如果用戶量很大的話,伺服器壓力是比較大的,並且django的session存到了django_session表中,不是很好操作,但是一般的場景都是沒有啥問題的,現在生產中使用的一個叫做token機制的方式比較多,現在我們是不是就知道個csrf_token啊,其實token有很多種寫法,如何加密,你是hashlib啊還是base64啊還是hmac啊等,是不是加上過期時間啊,是不是要加上一個secret_key(客戶端與服務端協商好的一個字元串,作為雙方的認證依據),是不是要持續刷新啊(有效時間要短,不斷的更新token,如果在這麼短的時間內還是被別人拿走了token,模擬了用戶狀態,那這個基本是沒有辦法的,但是你可以在網路或者網路設備中加安全,存客戶的ip地址等,防黑客)等等。
大致流程圖解:

首先我們需要創建一個表,用戶表,裡面放一個token欄位,其實一般我都是放到兩個表裡面,和用戶表是一個一對一關係的表,看程式碼:
################################# user表 ############################### class User(models.Model): user = models.CharField(max_length=32) pwd = models.CharField(max_length=32) type_choice=((1,"VIP"),(2,"SVIP"),(3,"SSVIP")) user_type = models.IntegerField(choices=type_choice) class UserToken(models.Model): user = models.OneToOneField(to=User) #一對一到用戶表 token = models.CharField(max_length=128) #設置的長度大一些 # expire_time = models.DateTimeField() #如果做超時時間限制,可以在這裡加個欄位來搞,這裡我沒有寫昂,簡單搞了
urls.py內容如下:
#登陸認證介面 url(r'^login/$', views.LoginView.as_view(),), #別忘了$符號結尾
views.py內容如下:自己寫一個每次登陸成功之後刷新token值
###################login邏輯介面####################### #關於邏輯介面而不是提供數據的介面,我們不用ModelViewSet,而是直接寫個類,繼承APIView,然後在類裡面直接寫咱的邏輯 import uuid import os import json class LoginView(APIView): #從前後端分離的項目來講,get請求不需要寫,因為get就是個要登陸頁面的操作,vue就搞定了,所以我們這裡直接寫post請求就可以了 def post(self,request): # 一般,請求過來之後,我們後端做出的響應,都是個字典,不僅包含錯誤資訊,還有要狀態碼等,讓客戶端明白到底發生了什麼事情 # 'code'的值,1表示成功,0表示失敗,2表示其他錯誤(自己可以做更細緻的錯誤程式碼昂) res = {'code': 1, 'msg': None, 'user': None,'token':None} print(request.data) try: user = request.data.get('user') pwd = request.data.get('pwd') # 資料庫中查詢 user_obj = models.User.objects.filter(user=user, pwd=pwd).first() if user_obj: res['user'] = user_obj.user # 添加token,用到咱們usertoken表 # models.UserToken.objects.create(user=user,token='123456') # 創建token隨機字元串,我寫了兩個方式,簡寫的昂,最好再加密一下 random_str = uuid.uuid4() # random_str = os.urandom(16) bytes類型的16位的隨機字元串 models.UserToken.objects.update_or_create( user=user_obj, # 查找篩選條件 defaults={ # 添加或者更新的數據 "token": random_str, } ) res['token'] = random_str res['msg'] = '登陸成功' else: res['code'] = 0 res['msg'] = '用戶名或者密碼錯誤' return Response(res) except Exception as e: res['code'] = 2 res['msg'] = str(e) return Response(res)
通過上面的程式碼我們將token返回給了用戶,那麼以後用戶不管發送什麼請求,都要帶著我給它的token值來訪問,認證token通過才行,並且更新token。
下面我們玩一下drf提供的認證組件的玩法。
DRF的認證組件
將來有些數據介面是必須要求用戶登陸之後才能獲取到數據,所以將來用戶登陸完成之後,每次再過來請求,都要帶著token來,作為身份認證的依據。
from app01.serializer import BookSerializers #####################Book表操作########################## class UserAuth(): def authenticate_header(self,request): pass #authenticate方法固定的,並且必須有個參數,這個參數是新的request對象,不信,看源碼 def authenticate(self,request): if 1: #源碼中會發現,這個方法會有兩個返回值,並且這兩個返回值封裝到了新的request對象中了,request.user-->用戶名 和 request.auth-->token值,這兩個值作為認證結束後的返回結果 return "chao","asdfasdfasdf" class BookView(APIView): #認證組件肯定是在get、post等方法執行之前執行的,還記得源碼的地方嗎,這個組件是在dispatch的地方調用的,我們是上面寫個UserAuth類 authentication_classes = [UserAuth,] #認證類可以寫多個,一個一個的順序驗證 def get(self,request): ''' 查看所有書籍 :param request: :return: ''' #這樣就拿到了上面UserAuth類的authenticate方法的兩個返回值 print(request.user) print(request.auth) book_obj_list = models.Book.objects.all() s_books = BookSerializers(book_obj_list,many=True) return Response(s_books.data)
def _authenticate(self): """ Attempt to authenticate the request using each authentication instance in turn. """ for authenticator in self.authenticators: try: user_auth_tuple = authenticator.authenticate(self) except exceptions.APIException: self._not_authenticated() raise if user_auth_tuple is not None: self._authenticator = authenticator #值得注意的是,self是APIView封裝的新的request對象 self.user, self.auth = user_auth_tuple return #退出了這個函數,函數就不會執行了,不會再循環了,所以如果你的第一個認證類有返回值,那麼第二個認證類就不會執行了,所以別忘了return是結束函數的意思,所以如果你有多個認證類,那麼返回值放到最後一個類裡面
好,我們寫一寫獲取token值,然後校驗的功能,看views.py的程式碼:
from django.shortcuts import render,HttpResponse,redirect from django.views import View from rest_framework import serializers from app01 import models from rest_framework.views import APIView from rest_framework.response import Response #將序列化組件都放到一個單獨的文件裡面,然後引入進來 from app01.serializer import BookSerializers,PublishSerializers,AuthorSerializers #drf提供的認證失敗的異常 from rest_framework.exceptions import AuthenticationFailed class UserAuth(): #每個認證類,都需要有個authenticate_header方法,並且有個參數request def authenticate_header(self,request): pass #authenticate方法固定的,並且必須有個參數,這個參數是新的request對象,不信,看源碼 def authenticate(self,request): # token = request._request.GET.get("token") #由於我們這個request是新的request對象,並且老的request對象被封裝到了新的request對象中,名字是self._request,所以上面的取值方式是沒有問題的,不過人家APIView不僅封裝了老的request對象,並且還給你加了query_params屬性,和老的request.GET得到的內容是一樣的,所以可以直接按照下面的方式來寫 token = request.query_params.get("token") #用戶請求來了之後,我們獲取token值,到資料庫中驗證 usertoken = models.UserToken.objects.filter(token=token).first() if usertoken: #驗證成功之後,可以返回兩個值,也可以什麼都不返回 return usertoken.user.user,usertoken.token #源碼中會發現,這個方法會有兩個返回值,並且這兩個返回值封裝到了新的request對象中了,request.user-->用戶名 和 request.auth-->token值,這兩個值作為認證結束後的返回結果 else: #因為源碼內部進行了異常捕獲,並且給你主動返回一個forbiden錯誤,所以我們在這裡主動拋出異常就可以了 raise AuthenticationFailed("認證失敗")
urls.py內容如下:
url(r'^books/$', views.BookView.as_view(),),
通過postman發送請求,你會發現錯誤:

如果我們請求中帶了資料庫中保存的token值,那麼就會成功獲取數據,看資料庫中的token值:

然後通過postman再請求,帶著token值,看效果,成功了:

繼承drf的BaseAuthentication認證類的寫法:
from app01 import models from rest_framework.views import APIView from rest_framework.response import Response #將序列化組件都放到一個單獨的文件裡面,然後引入進來 from app01.serializer import BookSerializers,PublishSerializers,AuthorSerializers #drf提供的認證失敗的異常 from rest_framework.exceptions import AuthenticationFailed from rest_framework.authentication import BaseAuthentication #繼承drf的BaseAuthentication類 class UserAuth(BaseAuthentication): # 繼承了BaseAuthentication類之後,這個方法就不用寫了 # def authenticate_header(self,request): # pass def authenticate(self,request): # token = request._request.GET.get("token") token = request.query_params.get("token") #有request對象,那麼不僅僅可以認證token,還可以認證請求裡面的其他內容 usertoken = models.UserToken.objects.filter(token=token).first() if usertoken: #驗證成功之後 return usertoken.user.user,usertoken.token else: raise AuthenticationFailed("認證失敗") class BookView(APIView): #通過源碼看,認證類的查找過程,和解析組件的查找過程是一樣的 authentication_classes = [UserAuth,] def get(self,request): ''' 查看所有書籍 :param request: :return: ''' print(request.user) print(request.auth) book_obj_list = models.Book.objects.all() s_books = BookSerializers(book_obj_list,many=True) return Response(s_books.data)
帶時間戳的隨機字元串:
def get_random_str(user): import hashlib,time ctime=str(time.time()) md5=hashlib.md5(bytes(user,encoding="utf8")) md5.update(bytes(ctime,encoding="utf8")) return md5.hexdigest()
全局視圖認證組件:
在settings.py文件中配置:如果我再app01文件夾下的service文件夾下的auth文件夾下寫了我們自己的認證類,那麼全局配置的寫法就按照下面的方式寫。
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",] #裡面是路徑字元串 }
認證組件就說這些,看許可權組件吧。
二 許可權組件
局部視圖許可權:
在app01.service.permissions.py中
from rest_framework.permissions import BasePermission class SVIPPermission(BasePermission): message="SVIP才能訪問!" #變數只能叫做message def has_permission(self, request, view): #重寫has_permission方法,自己寫許可權邏輯,看看源碼就明白了,這個view是咱當前類的實例化對象,一般用不到,但是必須給個參數寫在這裡。 if request.user.user_type==3: return True #通過許可權 return False #沒有通過
在views.py:
from app01.service.permissions import * class BookViewSet(generics.ListCreateAPIView): permission_classes = [SVIPPermission,] queryset = Book.objects.all() serializer_class = BookSerializers
全局視圖許可權:
settings.py配置如下:
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",] }
三 頻率組件
局部視圖throttle,反爬,防攻擊
在throttles.py中:
from rest_framework.throttling import BaseThrottle,SimpleRateThrottle import time from rest_framework import exceptions visit_record = {} class VisitThrottle(BaseThrottle): # 限制訪問時間 VISIT_TIME = 10 VISIT_COUNT = 3 # 定義方法 方法名和參數不能變 def allow_request(self, request, view): # 獲取登錄主機的id id = request.META.get('REMOTE_ADDR') self.now = time.time() if id not in visit_record: visit_record[id] = [] self.history = visit_record[id] # 限制訪問時間 while self.history and self.now - self.history[-1] > self.VISIT_TIME: self.history.pop() # 此時 history中只保存了最近10秒鐘的訪問記錄 if len(self.history) >= self.VISIT_COUNT: return False else: self.history.insert(0, self.now) return True def wait(self): return self.history[-1] + self.VISIT_TIME - self.now
在views.py中:
from app01.service.throttles import * class BookViewSet(generics.ListCreateAPIView): throttle_classes = [VisitThrottle,] queryset = Book.objects.all() serializer_class = BookSerializers
全局視圖throttle
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",], "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",] }
內置throttle類
在throttles.py修改為:
class VisitThrottle(SimpleRateThrottle): scope="visit_rate" def get_cache_key(self, request, view): return self.get_ident(request)
settings.py設置:
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.service.auth.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.service.permissions.SVIPPermission",], "DEFAULT_THROTTLE_CLASSES":["app01.service.throttles.VisitThrottle",], "DEFAULT_THROTTLE_RATES":{ "visit_rate":"5/m", } }
四 url註冊器
幫我們自動生成4個url,和我們自己寫的差不多:
from django.conf.urls import url,include from django.contrib import admin from app01 import views from rest_framework import routers router = routers.DefaultRouter() #自動幫我們生成四個url router.register(r'authors', views.AuthorView) router.register(r'books', views.BookView) urlpatterns = [ # url(r'^books/$', views.BookView.as_view(),), #別忘了$符號結尾 # url(r'api/', include(router.urls)), url(r'', include(router.urls)), #http://127.0.0.1:8000/books/ 也可以這樣寫:http://1270.0..1:8000/books.json/ #登陸認證介面 url(r'^login/$', views.LoginView.as_view(),), #別忘了$符號結尾 ]
但是有個前提就是,我們用的是:ModelViewSet序列化組件。
from rest_framework.viewsets import ModelViewSet class AuthorView(ModelViewSet): queryset = models.Author.objects.all() serializer_class = AuthorSerializers class BookView(ModelViewSet): queryset = models.Book.objects.all() serializer_class = BookSerializers
所以,這個url註冊器其實並沒有那麼好用,當然啦,看需求.
五 響應器
簡單看看就行啦:
from rest_framework.viewsets import ModelViewSet from rest_framework.renderers import JSONRenderer,BrowsableAPIRenderer #如果我們沒有在自己的視圖類裡面配置,那麼源碼裡面默認就用的這兩個JSONRenderer,BrowsableAPIRenderer #BrowsableAPIRenderer 是當客戶端為瀏覽器的時候,回復的數據會自動給你生成一個頁面形式的數據展示,一般開發的時候,都不用頁面形式的 #JSONRenderer:回復的是json數據 class BookView(ModelViewSet): # renderer_classes = [JSONRenderer,] #其實默認就是這個JSONRenderer,所以一般不用在這裡配置了 queryset = models.Book.objects.all() serializer_class = BookSerializers
六 分頁器組件
簡單使用:
#引入分頁 from rest_framework.pagination import PageNumberPagination class BookView(APIView): # 通過源碼看,認證類的查找過程,和解析組件的查找過程是一樣的 # authentication_classes = [UserAuth,] # throttle_classes = [VisitThrottle,] def get(self,request): ''' 查看所有書籍 :param request: :return: ''' book_obj_list = models.Book.objects.all() #創建分頁器對象,PageNumberPagination類中除了PAGE_SIZE屬性之外,還有個page屬性,這個page屬性是第幾頁,用法是http://127.0.0.1:8000/books/?page=1 pnb = PageNumberPagination() #通過分頁器對象的paginate_queryset方法進行分頁, paged_book_list = pnb.paginate_queryset(book_obj_list,request,) #將分頁的數據進行序列化 s_books = BookSerializers(paged_book_list,many=True) return Response(s_books.data)
settings配置文件:
REST_FRAMEWORK={ # "DEFAULT_THROTTLE_RATES":{ # "visit_rate":"5/m", # }, 'PAGE_SIZE':5, #這是全局的一個每頁展示多少條的配置,但是一般不用它,因為不同的數據展示可能每頁展示的數量是不同的 }
如果我們不想用全局的page_size配置,我們自己可以寫個類來繼承分頁類組件,重寫裡面的屬性:
#引入分頁 from rest_framework.pagination import PageNumberPagination class MyPagination(PageNumberPagination): page_size = 3 #每頁數據顯示條數 page_query_param = 'pp' #http://127.0.0.1:8000/books/?pp=1,查詢哪一頁的數據 page_size_query_param = 'size' #如果我們顯示的一頁數據不夠你用的,你想臨時的多看展示一些數據,可以通過你設置的這個page_size_query_param作為參數來訪問:http://127.0.0.1:8000/books/?pp=2&size=5 #那麼你看到的雖然是第二頁,但是可以看到5條數據,意思是將page_size的數字臨時擴大了,每頁展示的數據就多了或者少了,看你的page_size_query_param設置的值 max_page_size = 10 #最大每頁展示多少條,即便是你前端通過page_size_query_param臨時調整了page_size的值,但是最大也不能超過我們設置的max_page_size的值 class BookView(APIView): def get(self,request): ''' 查看所有書籍 :param request: :return: ''' book_obj_list = models.Book.objects.all() pnb = MyPagination() paged_book_list = pnb.paginate_queryset(book_obj_list,request,) s_books = BookSerializers(paged_book_list,many=True) return Response(s_books.data)
還有我們玩的繼承了ModelViewSet類的試圖類使用分頁器的寫法:
from rest_framework.viewsets import ModelViewSet from rest_framework.pagination import PageNumberPagination class MyPagination(PageNumberPagination): page_size = 3 page_query_param = 'pp' page_size_query_param = 'size' max_page_size = 10 class BookView(ModelViewSet): queryset = models.Book.objects.all() serializer_class = BookSerializers pagination_class = MyPagination #配置我們自己寫的分頁類
還有個偏移分頁,了解一下就行了
from rest_framework.pagination import LimitOffsetPagination