drf 認證校驗及原始碼分析
認證校驗
認證校驗是十分重要的,如使用者如果不登陸就不能訪問某些介面。
drf
中認證的寫法流程如下:
1.寫一個類,繼承BaseAuthentication,並且覆寫其authenticate方法
2.當認證通過後應該返回兩個值,並且第一個值會傳遞給request.user這個屬性中,第二個值將會傳遞給request.auth這個屬性中
3.如果認證失敗,則丟擲異常APIException或者AuthenticationFailed,它會自動捕獲並返回
4.當前認證類設定是全域性使用還是區域性使用
準備工作
我們有一個登入功能,並且還有一個查詢商品的介面,只有當用戶登入後才能進行查詢,否則就不可以。
模型表
兩張表如下:
from django.db import models class User(models.Model): # 使用者 user_id = models.AutoField(primary_key=True) user_name = models.CharField(max_length=32) user_password = models.CharField(max_length=32) user_token = models.CharField(max_length=64,unique=True,null=True) # token,唯一 def __str__(self): return self.user_name class Meta: db_table = "" managed = True verbose_name = "User" verbose_name_plural = "Users" class Merchandise(models.Model): # 商品 merchandise_id = models.AutoField(primary_key=True) merchandise_name = models.CharField(max_length=32) merchandise_price = models.IntegerField() def __str__(self): return self.merchandise_name class Meta: db_table = "" managed = True verbose_name = "Merchandise" verbose_name_plural = "Merchandises"
使用者表的資料如下:
商品表的資料如下:
現在,只有當用戶登入後,才能夠訪問商品的介面。
也就是說,使用者的token
自動如果為空,將會被認為沒有登陸。
序列類
下面是序列類,我們只展示商品,使用者列表將不會展示:
from rest_framework.serializers import ModelSerializer from . import models class MerchandiseModelSerializer(ModelSerializer): class Meta: model = models.Merchandise fields = "__all__"
檢視
檢視,我們只寫了關於使用者登入與商品的介面:
from uuid import uuid4
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from . import models
from . import ser
from . import authLogin # 匯入認證的檔案
class MerchandiseAPI(ModelViewSet):
queryset = models.Merchandise.objects.all()
serializer_class = ser.MerchandiseModelSerializer
class Login(APIView):
def post(self,request):
# 代表使用者登入
login_msg = {
"user_name": request.data.get("user_name"),
"user_password": request.data.get("user_password"),
}
user_obj = models.User.objects.filter(**login_msg).first()
if user_obj:
token = uuid4() # 生成隨機字串
user_obj.user_token = token
user_obj.save()
return Response(data="登入成功",headers={"token":token}) # 返回隨機字串
else:
return Response(data="登入失敗,使用者名稱或密碼錯誤")
url
使用自動生成路由:
from django.contrib import admin
from django.urls import path, re_path
from rest_framework.routers import SimpleRouter
from app01 import views
router = SimpleRouter()
router.register("merchandises",views.MerchandiseAPI)
urlpatterns = [
path('admin/', admin.site.urls),
path('login/',views.Login.as_view()),
]
urlpatterns.extend(router.urls)
基本使用
認證類
接下來我們要書寫一個認證類:
from rest_framework.authentication import BaseAuthentication # 繼承的基類
from rest_framework.exceptions import AuthenticationFailed # 異常
from . import models
from django.http import request
class LoginVerify(BaseAuthentication):
def authenticate(self, request):
token = request.META.get("HTTP_TOKEN")
# 如果在請求頭中設定的是token的key名,獲取時一定要全大寫並加上HTTP
if not token:
raise AuthenticationFailed("請求失敗,請求頭中缺少token")
else:
user_obj = models.User.objects.filter(user_token=token).first() # 獲取使用者物件
if user_obj:
return user_obj,user_obj.user_token # 返回使用者本身和token。這樣request.user裡面就能拿到該使用者了
else:
raise AuthenticationFailed("token不存在,使用者不存在,請不要偽造登入")
區域性使用
只需要在商品介面中設定一個類屬性,該介面便會進行認證。
class MerchandiseAPI(ModelViewSet):
authentication_classes = [authLogin.LoginVerify] # 使用認證
queryset = models.Merchandise.objects.all()
serializer_class = ser.MerchandiseModelSerializer
全域性使用
只需要在settings.py
中進行配置。
REST_FRAMEWORK={
"DEFAULT_AUTHENTICATION_CLASSES":["app01.authLogin.LoginVerify",]
}
如果想取消某個介面的認證,則在其中設定類屬性authentication_classes
是一個空列表。
如下所示,登入功能不需要驗證,我們對他取消掉即可。
class Login(APIView):
authentication_classes = []
原始碼分析
流程分析
由於modelViewSet
繼承自APIView
,所以我們直接看as_view()
,在下面這一句程式碼中,將會對request
進行二次封裝。
def dispatch(self, request, *args, **kwargs):
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs) # 這裡
self.request = request
self.headers = self.default_response_headers # deprecate?
在二次封裝中,例項化出了一個Request
物件並返回了,在例項化時,會呼叫self.get_authenticators()
方法,此時的self
是我們自定義的檢視類,切記這一點。
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(), # 看這裡,獲取認方式
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
下面是get_authenticators()
的程式碼,可以看見它會迴圈self.authentication_classes
這個可迭代物件,如果你沒有傳遞這個可迭代物件,那麼該物件是一個預設的設定。
def get_authenticators(self):
return [auth() for auth in self.authentication_classes] # ( authLogin.LoginVerify呼叫,例項化 )
如果沒有傳遞,將會找到APIView
中的預設設定:
class APIView(View):
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES # 預設的設定,預設的認證類,可以自己看一下
如果有進行傳遞,可以發現它是使用了一個括號,這就代表會呼叫,由於傳入的是一個類,所以它會進行例項化。
所以我們可以認為request.authenticators
這個引數是一個tuple
,裡面包含了認證類的例項化物件。
然後,request
就被二次包裝完畢了。接下來執行 self.initial()
,現在的self
依然是我們自定義的檢視類。
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
下面是self.inital()
的程式碼,
def initial(self, request, *args, **kwargs):
self.format_kwarg = self.get_format_suffix(**kwargs)
self.perform_authentication(request) # 只看這個,認證相關的
self.check_permissions(request)
self.check_throttles(request)
到了self.perform_authentication()
時,它傳遞進了個request
,並且會去找user
這個屬性抑或是被property
裝飾的方法,所以我們需要到Request
這個類中去找,需要注意的是如果user
是一個方法,這代表會自動傳遞進self
,此時的self
則是我們經過二次封裝的request
物件。
可以發現它是一個被裝飾的方法。很顯然我們沒有_user
這個方法或屬性,會執行with
語句,其實直接看self._authenticate()
即可。再次強調,此次的self
是二次封裝的request
物件。
@property
def user(self):
if not hasattr(self, '_user'):
with wrap_attributeerrors():
self._authenticate()
return self._user
下面是整個程式碼的核心。
def _authenticate(self):
for authenticator in self.authenticators: # 迴圈認證類物件 ( authLogin.LoginVerify的例項化 )
try:
user_auth_tuple = authenticator.authenticate(self) # 這裡會找authenticate方法並將request物件進行傳遞,我們的認證類繼承了BaseAuthentication這個類,它會實現一個介面方法, 但會丟擲異常。
except exceptions.APIException: # 如果沒有實現介面方法,或在驗證時丟擲異常都會被這裡捕獲
self._not_authenticated() # 執行這裡 self.user將會是匿名使用者AnonymousUser,而self.auth則是None
raise
if user_auth_tuple is not None: # 如果返回的值不是空
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple # 分別賦值給self.user,以及self.auth中
return # 返回
self._not_authenticated() # 上面有認證物件就會return,沒有還是設定匿名使用者和None
最後總結
其實看了原始碼後,你可以發現我們的認證類可以不繼承BaseAuthentication
,但是推薦繼承會更規範,因為這個基類實現了抽象介面。
其次,它將返回的兩個值分別賦值給了request.user
以及request.auth
。
如果你沒有返回值,那麼對應的,request.user
就是匿名使用者,request.auth
就是None
。
如果你沒有配置認證類,其實它會走預設的認證類。
老規矩,關於配置認證類時依舊是先用區域性的,再用全域性的,最後是用預設的,如果你的上面的原始碼確實有感覺了的話,應該能夠看