1. 程式人生 > >DRF之許可權認證頻率元件

DRF之許可權認證頻率元件

在程式設計的世界中,我們認為,使用者輸入的所有資料都是不可靠的,不合法的,直接使用使用者輸入的資料是不安全的,不僅如此,我們還需要控制使用者的訪問行為,接下來,我們要學習認證元件、許可權元件、頻率元件。

引入

同學們,通過前面三節課的學習,我們已經詳細瞭解了DRF提供的幾個重要的工具,DRF充分利用了面向物件程式設計的思路,對Django的View類進行了繼承,並封裝了其as_view方法和dispatch方法,隨後提供了幾個非常方便的程式設計工具,比如解析器、序列化。

我們通過解析器,可以對來自客戶端的application/json資料進行解析,另外,通過序列化工具,我們能夠快速構建一套符合REST規範的api,隨後又通過DRF的mixin、view及viewset對這些介面邏輯進行優化。

有了他們,程式設計師開發Web應用的效率大大提高了,雖然我們也嘗試自己動手實現了這些功能,但是既然有了優秀的工具,我們就不必費盡心思重複發明輪子。DRF並不僅僅提供了這幾個工具,今天我們就來繼續深入學習DRF提供的一些其他的工具。

跟以往一樣,我們不僅僅要學會這些工具的使用方式,並且要深入研究他們的原始碼,希望大家在研究原始碼的過程中,能夠對面向物件程式設計的思路有更加深刻的認識。

當然,按照慣例,為了方便學習新知識以及閱讀原始碼,我們先來複習回顧一下之前已經學習過的知識。

今日概要

  • retrieve方法原始碼剖析
  • 認證元件的使用方式及原始碼剖析
  • 許可權元件的使用方式及原始碼剖析
  • 頻率元件的使用方式及原始碼剖析

知識點複習回顧

  • 昨日回顧
  • Python邏輯運算
知識點複習回顧一:Python邏輯運算

有了前兩天的基礎,今天看原始碼我們就沒有那麼大的壓力了,所要複習的知識也僅僅只有一個,那就是Python的邏輯運算,當然,稍後還會有幾個簡單的知識點,就不單獨拿出來複習了。

什麼是邏輯運算呢?就是and、or、not。not為取反,比較簡單,而and和or表示通過運算,計算表示式的布林值,判斷最終結果為真即止

  • and:x and y 表示布林與,意為,判斷and運算之後的最終結果,為真即止,and運算必須表示式兩端所有值均為真才能確定最終結果,必須所有值都為真
  • or:x and y 筆試布林或,意為,判斷or運算之後的最終結果,為真即止,or運算遇到真即返回,即有一個真值即可。
  • not x:取反

看下面的程式碼吧:

1
2
3
4
x = 10 and 20 # x = 20
x = 0 and 20 # x = 0
x = 10 or 20 # x = 10
x = 0 or 20 # x = 20

好了,知識點複習就這麼多。

今日詳細

mixin之retrieve原始碼剖析

上節課,我們分析過mixin中create方法的原始碼,今天,create方法比較簡單,今天,我們來分析分析retrieve方法的原始碼,它比create方法稍微複雜一點點,複雜的地方在於如何獲取需要操作的那條資料,因為我們知道,我們傳遞給不同的檢視類的所有方法都是一樣的,唯一變化的兩個是queryset和serializer_classes。

好了,廢話不多說,下面來分析一下:

  • Django程式啟動,開始初始化,獲取配置資訊,獲取檢視類並載入到記憶體中,獲取url及檢視類的對應關係
  • 開始繫結檢視類和url的對應關係,執行as_view()方法
  • as_view()方法被執行的時候傳遞了引數,為字典形式:{ “get”: “retrieve”, “delete”: “destroy”, “put”: “update” }
  • 上一步中執行as_view()方法傳遞引數的目的是為了完成優化,將delete請求方式重新命名為不同的函式
  • ViewSetMixin類重寫了as_view()方法,也就是在這個地方將幾個函式重新繫結,它並沒有重寫dispatch方法
  • 該方法返回檢視函式view,注意在這個函式中有一個行 self = cls(**initkwargs), cls是檢視類,執行檢視函式時self就指向檢視函式的例項物件
  • 等待客戶端請求
  • 請求到來,開始執行檢視函式,注意,呼叫檢視函式時的方式是view(request),而如果url帶有引數,呼叫方式為view(request, xxx=id)的形式
  • 顯然,我們有命名引數(?P\d+),所以此時的呼叫方式為view(request, pk=id)
  • 檢視函式中有一行self.kwargs = kwargs,所以pk已經被檢視函式找到了
  • 檢視函式返回self.dispatch(),開始執行dispatch方法,注意self是檢視類的例項化物件(每個請求都被封裝為一個物件)
  • dispatch開始執行get方法,注意此時的get方法會執行retrieve,以為已經被重定向了
  • 開始執行retrieve,有一行instance = self.get_object(), 該方法在GenericAPIView中
  • 至關重要的是拿到self.kwargs中的pk關鍵字,然後從queryset中拿到想要的資料
  • 返回結果

從以上過程中我們可以看出,最關鍵的一步就是對kwargs的封裝,這就是玄機所在,看到這裡,你對面向物件有了什麼新的領悟嗎?對於反射呢,有了跟多的思考和理解嗎?

如果沒有,不用著急,任何質的飛躍都需要量的積累,等我們寫的多了,看得多了,自然就會突破瓶頸。

好了,同志們,這些內容算是對於檢視元件的進一步挖掘和吸收,至此,檢視元件我們就差不多講完了。接下來,我們要學習其他工具了。

認證元件

很久很久以前,Web站點只是作為瀏覽伺服器資源(資料)和其他資源的工具,甚少有什麼使用者互動之類的煩人的事情需要處理,所以,Web站點的開發這根本不關心什麼人在什麼時候訪問了什麼資源,不需要記錄任何資料,有客戶端請求,我即返回資料,簡單方便,每一個http請求都是新的,響應之後立即斷開連線。

而如今,網際網路的世界發生了翻天覆地的變化,使用者不僅僅需要跟其他使用者溝通交流,還需要跟伺服器互動,不管是論壇類、商城類、社交類、門戶類還是其他各類Web站點,大家都非常重視使用者互動,只有跟使用者互動了,才能進一步留住使用者,只有留住了使用者,才能知道使用者需求,知道了使用者需求,才會產生商機,有了使用者,就等於有了流量,才能夠騙到…額…抱歉…是融到錢,有了資金企業才能繼續發展,可見,使用者互動是非常重要的,甚至可以說是至關重要的一個基礎功能。

而談到使用者互動,則必須要談到我們今天所要學習的知識點,認證、許可權和頻率。首先我們來看看認證。

登入成功後生成token

之前我們學習過使用cookie和session兩種方式可以儲存使用者資訊,這兩種方式不同的是cookie儲存在客戶端瀏覽器中,而session儲存在伺服器中,他們各有優缺點,配合起來使用,可將重要的敏感的資訊儲存在session中,而在cookie中可以儲存不太敏感的資料。

今天我們要講到的是使用token的方式,token稱之為令牌。cookie、session和token都有其應用場景,沒有誰好誰壞,不過我們開發資料介面類的Web應用,目前用token還是比較多的。

token認證的大致步驟是這樣的:

  • 使用者登入,伺服器端獲取使用者名稱密碼,查詢使用者表,如果存在該使用者且第一次登入(或者token過期),生成token,否則返回錯誤資訊
  • 如果不是第一次登入,且token未過期,更新token值

接下來,我們建立兩個model,如下所示(token也可以儲存在user表中,不過建議儲存在user表中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django.db import models

# Create your models here.


class User(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=32)
user_type_entry = (
(1, 'Delux'),
(2, 'SVIP'),
(3, "VVIP")
)
user_type = models.IntegerField(choices=user_type_entry)
address = models.CharField(max_length=32)

def __str__(self):
return self.username


class UserToken(models.Model):
user = models.OneToOneField("User", on_delete=models.CASCADE)
token = models.CharField(max_length=128)

我們無需實現get方法,因為涉及登入認證,所有寫post方法介面,登入都是post請求,檢視類如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from django.http import JsonResponse

from rest_framework.views import APIView

from .models import User, Book, UserToken
from .utils import get_token


class UserView(APIView):

def post(self, request):
response = dict()
try:
username = request.data['username']
password = request.data['password']

user_instance = User.objects.filter(
user_name=username,
password=password
).first()

if user_instance:
access_token = get_token.generater_token()

UserToken.objects.update_or_create(user=user_instance, defaults={
"token": access_token
})
response["status_code"] = 200
response["status_message"] = "登入成功"
response["access_token"] = access_token
response["user_role"] = user_instance.get_user_type_display()
else:
response["status_code"] = 201
response["status_message"] = "登入失敗,使用者名稱或密碼錯誤"
except Exception as e:
response["status_code"] = 202
response["status_message"] = str(e)

return JsonResponse(response)

簡單寫了個獲取隨機字串的方法用來生成token值:

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-
import uuid


def generater_token():
random_str = ''.join(str(uuid.uuid4()).split('-'))
return random_str

以上就是token的簡單生成方式,當然,在生產環境中不會如此簡單,關於token也有相關的庫,好了,我們構造幾條資料之後,可以通過POSTMAN工具來建立幾個使用者的token資訊。

接下來,如何對已經登入成功的使用者實現訪問授權呢?也就是說,只有登入過的使用者(有token值)才能訪問特定的資料,該DRF的認證元件出場了。

DRF認證元件使用

首先,我們來看一看,DRF認證元件的使用方式,首先,我們必須新建一個認證類,之後的認證邏輯就包含在這個類裡面:

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserAuth(object):

def authenticate_header(self, request):
pass

def authenticate(self, request):
user_post_token = request.query_params.get('token')

token_object = UserToken.objects.filter(token=user_post_token).first()
if token_object:
return token_object.user.username, token_object.token
else:
raise APIException("認證失敗")

實現方式看上去非常簡單,到token表裡面檢視token是否存在,然後根據這個資訊,返回對應資訊即可,然後,在需要認證通過才能訪問的資料介面裡面註冊認證類即可:

1
2
3
4
5
6
class BookView(ModelViewSet):

authentication_classes = [UserAuth, UserAuth2]

queryset = Book.objects.all()
serializer_class = BookSerializer

至於為什麼這麼寫,接下來,我們一起分析原始碼,大家就都非常清楚了。

DRF認證原始碼剖析

前面的步驟都差不多,我們來看有差別的地方,我們說,request物件是APIView重寫的,這個是在dispatch方法裡面實現的,繼續往後看dispatch方法,我們會看到self.initial方法,就是在這個方法裡面,我們會看到認證、許可權、頻率幾個元件的實現:

  • 執行self.initial()方法
  • 執行self.perform_authentication(request),方法,注意,新的request物件被傳遞進去了
  • 該方法只有一行request.user,根據之前的經驗,解析器(request.data),我們知道這個user肯定也是request對的一個屬性方法
  • 所料不錯,該方法繼續執行self._authenticate(),注意此時的self是request物件
  • 該方法會迴圈self.authenticators,而這個變數是在重新例項化request物件時通過引數傳遞的
  • 傳遞該引數是通過get_authenticatos()的返回值來確定的,它的返回值是
    • [ auth for auth in self.authentication_classes ]
    • 也就是我們的BookView裡面定義的那個類變數,也就是認證類
  • 一切都明朗了,迴圈取到認證類,例項化,並且執行它的authenticate方法
    • 這就是為什麼認證類裡面需要有該方法
    • 如果沒有該方法,認證的邏輯就沒辦法執行
    • 至於類裡面的header方法,照著寫就行,有興趣的可以研究原始碼,這裡就不細究了
  • 該方法如果執行成功就返回一個元組,執行完畢
    • 如果失敗,它會捕捉一個APIException
    • 如果我們不希望認證通過,可以raise一個APIException

這就是認證元件的實現方式,非常簡單。

多個認證類的實現

並且,我們還可以指定多個認證類,只是需要注意的是,如果需要返回什麼資料,請在最後一個認證類中返回,因為如果在前面返回,在self._authentication()方法中會對返回值進行判斷,如果不為空,認證的過程就會中止,多個認證類的實現方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UserAuth2(object):

def authenticate(self, request):
raise APIException("認證失敗")


class UserAuth(object):

def authenticate_header(self, request):
pass

def authenticate(self, request):
user_post_token = request.query_params.get('token')

token_object = UserToken.objects.filter(token=user_post_token).first()
if token_object:
return token_object.user.username, token_object.token
else:
raise APIException("認證失敗")


class BookView(ModelViewSet):

authentication_classes = [UserAuth, UserAuth2]

如果不希望每次都寫那個無用的authenticate_header方法,我們可以這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rest_framework.authentication import BaseAuthentication

class UserAuth2(BaseAuthentication):

def authenticate(self, request):
raise APIException("認證失敗")


class UserAuth(BaseAuthentication):

def authenticate(self, request):
user_post_token = request.query_params.get('token')

token_object = UserToken.objects.filter(token=user_post_token).first()
if token_object:
return token_object.user.user_name, token_object.token
else:
raise APIException("認證失敗")

繼承BaseAuthentication類即可。

全域性認證

如果希望所有的資料介面都需要認證怎麼辦?很簡單,還是根據之前的經驗,就是這句程式碼:

1
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES

如果認證類自己沒有authentication_classes,就會到settings中去找,通過這個機制,我們可以將認證類寫入到settings檔案中即可實現全域性認證:

1
2
3
4
5
6
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'authenticator.utils.authentication.UserAuth',
'authenticator.utils.authentication.UserAuth2',
),
}

好了,認證到這裡就差不多了。接下來繼續介紹許可權元件

許可權元件

與認證元件幾乎差不多,我們直接看使用方式吧

許可權元件使用

定義許可權類:

1
2
3
4
5
6
7
class UserPerms():
message = "您沒有許可權訪問該資料"
def has_permission(self, request, view):
if request.user.user_type > 2:
return True
else:
return False

同樣的邏輯,同樣的方式,只是執行許可權的方法名與執行認證的方法名不一樣而已,名為has_permission,並且需要將當前的檢視類傳遞給該方法。

檢視類中加入permission_classes變數:

1
2
3
4
5
6
7
class BookView(ModelViewSet):

authentication_classes = [UserAuth]
permission_classes = [UserPerms2]

queryset = Book.objects.all()
serializer_class = BookSerializer
許可權元件原始碼剖析

許可權元件的原始碼與認證元件是一樣的。

頻率元件

使用自定義方式實現對ip地址進行訪問頻率控制

使用方式介紹,上面兩個元件也是幾乎一樣,只是用來做判斷的邏輯不一樣而已,下面是作業的答案:

throttles.py(該方式沒有DRF提供的方式簡潔)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import time
import math

from rest_framework import exceptions


class MyException(exceptions.Throttled):
default_detail = '連線次數過多'
extra_detail_plural = extra_detail_singular = '請在{wait}秒內訪問'

def __init__(self, wait=None, detail=None, code=None):
super().__init__(wait=wait, detail=detail, code=code)


class VisitThrottle():
user_visit_information = dict()
visited_times = 1
period = 60
allow_times_per_minute = 5
first_time_visit = True

def allow_request(self, request, view):
self.request_host = request_host = request.META.get("REMOTE_ADDR")
current_user_info = self.user_visit_information.get(request_host, None)

if not self.__class__.first_time_visit:
self.user_visit_information[request_host][0] += 1
current_visit_times = self.user_visit_information[request_host][0]

if current_visit_times > self.allow_times_per_minute:
if self._current_time - current_user_info[1] <= self.period:
if len(current_user_info) > 2:
current_user_info[2] = self._time_left
else:
current_user_info.append(self._time_left)

view.throttled = self.throttled
return None
else:
self.__class__.first_time_visit = True

if self.first_time_visit:
self.__class__.first_time_visit = False
self._initial_infomation()

return True

def wait(self):
return self.period - self.user_visit_information[self.request_host][2]

def throttled(self, request, wait):
raise MyException(wait=wait)

@property
def _current_time(self):
return time.time()

@property
def _time_left(self):
return math.floor(self._current_time - self.user_visit_information.get(self.request_host)[1])

def _initial_infomation(self):
self.user_visit_information[self.request_host] = [self.visited_times, self._current_time]

檢視類中:

1
2
3
4
class BookView(ModelViewSet):
throttle_classes = [ VisitThrottle ]
queryset = Book.objects.all()
serializer_class = BookSerializer
使用DRF簡單頻率控制實現對使用者進行訪問頻率控制

區域性訪問頻率控制

1
2
3
4
5
6
7
8
from rest_framework.throttling import SimpleRateThrottle


class RateThrottle(SimpleRateThrottle):
rate = '5/m'

def get_cache_key(self, request, view):
return self.get_ident(request)

rate代表訪問評率,上面表示每分鐘五次,get_cache_key是必須存在的,它的返回值告訴當前頻率控制組件要使用什麼方式區分訪問者(比如ip地址)。

之後在檢視中使用即可:

1
2
3
4
5
6
7
8
9
from .utils.throttles import RateThrottle

# Create your views here.


class BookView(ModelViewSet):
throttle_classes = [ RateThrottle ]
queryset = Book.objects.all()
serializer_class = BookSerializer
全域性訪問頻率控制

首先定義一個頻率控制類,並且必須繼承SimpleRateThrottle這個類,它是DRF提供的一個方便的頻率控制類,請看下面的程式碼:

1
2
3
4
5
class RateThrottle(SimpleRateThrottle):
scope = "visit_rate"

def get_cache_key(self, request, view):
return self.get_ident(request)

另外,我們需要在全域性配置頻率控制引數

1
2
3
4
5
6
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": ('throttler.utils.throttles.RateThrottle',),
"DEFAULT_THROTTLE_RATES": {
"visit_rate": "5/m"
}
}

這樣就實現了,每分鐘最多五次訪問的邏輯。

今日總結

  • retrieve方法原始碼剖析
  • 認證元件的使用方式及原始碼剖析
  • 許可權元件的使用方式及原始碼剖析
  • 頻率元件的使用方式及原始碼剖析

                                                    轉自:pizzali