源碼剖析Django REST framework的認證方式及自定義認證

源碼剖析Django REST framework的認證方式

由Django的CBV模式流程,能夠知道在url匹配完成後,會執行自定義的類中的as_view方法javascript

若是自定義的類中沒有定義as_view方法,根據面向對象中類的繼承能夠知道,則會執行其父類View中的as_view方法html

在Django的View的as_view方法中,又會調用dispatch方法java

如今來看看Django restframework的認證流程python

Django restframework是基於Django的框架,因此基於CBV的模式也會執行自定義的類中的as_view方法nginx

先新建一個項目,配置url數據庫

from django.conf.urls import url
from django.contrib import admin

from app01 import views

urlpatterns = [
    url(r'^user/', views.UserView.as_view()),
]

views.py文件內容django

from django.shortcuts import render,HttpResponse
from rest_framework.views import APIView

class UserView(APIView):

    def get(self,request,*args,**kwargs):
        print(request.__dict__)
        print(request.user)
        return HttpResponse("UserView GET")

    def post(self,request,*args,**kwargs):
        return HttpResponse("UserView POST")

啓動項目,用瀏覽器向http://127.0.0.1:8000/user/發送get請求api

能夠知道請求發送成功。如今來看看源碼流程,因爲UserView繼承APIView,查看APIView中的as_view方法瀏覽器

class APIView(View):
    ...
 @classmethod
    def as_view(cls, **initkwargs):
        if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
            def force_evaluation():
                raise RuntimeError(
                    'Do not evaluate the `.queryset` attribute directly, '
                    'as the result will be cached and reused between requests. '
                    'Use `.all()` or call `.get_queryset()` instead.'
                )
            cls.queryset._fetch_all = force_evaluation

        view = super(APIView, cls).as_view(**initkwargs)
        view.cls = cls
        view.initkwargs = initkwargs
        return csrf_exempt(view)

經過super來執行APIView的父類Django的View中的as_view方法。上一篇文章源碼解析Django CBV的本質中已經知道,View類的as_view方法會調用dispatch方法。ruby

View類的as_view方法源碼以下所示

class View(object):
    ...
    @classonlymethod
    def as_view(cls, **initkwargs):
        ...
        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        ...

as_view方法中的self實際上指的是自定義的UserView這個類,上面的代碼會執行UserView類中dispatch方法。

因爲UserView類中並無定義dispatch方法,而UserView類繼承自Django restframework的APIView類,因此會執行APIView類中的dispatch方法

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?

    try:
        self.initial(request, *args, **kwargs)
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(),
                              self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed

        response = handler(request, *args, **kwargs)

    except Exception as exc:
        response = self.handle_exception(exc)

    self.response = self.finalize_response(request, response, *args, **kwargs)
    return self.response

能夠看到,先執行initialize_request方法處理瀏覽器發送的request請求

來看看initialize_request方法的源碼

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
    )

在initialize_request方法裏,把瀏覽器發送的request和restframework的處理器,認證,選擇器等對象列表做爲參數實例化Request類中獲得新的request對象並返回,其中跟認證相關的對象就是authenticators。

def get_authenticators(self):
    """ Instantiates and returns the list of authenticators that this view can use. """
    return [auth() for auth in self.authentication_classes]

get_authenticators方法經過列表生成式獲得一個列表,列表中包含認證類實例化後的對象

在這裏,authentication_classes來自於api_settings的配置

authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

經過查看api_settings的源碼能夠知道,能夠在項目的settings.py文件中進行認證相關的配置

api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)

def reload_api_settings(*args, **kwargs):
    setting = kwargs['setting']
    if setting == 'REST_FRAMEWORK':
        api_settings.reload()

Django restframework經過initialize_request方法對原始的request進行一些封裝後實例化獲得新的request對象

而後執行initial方法來處理新獲得的request對象,再來看看initial方法中又執行了哪些操做

def initial(self, request, *args, **kwargs):
    self.format_kwarg = self.get_format_suffix(**kwargs)
    neg = self.perform_content_negotiation(request)
    request.accepted_renderer, request.accepted_media_type = neg

    version, scheme = self.determine_version(request, *args, **kwargs)
    request.version, request.versioning_scheme = version, scheme

    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)

由上面的源碼能夠知道,在initial方法中,執行perform_authentication來對request對象進行認證操做

def perform_authentication(self, request):
    request.user

perform_authentication方法中調用執行request中的user方法這裏的request是封裝了原始request,認證對象列表,處理器列表等以後的request對象

class Request(object):
    ...
 @property
    def user(self):
        """ Returns the user associated with the current request, as authenticated by the authentication classes provided to the request. """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user

從request中獲取_user的值,若是獲取到則執行_authenticate方法,不然返回_user

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.user, self.auth = user_auth_tuple
            return

在這裏self.authenticators其實是get_authenticators方法執行完成後返回的對象列表

class Request(object): def __init__(self, request, parsers=None, authenticators=None, negotiator=None, parser_context=None): assert isinstance(request, HttpRequest), ( 'The `request` argument must be an instance of ' '`django.http.HttpRequest`, not `{}.{}`.' .format(request.__class__.__module__, request.__class__.__name__) ) self._request = request self.parsers = parsers or () self.authenticators = authenticators or () ...

循環認證的對象列表,執行每個認證方法的類中的authenticate方法,獲得經過認證的用戶及用戶的口令的元組,並返回元組完成認證的流程

_authenticate方法中使用了try/except方法來捕獲authenticate方法可能出現的異常

若是出現異常,就調用_not_authenticated方法來設置返回元組中的用戶及口令並終止程序繼續運行

總結,Django restframework的認證流程以下圖

Django restframework內置的認證類

在上面的項目例子中,在UsersView的get方法中,打印authentication_classesrequest._user的值

class UserView(APIView):
    # authentication_classes = [MyAuthentication,]

    def get(self,request,*args,**kwargs):
        print('authentication_classes:', self.authentication_classes)
        print(request._user)
        return HttpResponse("UserView GET")

打印結果爲

authentication_classes: [<class 'rest_framework.authentication.SessionAuthentication'>, <class 'rest_framework.authentication.BasicAuthentication'>]
AnonymousUser

由此能夠知道,authentication_classes默認是Django restframework內置的認證類,而request._user爲AnonymousUser,由於發送GET請求,用戶沒有進行登陸認證,因此爲匿名用戶

在視圖函數中導入這兩個類,再查看這兩個類的源碼,能夠知道

class BasicAuthentication(BaseAuthentication):

    www_authenticate_realm = 'api' 

    def authenticate(self, request):

        ...

    def authenticate_credentials(self, userid, password):

        ...

class SessionAuthentication(BaseAuthentication):

    def authenticate(self, request):

        ...

    def enforce_csrf(self, request):

        ...
        
class TokenAuthentication(BaseAuthentication):
    ...

從上面的源碼能夠發現,這個文件中不只定義了SessionAuthenticationBasicAuthentication這兩個類,

相關的類還有TokenAuthentication,並且這三個認證相關的類都是繼承自BaseAuthentication

從上面的源碼能夠大概知道,這三個繼承自BaseAuthentication的類是Django restframework內置的認證方式.

自定義認證功能

在上面咱們知道,Request會調用認證相關的類及方法,APIView會設置認證相關的類及方法

因此若是想自定義認證功能,只須要重寫authenticate方法及authentication_classes的對象列表便可

修改上面的例子的views.py文件

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions

TOKEN_LIST = [  # 定義token_list
    'aabbcc',
    'ddeeff',
]

class UserAuthView(BaseAuthentication):
    def authenticate(self, request):
        tk = request._request.GET.get("tk")  # request._request爲原生的request

        if tk in TOKEN_LIST:
            return (tk, None)  # 返回一個元組
        raise exceptions.AuthenticationFailed("用戶認證失敗")

    def authenticate_header(self, request):
        # 若是不定義authenticate_header方法會拋出異常
        pass

class UserView(APIView):
    authentication_classes = [UserAuthView, ]

    def get(self, request, *args, **kwargs):
        print(request.user)

        return HttpResponse("UserView GET")

啓動項目,在瀏覽器中輸入http://127.0.0.1:8000/users/?tk=aabbcc,而後回車,在服務端後臺會打印

aabbcc

把瀏覽器中的url換爲http://127.0.0.1:8000/users/?tk=ddeeff,後臺打印信息則變爲

ddeeff

這樣就實現REST framework的自定義認證功能

Django restframework認證的擴展

基於Token進行用戶認證

修改上面的項目,在urls.py文件中添加一條路由記錄

from django.conf.urls import url
from django.contrib import admin
from app01 import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^users/',views.UsersView.as_view()),
    url(r'^auth/',views.AuthView.as_view()),
]

修改視圖函數

from django.shortcuts import render,HttpResponse
from rest_framework.views import APIView
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
from django.http import JsonResponse

def gen_token(username):
    """ 利用時間和用戶名生成用戶token :param username: :return: """
    import time
    import hashlib
    ctime=str(time.time())
    hash=hashlib.md5(username.encode("utf-8"))
    hash.update(ctime.encode("utf-8"))
    return hash.hexdigest()

class AuthView(APIView):
    def post(self, request, *args, **kwargs):
        """ 獲取用戶提交的用戶名和密碼,若是用戶名和密碼正確,則生成token,並返回給用戶 :param request: :param args: :param kwargs: :return: """
        res = {'code': 1000, 'msg': None}
        user = request.data.get("user")
        pwd = request.data.get("pwd")

        from app01 import models
        user_obj = models.UserInfo.objects.filter(user=user, pwd=pwd).first()

        if user_obj:
            token = gen_token(user) # 生成用戶口令

            # 若是數據庫中存在口令則更新,若是數據庫中不存在口令則建立用戶口令
            models.Token.objects.update_or_create(user=user_obj, defaults={'token': token})
            print("user_token:", token)
            res['code'] = 1001
            res['token'] = token
        else:
            res['msg'] = "用戶名或密碼錯誤"

        return JsonResponse(res)
    
class UserAuthView(BaseAuthentication):
    def authenticate(self,request):
        tk=request.query_params.GET.get("tk")   # 獲取請求頭中的用戶token

        from app01 import models

        token_obj=models.Token.objects.filter(token=tk).first()

        if token_obj:   # 用戶數據庫中已經存在用戶口令返回認證元組
            return (token_obj.user,token_obj)

        raise exceptions.AuthenticationFailed("認證失敗")

    def authenticate_header(self,request):
        pass

class UsersView(APIView):
    authentication_classes = [UserAuthView,]

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

        return HttpResponse(".....")

建立用戶數據庫的類

from django.db import models
 class UserInfo(models.Model): user=models.CharField(max_length=32) pwd=models.CharField(max_length=64) email=models.CharField(max_length=64) class Token(models.Model): user=models.OneToOneField(UserInfo) token=models.CharField(max_length=64)

建立數據庫,並添加兩條用戶記錄

再建立一個test_client.py文件,來發送post請求

import requests

response=requests.post(
    url="http://127.0.0.1:8000/auth/",
    data={'user':'user1','pwd':'user123'},
)

print("response_text:",response.text)

啓動Django項目,運行test_client.py文件,則項目的響應信息爲

response_text: {"code": 1001, "msg": null, "token": "eccd2d256f44cb25b58ba602fe7eb42d"}

由此,就完成了自定義的基於token的用戶認證

若是想在項目中使用自定義的認證方式時,能夠在authentication_classes繼承剛纔的認證的類便可

authentication_classes = [UserAuthView,]

全局自定義認證

在正常的項目中,一個用戶登陸成功以後,進入本身的主頁,能夠看到不少內容,好比用戶的訂單,用戶的收藏,用戶的主頁等

此時,難倒要在每一個視圖類中都定義authentication_classes,而後在authentication_classes中追加自定義的認證類嗎?

經過對Django restframework認證的源碼分析知道,能夠直接在項目的settings.py配置文件中引入自定義的認證類,便可以對全部的url進行用戶認證流程

在應用app01目錄下建立utils包,在utils包下建立auth.py文件,內容爲自定義的認證類

from rest_framework import exceptions
from api import models

class Authtication(object):
    def authenticate(self,request):
        token = request._request.GET.get("token")       # 獲取瀏覽器傳遞的token
        token_obj = models.UserToken.objects.filter(token=token).first()    # 到數據庫中進行token查詢,判斷用戶是否經過認證
        if not token_obj:
            raise exceptions.AuthenticationFailed("用戶認證失敗")

        # restframework會將元組賦值給request,以供後面使用
        return (token_obj.user,token_obj)
    
    # 必須建立authenticate_header方法,不然會拋出異常
    def authenticate_header(self,request):
        pass

在settings.py文件中添加內容

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES':['app01.utils.auth.Authtication',]
}

修改views.py文件

from django.shortcuts import render, HttpResponse
from rest_framework.views import APIView
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
from django.http import JsonResponse

def gen_token(username):
    """ 利用時間和用戶名生成用戶token :param username: :return: """
    import time
    import hashlib
    ctime = str(time.time())
    hash = hashlib.md5(username.encode("utf-8"))
    hash.update(ctime.encode("utf-8"))
    return hash.hexdigest()

class AuthView(APIView):
    authentication_classes = []     # 在這裏定義authentication_classes後,用戶訪問auth頁面不須要進行認證
    def post(self, request, *args, **kwargs):
        """ 獲取用戶提交的用戶名和密碼,若是用戶名和密碼正確,則生成token,並返回給用戶 :param request: :param args: :param kwargs: :return: """
        res = {'code': 1000, 'msg': None}
        user = request.data.get("user")
        pwd = request.data.get("pwd")

        from app01 import models
        user_obj = models.UserInfo.objects.filter(user=user, pwd=pwd).first()

        if user_obj:
            token = gen_token(user)  # 生成用戶口令

            # 若是數據庫中存在口令則更新,若是數據庫中不存在口令則建立用戶口令
            models.Token.objects.update_or_create(user=user_obj, defaults={'token': token})
            print("user_token:", token)
            res['code'] = 1001
            res['token'] = token
        else:
            res['msg'] = "用戶名或密碼錯誤"

        return JsonResponse(res)

class UserView(APIView):
    def get(self, request, *args, **kwargs):
        return HttpResponse("UserView GET")

class OrderView(APIView):
    def get(self,request,*args,**kwargs):
        return HttpResponse("OrderView GET")

啓動項目,使用POSTMAN向http://127.0.0.1:8000/order/?token=eccd2d256f44cb25b58ba602fe7eb42dhttp://127.0.0.1:8000/user/?token=eccd2d256f44cb25b58ba602fe7eb42d發送GET請求,響應結果以下

在url中不帶token,使用POSTMAN向http://127.0.0.1:8000/order/http://127.0.0.1:8000/user/發送GET請求,則會出現"認證失敗"的提示

由此能夠知道,在settings.py配置文件中配置自定義的認證類也能夠實現用戶認證功能

配置匿名用戶

修改settings.py文件

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ['app01.utils.auth.Authtication', ],
    'UNAUTHENTICATED_USER': lambda :"匿名用戶",     # 用戶未登陸時顯示的名稱
    'UNAUTHENTICATED_TOKEN': lambda :"無效token", # 用戶未登陸時打印的token名
}

修改views.py文件中的OrderView類

class OrderView(APIView):
    authentication_classes = []         # authentication_classes爲空列表表示視圖類不進行認證
    def get(self,request,*args,**kwargs):
        print(request.user)
        print(request.auth)
        return HttpResponse("OrderView GET")

使用瀏覽器向http://127.0.0.1:8000/order/發送GET請求,後臺打印

這說明在settings.py文件中配置的匿名用戶和匿名用戶的token起到做用

建議把匿名用戶及匿名用戶的token都設置爲:None

Django restframework內置的認證類

從rest_framework中導入authentication

from rest_framework import authentication

能夠看到Django restframework內置的認證類

class BaseAuthentication(object):
    def authenticate(self, request):
        ...

    def authenticate_header(self, request):
        pass


class BasicAuthentication(BaseAuthentication):
    def authenticate(self, request):
        ...

    def authenticate_credentials(self, userid, password, request=None):
        ...

    def authenticate_header(self, request):
        ...


class SessionAuthentication(BaseAuthentication):

    def authenticate(self, request):
        ...

    def enforce_csrf(self, request):
        ...


class TokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        ...

    def authenticate_credentials(self, key):
        ...

    def authenticate_header(self, request):
        ...


class RemoteUserAuthentication(BaseAuthentication):
    def authenticate(self, request):
        ...

能夠看到,Django restframework內置的認證包含下面的四種:

BasicAuthentication
SessionAuthentication
TokenAuthentication
RemoteUserAuthentication

而這四種認證類都繼承自BaseAuthentication在BaseAuthentication中定義了兩個方法:authenticate和authenticate_header

總結:

爲了讓認證更規範,自定義的認證類要繼承 BaseAuthentication類
自定義認證類必需要實現authenticate和authenticate_header方法
authenticate_header方法的做用:在認證失敗的時候,給瀏覽器返回的響應頭,能夠直接pass,不實現authenticate_header程序會拋出異常
相關文章
相關標籤/搜索