Django Rest framework 之 认证

时间:2024-01-22 11:00:33

Django Rest framework 之 认证

django rest framework 官网

在学习django rest framework(下面简称drf)之前需要知道

  • 对RESTful API设计有一定了解 restful api设计风格
  • 对django框架有一定认识,本身drf就是基于django做的
  • 对python面向对象编程有了解(drf会对一些原生的django类做封装)

一、前言

在学习drf之前的时候,先简单说一下需要的预备知识。在django中,路由匹配之后,会进行路由分发,这个时候会有两种选择模式的选择。也就是FBVCBV

1、FBV

fbv就是在url中一个路径对应一个函数

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.index)
]

在视图函数中

def index(request):
    return render(request, 'index.html')

2、CBV

cbv就是在url中一个路径对应一个类,drf主要使用CBV

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^index/', views.IndexView.as_view())     # 执行类后面的as_view()方法,是父类里面的方法
]

在视图函数中

from django.views import View


class IndexView(View):
  
    # 以get形式访问会执行get函数,一般情况下获取数据
    def get(self, *args, **kwargs):  
        return HttpResponse('666')
      
    # 以post形式访问的话会执行post函数,一般情况下发送数据
    def post(self, *args, **kwargs):  
        return HttpResponse('999')

我们在路由匹配的时候看到url(r'^index/', views.IndexView.as_view()),那这个as_view()是什么,既然我们在视图类中没有定义这个as_view()方法,就应该到父类(也就是IndexView的父类View)中看一下View。以下是django源码,路径是\django\views\generic\base.py

class View:

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']  # 支持的各种http方法

    def __init__(self, **kwargs):
         pass

    @classonlymethod
    def as_view(cls, **initkwargs):  # url路由匹配进入as_view方法
        def view(request, *args, **kwargs):
            return self.dispatch(request, *args, **kwargs)  # 返回dispath方法
        return view

    def dispatch(self, request, *args, **kwargs):  # dispath方法是drf的关键,dispath方法会通过反射,通过请求的方法,分发到各个视图类的方法中
        pass

3、django的请求周期

因此根据CBV和FBVdjango的生命周期可以又两类

  • FBV:请求通过uwsgi网关,中间件,然后进入路由匹配,进入视图函数,连接数据库ORM操作,模板渲染,返回经过中间件,最终交给浏览器response字符串。
  • CBV:请求通过uwsgi网关,中间件,然后进入路由匹配,这里就与FBV有区别了,因为不再是试图函数而是视图类,说的详细一点,先经过父类View的dispath方法,进行请求方法的判断,在分发到视图类的方法,连接数据库ORM操作,模板渲染,返回经过中间件,最终交给浏览器response字符串。

而再drf中主要使用CBV,生命周期就变成了如下
请求通过uwsgi网关,中间件,然后进入路由匹配,这里就有区别了,先经过drfAPIView类中的dispath方法(这里假定视图类没有重写APIView中的dispath方法),在dispath中对request请求进行封装,反射回到视图类,连接数据库ORM操作,模板渲染,返回经过中间件,最终交给浏览器响应字符串。

4、面向对象

说到面向对象就是三个特性,封装,多态,继承。

<1>、子类重写父类方法

我们在继承父类的时候往往会重写父类中的方法,例如

class A:
    def get_name(self):
        return self.name
    def return_name(self):
        if hasattr(self, 'name'):
            return 'name: ' + getattr(self, 'name', None)    
    
    
class B(A):
    name = "b"
    def get_name(self):
        return self.name
    
b = B()
b.get_name()  # 输出B
b.return_name()  # 输出name: B,这里由于B类中没有实现return_name方法,实例化B得到b之后,会调用父类A中的return_name方法,hasattr方法会查找类中是否有name属性,这里虽然在类A中没有,会向下查找B类中是否有name属性,然后返回'name: ' + getattr(self, 'name', None)  ,也就是name:b

这是简单的子类方法重写父类中的方法,我们再使用drf的认证,权限等组件是会经常对父类中的方法重写,从而细粒度的实现自己的功能。
请注意:事情往往不是绝对的,如果像重写python内置的基本数据类型,如字典,列表中的特殊方法,就会的到意想不到的结果,就是实例化的对象不再调用你重写的方法,而是调用本来的方法。这是因为python的一些基本类型的方法是由c语言编写的,python为了满足速度,抄近道不会再调用你重写的特殊方法。

<2>、mixin模式

class X(object):
    def f(self):
        print( 'x')

class A(X):
    def f(self):
        print('a')

    def extral(self):
        print('extral a')

class B(X):
    def f(self):
        print('b')

    def extral(self):
        print( 'extral b')

class C(A, B, X):
    def f(self):
        super(C, self).f()
        print('c')

print(C.mro())

c = C()
c.f()
c.extral()

这样做也可以输出结果

[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.X'>, <class 'object'>]  # 继承的顺序是 A-->B-->X-->object 这了的object在python3中是一切类的基类,包括object类本身。
a
c
extral a  # 虽然类C中没有实现接口extral(),却调用了父类A中的extral()方法

这样的继承虽然可以实现功能,但是有一个很明显的问题,那就是在面向对象中,一定要指明一个类到底是什么。也就是说,如果我想构造一个类,假如是Somthing,那么我想让这个类实现会飞,会游泳,会跑,三种行为,我可以这样做,同时继承,鸟,鱼,马三个类,像这样

class Bird:
    def fly(self):
        print('fly')

class Fish:
    def swim(self):
        print('swim')
        
class Horse:
    def run(self):
        print('run')
        
        
class Something(Bird, Fish, Horse):
    pass


s = Something()
s.fly()
s.swim()
s.run()

输出

fly
swim
run

可是实现会跑,会飞,会游泳的三种行为,但是这个类到底是什么,是鱼,是马,还是鸟,也就是说不知道Something到底是个什么类。为了解决这个问题,我们可以引用mixin模式。改写如下

class BirdMixin:
    def fly(self):
        print('fly')

class FishMixin:
    def swim(self):
        print('swim')
        
class Horse:
    def run(self):
        print('run')
        
        
class Something(BirdMixin,  FishMixin,  Horse):
    pass

这样就解决了上面的问题,也许你会发现,这其实没有什么变化,只是在类的命名加上了以Mixin结尾,其实这是一种默认的声明,告诉你,Something类其实是一种马,父类是HorseHorse,继承其他两个类,只是为了调用他们的方法而已,这种叫做mixin模式,在drf的源码种会用到。
例如drf中的generics 路径为rest_framework/generics.py

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    pass


class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    pass


class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    pass

相当于每多一次继承,子类可调用的方法就更多了。

二、生成项目

1、生成项目

这里可以使用pycharm作为集成开发工具,创建django项目查看Python和第三方库源码很方便,使用pycharm创建一个django项目,然后将django rest framework作为第三方包放入django项目中

2、数据库设计

先来看一下如果不使用drf怎么进行用户认证,通常是用字段验证的方式,来生成相应的数据库,在用户登录时候,对数据库查询,简单的数据库设计如下

from django.db import models


class UserInfo(models.Model):
    USER_TYPE = (
        (1,'普通用户'),
        (2,'VIP'),
        (3,'SVIP')
    )

    user_type = models.IntegerField(choices=USER_TYPE, default=1)
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)

class UserToken(models.Model):
    user = models.OneToOneField(UserInfo,on_delete=models.CASCADE)
    token = models.CharField(max_length=64)

简单的用户信息,每个用户关联一个一对一的usertoken做为验证
然后在项目目录下执行生成数据库命令

    python manage.py makemigrations
    python manage.py migrate

3、路由系统

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

from api.views import AuthView


urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^api/v1/auth/$', AuthView.as_view())
]

api/v1/auth/中的api分别代表接口和版本号,后面会说到

4、视图函数

  • md5函数根据用户名和用户的访问时间进行加密
  • 当用户第一次访问时,数据库创建用户,并将token字符串,存储到数据库
  • 当用户下次访问的时候,需要带着这个字符串与数据库比对,并返回相应的提示信息
    这里的token暂时没有放回浏览器端,真正项目中可以写入到浏览器cookie
from django.shortcuts import render, HttpResponse
from django.http import JsonResponse
from django.views import View

from api import models


def md5(user):
    import hashlib
    import time

    # 当前时间,相当于生成一个随机的字符串
    ctime = str(time.time())

    # token加密
    m = hashlib.md5(bytes(user, encoding='utf-8'))
    m.update(bytes(ctime, encoding='utf-8'))
    return m.hexdigest()


class AuthView(View):

    def get(self, request, *args, **kwargs):
        ret = {'code': 1000, 'msg': 'success', 'name': '偷偷'}
        ret = json.dumps(ret, ensure_ascii=False)

        return HttpResponse(ret)

    def post(self, request, *args, **kwargs):
        ret = {'code': 1000, 'msg': None}
        try:
            user = request.POST.get('username')
            pwd = request.POST.get('password')
            obj = models.UserInfo.objects.filter(username=user).first()

            if not obj:
                # 如果用户第一次登陆则创建用户
                obj = models.UserInfo.objects.create(username=user, password=pwd)
                ret['code'] = 1001
                ret['msg'] = '创建用户成功'

            # 为用户创建token
            token = md5(user)
            # 存在就更新,不存在就创建
            models.UserToken.objects.update_or_create(user=obj, defaults={'token': token})
            ret['token'] = token
        except Exception as e:
            ret['code'] = 1002
            ret['msg'] = '请求异常'
        return JsonResponse(ret)

第一次发送请求

返回请求信息

第二次发送请求

返回请求信息

这里没有使用drf的认证组件

三、使用Django rest framewok 认证组件

1、实例

假如用户想获取自己的订单信息,发送请求之后返回订单信息以json格式的数据返回。

from rest_framework.views import APIView
from django.http import JsonResponse
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions

from api import models


# 这里直接表示订单
ORDER_DICT = {
    1:{
        'name':'apple',
        'price':15
    },
    2:{
        'name':'狗子',
        'price':100
    }
}


class FirstAuthenticate(BaseAuthentication):
    # 添加自己的认证逻辑,基类BaseAuthentication中有一个必须要重写的接口

    def authenticate(self, request):
        pass

    def authenticate_header(self, request):
        pass


class MyAuthenticate(BaseAuthentication):
    # 添加自己的认证逻辑,基类BaseAuthentication中有两个必须要重写的接口

    def authenticate(self, request):
        token = request._request.GET.get('token')  # 获取token参数
        token_obj = models.UserToken.objects.filter(token=token).first()  # 在数据库UserToken查找是否有相应的对象
        if not token_obj:  # 如果没有,则报错
            raise exceptions.AuthenticationFailed('用户认证失败')
        return (token_obj.user, token_obj)  # 这里需要返回两个对象,分别是UserInfo对象和UserToken对象
  
    def authenticate_header(self, request):  # 返回相应头信息
          pass


class OrderView(APIView):
    # 用户想要获取订单,就要先通过身份认证、
    # 这里的authentication_classes 就是用户的认证类
    authentication_classes = [FirestAuthenticate, MyAuthenticate]
    
    def get(self, request, *args, **kwargs):
        ret = {
            'code': 1024,
            'msg': '订单获取成功',
        }
        try:
            ret['data'] = ORDER_DICT
        except Exception as e:
            pass
        return JsonResponse(ret)

这里继承了rest framek中的APIView,在APIView中将原生的request进行了封装,封装了一些用于认证,权限的类,在请求来的时候,会依次通过FirestAuthenticate MyAuthenticate两个类,并调用authenticate进行认证。
发送请求

返回订单的数据

认证成功

2、源码分析

这里推荐使用pycharm作为集成开发工具,可以ctrl+鼠标左键点击方法,或者类直接进入源码查看

<1>、第1步

在路由匹配之后会先进入到APIView中的as_view方法中,然后进入到djangoView中,

<2>、第2步

由于子类APIView已经实现了dispath方法,接着返回APIView中的disapth方法

<3>、第3步

然后会发现drf对原生request做的操作

<4>、第4步

这里的initialize_request,主要进行封装

<5>、第5步

而initial则会对调用封装类中的方法,实现各种功能

至此可以看到requestdrf中大概的流程。

3、drf认证流程

在上面第4步和第5步可以看到APIView中的两个方法的initialize_request,initial

我们进入到initialize_request,查看authenticators=self.get_authenticators()

这里的authentication_classes,其实是一个所有认证类的集合(指的是一个可以迭代的容器对象,如list,tuple等,而不是特指set()内置类型),

这里的api_settings其实就是django项目的全局配置文件settings.py,这说明我们可以在需要认证的视图函数多的情况下使用全局配置使得每一个进行认证。

<1>、全局与局部配置认证类

可以直接在settings.py中添加全局配置项

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES':  ['api.utils.authenticate.FirstAuthenticate', 'api.utils.authenticate.MyAuthenticate'],
}

那么如果我的个别视图类不想认证呢?可以这样写

class OrderView(APIView):
    # 这里没有重写authentication_classes属性,则使用全局配置的authentication_classes,即在setting.py中的authentication_classes。
    def get(self, request, *args, **kwargs):
          pass

class CartView(APIView):
     authentication_classes = [authenticate.FirstAuthenticate,]  # authentication_classes中只包含FirstAuthenticate,则只通过他的认证
    def get(self, request, *args, **kwargs):
          pass

class UserInfoView(APIView):
    authentication_classes = []  #  authentication_classes为空,则不会进行认证
    def get(self, request, *args, **kwargs):
          pass

<2>、究竟如何进行认证

上面说了想要定义多个认证规则,其实就是封装多个认证类,那么这些认证类如何进行认证呢?

这里的perform_authentication就是进行主要的功能,在request类中有一个_authenticate

来分析下源码

    def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        for authenticator in self.authenticators:  # 找到 authentication_classes,并循环每一个认证类
            try:
                user_auth_tuple = authenticator.authenticate(self)  # 调用认证类的authenticate方法,也就是上面我们实现的方法,并将返回值赋值给user_auth_tuple
            except exceptions.APIException:
                self._not_authenticated()  # 如果出错调用_not_authenticated,方法,下面会说到
                raise

            if user_auth_tuple is not None:  # 如果authenticate方法的返回值不为空
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple  # 这也就是为什么认证类的authenticate方法会返回两个对象的原因
                return

        self._not_authenticated()  # 如果没有通过认证,则调用_not_authenticated方法

    def _not_authenticated(self):
        """
        Set authenticator, user & authtoken representing an unauthenticated request.

        Defaults are None, AnonymousUser & None.
        """
        self._authenticator = None

        if api_settings.UNAUTHENTICATED_USER:
            self.user = api_settings.UNAUTHENTICATED_USER()
        else:
            self.user = None

        if api_settings.UNAUTHENTICATED_TOKEN:
            self.auth = api_settings.UNAUTHENTICATED_TOKEN()
        else:
            self.auth = None

_authenticate方法中调用authenticator.authenticate(self) 方法,返回给user_auth_tuple,并通过判断user_auth_tuple是否为空,其实就像是我从浏览器发送请求,request中携带我的用户认证信息,在进入视图类之前,通过一次一次调用认证类来查看我携带的认证信息是否正确,如果正确则返回数据库中正确的User对象。如果不通过或者没有认证信息,则在_not_authenticated中按照匿名用户处理。
来看一下authenticator.authenticate(self)中的authenticate(self)具体做了什么

在authenticate中可以添加具体的认证逻辑,当然也可以在视图类中书写,但是drf中提供的组件,可以使得代码耦合度更低,维护性更强,更方便。

<3>、匿名用户认证

上面_not_authenticatedUNAUTHENTICATED_TOKENUNAUTHENTICATED_USER说明,也可以通过在setting.py中定义匿名用户的认证。
只要再setting.py中添加如下

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ['api.utils.authenticate.FirstAuthenticate', 'api.utils.authenticate.MyAuthenticate'],
    "UNAUTHENTICATED_USER": None, # 匿名,request.user = None
    "UNAUTHENTICATED_TOKEN": None,# 匿名,request.auth = None
}

4、认证总结

要理解django rest framework ,就要先理解面向对象。子类继承父类属性和方法,而在基类中往往以定义抽象接口的形式,强制使子类重写抽象接口。不过抽象接口这往往是框架开发者做的,而不是我们要需要做的。实例化的对象可以调用所类的属性和方法,其实方法也可以看作是一种属性。子类新定义或者重写父类的属性,实例化的对象可以调用父类中的方法查询到子类的属性,就是说实例化的对象集所有父类子类于一身。子类中的方法或者属性会覆盖掉父类中的方法和属性,实例化对象调用的时候不会管父类中怎么样,所以在变量和方法命名的时候应该注意,或者也可以使用super等操作。
而在django rest framework中,对原生request做了封装。原本我们可以再视图类中的进行的比如访问限流,用户认证,权限管理等逻辑,封装到一个一个类中的方法中,在用户请求进入视图类之前,会先查找并迭代相关封装的类,然后调用这些类的相关方法,根据返回值判断是否满足认证,权限等功能。如果不通过则不会进入到视图类执行下一步,并返回相应的提示信息。这样分开的好处是当然就是最大程度的解耦,各个相关功能相互不影响,又相互关联,维护性更高。

posted on 2018-12-11 09:49 蔚蓝的蓝 阅读(...) 评论(...) 编辑 收藏