DRF知识点总结(1)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: DRF知识点总结(1)

django restframework(简称drf)本质上其实就是一个别人编写好的app,里面集成了很多编写restful API的功能功能。

 

其目录中有很多我们以前写django程序见到过的,因为它就是一个别人写好了的app,我们拿来用。

因此 每次新建项目时要记得在settings中注册app。

同时在settings中也要加上如下字典,以后drf相关的全局配置都会在该字典中添加。

REST_FRAMEWORK = {
}

一、基本流程分析*

drf为我们封装了许许多多的功能,我们要了解其大概才能在日后的使用中更加灵活。

当路由匹配到如上user的时候,执行其后面的views.UserView中的as_view方法。我们可以看到在我们自己写的类UserView中并没有as_view方法,但它继承于APIView,我们可以在1中看到,APIView的as_view继承了父类View(也就是CBV模式下最先学习的基类)的全部功能,此时我们获得了原本的view函数。在1的retrun中我们看到了csrf_exempt(view),这一步的过程其实就像我们以前去掉csrf_token认证引入的装饰器一样,用于去除csrf_token认证。我们以后选择用jwt进行认证。


期间2中还执行了dispatch方法,由于我们自己写的UserView中没有重写dispatch,它向上找到APIView的dispatch。我们与View基类的dispatch做对比,看到它除了与基类同样运用反射获取请求方式外,其前后还分别对request和response进行了封装。详细内容后面会详细讲。

drf中重写了 as_view 和dispatch方法,其实就是在原来django的功能基础上添加了一些功能,例如:


as_view,免除了csrf 验证,一般前后端分离不会使用csrf token认证(后期会使用jwt认证)。


dispatch,内部添加了 版本处理、认证、权限、访问频率限制等诸多功能(后期逐一讲解)。

二、请求数据的封装

以前我们通过django开发项目时,视图中的request是 django.core.handlers.wsgi.WSGIRequest 类的对象,其中包含了请求相关的所有数据。

# Django FBV
def index(request):
  request.method
  request.POST
  request.GET
  request.body
# Django CBV
from django.views import View
class UserView(View):
  def get(self,request):
        request.method
        request.POST
        request.GET
        request.body

而在使用drf框架时,视图中的request是rest_framework.request.Request类的对象,其是又对django的request进行了一次封装,包含了除django原request对象以外,还包含其他后期会使用的其他对象。

from rest_framework.views import APIView
from rest_framework.response import Response
class UserView(APIView):
    def get(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})
    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

注:此时你看到的request,不再是曾经django中的request,而是又被封装了一层,内部包含:django的request、认证、解析器等。


在源码中,大概是这样:有一个rest_framework.request.Request类,我们最后获得的request=Request(request, 认证,分页..)

Request源码:

# rest_framework.request.Request 类
class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.
    Kwargs:
        - request(HttpRequest). The original request instance. (django中的request)
        - parsers(list/tuple). The parsers to use for parsing the
          request content.
        - authenticators(list/tuple). The authenticators used to try
          authenticating the request's user.
    """
    def __init__(self, request, parsers=None, authenticators=None,negotiator=None, parser_context=None):
      self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        ...
    @property
    def query_params(self):
        """
        More semantically correct name for request.GET.
        """
        return self._request.GET
    @property
    def data(self):
        if not _hasattr(self, '_full_data'):
            self._load_data_and_files()
        return self._full_data
  def __getattr__(self, attr):
        try:
            return getattr(self._request, attr) # self._request.method
        except AttributeError:
            return self.__getattribute__(attr)

我们从源码中可以看到,想要从请求中获取数据,有两种方法:

第一种:

request._request.method
request._request.GET
request._request.POST
request._request.body

第二种:

# 直接读取新request对象中的值,一般此处会对原始的数据进行一些处理,方便开发者在视图中使用。
request.query_params  # 内部本质上就是 request._request.GET
# 内部读取请求体中的数据,并进行处理,例如:请求者发来JSON格式,他的内部会对json字符串进行反序列化。
request.data 
# 通过 __getattr__ 去访问 request._request 中的值
request.method

request.method这样的可以通过反射获取,因此不需要改变。这里我们用request.query_params 替换了request.GET,request.data替换了request.POST.


值得一提的是,request.data会对收到的json格式自动进行反序列化。此外无论是content-type: url-form-encoded还是content-type: application/json,该方法都可以获取到内容。

三、版本管理

在restful规范中要去,后端的API中需要体现版本。

drf框架中支持5种版本的设置,这里我们只介绍常用的两种。

1. URL的GET参数传递版本

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import QueryParameterVersioning
class UserView(APIView):
    versioning_class = QueryParameterVersioning
    def get(self, request, *args, **kwargs):
        print(request.version)
        return Response({"code": 999, "data": "成功了"})

此时我们访问:127.0.0.1/api/user/  输出结果为None


             访问:127.0.0.1/api/user?version=v1 输出结果为v1


             访问:127.0.0.1/api/user?xx=oo 输出结果为None


即默认情况下通过get请求携带参数只认version这一字符串,等号后面是什么结果输出就是什么。


简化:

settings中配置

REST_FRAMEWORK = {
    "VERSION_PARAM": "v",  # 改名字 
    "DEFAULT_VERSION": "v1",  # 默认版本 不传的话就是v1
    "ALLOWED_VERSIONS": ["v1", "v2", "v3"],  # 不在里面返回404 Invalid version in query parameter.
    # 全局配置不用在视图类中写versioning_class = QueryParameterVersioning了(有优先级)
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.QueryParameterVersioning"
}

此时可以不在视图中声明versioning_class = QueryParameterVersioning了。但由于又是全局配置,版本还好,后面一些权限类的不想要可以在视图类中重新声明versioning_class为空就好了。


按照上面的配置,我们访问127.0.0.1/api/user?v=xx, xx不在允许的版本中,直接返回404不执行函数。访问127.0.0.1/api/user?v=v2,print(request.version)输出v2。

2. URL路径传递

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import URLPathVersioning
class UserView(APIView):
    versioning_class = URLPathVersioning
    def get(self, request, *args, **kwargs):
        print(request.version)
        return Response({"code": 999, "data": "成功了"})

此时路由中要留个位置写版本:

path('api/<str:v>/user/', views.UserView.as_view()),

注:我们上面改过settings里的名字了,从默认的version改为了v,因此这里命名也必须是v。由于该种方法写在了视图类内部,比settings配置的QueryParameterVersioning优先级更高,因此我们可以访问127.0.0.1/api/v1/user/、127.0.0.1/api/v2/user/、127.0.0.1/api/v2/user/,由于该路由参数必须填写,因此不采取什么错的情况下,settings中的默认版本与该方法无关。

REST_FRAMEWORK = {
    "VERSION_PARAM": "v",  # 改名字 
    "ALLOWED_VERSIONS": ["v1", "v2", "v3"],  # 不在里面返回404 Invalid version in query parameter.
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning"
}

一种解决不得不填写的办法:

urlpatterns = [
    # 两个路由映射同一个类 
    path('api/user/', views.UserView.as_view()),
    path('api/<str:v>/user/', views.UserView.as_view()),
]

四、认证

在开发后端的API时,不同的功能会有不同的限制,例如:

  • 无需认证,就可以访问并获取数据。
  • 需认证,用户需先登录,后续发送请求需携带登录时发放的凭证(后期会讲jwt)

建表如下:

class UserInfo(models.Model):
    username = models.CharField(verbose_name="用户名", max_length=32)
    password = models.CharField(verbose_name="密码", max_length=64)
    token = models.CharField(verbose_name="TOKEN", max_length=64, null=True, blank=True)

我们设定:从url中获取token,如果没有token或者token不正确或token对应的用户不存在,令其抛出异常。否则通过认证。

代码如下:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01.models import UserInfo
class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.query_params.get('token')
        if not token:
            raise AuthenticationFailed({"code": 1001, "error": "认证失败"})
        user_obj = UserInfo.objects.filter(token=token).first()
        if not user_obj:
            raise AuthenticationFailed({"code": 1001, "error": "认证失败"})
        return user_obj, token
    def authenticate_header(self, request):
        return 'Bearer realm="API"'
class OrderView(APIView):
    authentication_classes = [MyAuthentication, ]
    def get(self, request, *args, **kwargs):
        return Response({"code": 999, "data": "成功了"})

此时我们不带token访问(后面后会通过postman进行调试)

当我们带上正确token:

在视图类中设置类变量 authentication_classes的值为 认证类 MyAuthentication,表示此视图在执行内部功能之前需要先经过 认证。


认证类的内部就是去执行:authenticate方法,根据返回值来表示认证结果。


抛出异常AuthenticationFailed,表示认证失败。内部还会执行 authenticate_header将返回值设置给响应头 WWW-Authenticate


返回含有两个元素的元组,表示认证成功,并且会将元素的第1个元素赋值给 request.user、第2个值赋值给request.auth 。


第1个值,一般是用户对象。

第2个值,一般是token

返回None,表示继续调用后续的认证类 进行认证(上述案例未涉及)


我们也看到了authentication_classes列表里面可以添加多个认证类,比如


authentication_classes = [MyAuthenticationA, MyAuthenticationB, ]

三种情况

认证类A正常结束,返回一个用户对象和token:return user_obj, token  结果:在 request.user 和 request.auth 赋值,后续代码可以使用,此时认证结束不再执行认证类B。

认证类A抛出异常,此时认证结束不再执行认证类B。

认证类A返回一个None:return None, 表示继续调用后续的认证类。如果所有的认证类`authenticate`都返回了None,则默认 request.user= AnonymousUser() (匿名用户对象)  和 request.auth=None,也可以通过修改配置文件来修改默认值。

REST_FRAMEWORK = {
      "UNAUTHENTICATED_USER": lambda: None,
      "UNAUTHENTICATED_TOKEN": lambda: None,
}

”返回None“的应用场景:


当某个API,已认证 和 未认证 的用户都可以方法时,比如:


已认证用户,访问API返回该用户的视频播放记录列表。


未认证用户,访问API返回最新的的视频列表。


注意:不同于之前的案例,之前案例是:必须认证成功后才能访问,而此案例则是已认证和未认证均可访问。

class OrderView(APIView):
    authentication_classes = [TokenAuthentication, ]
    def get(self, request, *args, **kwargs):
        if not request.user: # 这样写前提是settings里修改了名字 匿名改为None
            return Response({"code": "999", "data": "未认证看到的"})
        return Response({"code": "999", "data": "认证了看到的"})

关于多个认证类

一般情况下,编写一个认证类足矣。


当项目中可能存在多种认证方式时,就可以写多个认证类。例如,项目认证支持:


在请求中传递token进行验证。


请求携带cookie进行验证。


请求携带jwt进行验证(后期讲)。


请求携带的加密的数据,需用特定算法解密(一般为app开发的接口都是有加密算法)

全局配置:

REST_FRAMEWORK = {
    "UNAUTHENTICATED_USER": lambda: None,
    "UNAUTHENTICATED_TOKEN": lambda: None,
    "DEFAULT_AUTHENTICATION_CLASSES":["xxxx.xxxx.类名","xxxx.xxxx.类名",]
}

对于一些不需要认证的url:,视图类中声明:authentication_classes = []

五、权限

认证:根据用户携带的 token/其他 获取当前用户信息。

权限:读取认证中获取的用户信息,判断当前用户是否有权限访问,例如:普通用户、管理员、超级用户,不同用户具有不同的权限。

创建表结构如下:

class UserInfo(models.Model):
    role_choices = ((1, "普通用户"), (2, "管理员"), (3, "超级管理员"),)
    role = models.IntegerField(verbose_name="角色", choices=role_choices, default=1)
    username = models.CharField(verbose_name="用户名", max_length=32)
    password = models.CharField(verbose_name="密码", max_length=64)
    token = models.CharField(verbose_name="TOKEN", max_length=64, null=True, blank=True)

则:

class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.query_params.get('token')
        if not token:
            raise AuthenticationFailed({"code": 1001, "error": "认证失败"})
        user_obj = UserInfo.objects.filter(token=token).first()
        if not user_obj:
            raise AuthenticationFailed({"code": 1001, "error": "认证失败"})
        return user_obj, token
    def authenticate_header(self, request):
        return 'Bearer realm="API"'
class PermissionA(BasePermission):
    message = {"code": 1003, "data": "无权访问"}  # 无权访问的话返回的就是它了
    def has_permission(self, request, view):
        exists = request.user.role
        if exists == 2:
            return True
        return False
    def has_object_permission(self, request, view, obj):
        pass
class OrderView(APIView):
    authentication_classes = [MyAuthentication, ]
    permission_classes = [PermissionA, ]
    def get(self, request, *args, **kwargs):
        return Response({"code": 999, "data": "成功了"})

这里我们简写,执行权限前必先执行了认证。我们这里让认证最后通过时必须是一个完整的用户对象,这样我们在Permission中写的代码request.user.role就不会报错(匿名用户或者None是.不出来role的)。


我们访问127.0.0.1:8000/api/order?token=df6e0d2f-4686-4544-b85b-45bde26f0c8d

修改身份再次访问:

关于多个权限类

当开发过程中需要用户同时具备多个权限(缺一不可 错一不可)时,可以用多个权限类来实现。


权限组件内部处理机制:按照列表的顺序逐一执行 has_permission 方法,如果返回True,则继续执行后续的权限类;如果返回None或False,则抛出权限异常并停止后续权限类的执行。


关于 has_object_permission【欠】


当我们使用drf来编写 视图类时,如果是继承 APIView,则 has_object_permission不会被执行(没用)但是,当我们后期学习了 视图类的各种骚操作之后,发现视图也可以继承 GenericAPIView,此时 有可能 会执行 has_object_permission 用于判断是否有权限访问某个特定ID的对象(学完视图后,再细讲)全局配置

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES":["xxxx.xxxx.xx.类名","xxxx.xxxx.xx.类名",]
}

六、限流

限流,限制用户访问频率,例如:用户1分钟最多访问100次 或者 短信验证码一天每天可以发送50次, 防止盗刷。

  • 对于匿名用户,使用用户IP作为唯一标识。
  • 对于登录用户,使用用户ID或名称作为唯一标识。

pip3 install django-redis

# settings.py
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": "",
        }
    }
}

代码如下:

import uuid
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework import exceptions
from app01.models import UserInfo
from rest_framework.permissions import BasePermission
from rest_framework import status
from rest_framework.throttling import SimpleRateThrottle
from django.core.cache import cache as default_cache
class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.query_params.get('token')
        if not token:
            raise AuthenticationFailed({"code": 1001, "error": "认证失败"})
        user_obj = UserInfo.objects.filter(token=token).first()
        if not user_obj:
            raise AuthenticationFailed({"code": 1001, "error": "认证失败"})
        return user_obj, token
    def authenticate_header(self, request):
        return 'Bearer realm="API"'
class PermissionA(BasePermission):
    message = {"code": 1003, "data": "无权访问"}  # 无权访问的话返回的就是它了
    def has_permission(self, request, view):
        exists = request.user.role
        if exists == 2:
            return True
        return False
    def has_object_permission(self, request, view, obj):
        pass
class ThrottledException(exceptions.APIException):
    status_code = status.HTTP_429_TOO_MANY_REQUESTS
    default_code = 'throttled'
class MyRateThrottle(SimpleRateThrottle):
    cache = default_cache  # 访问记录存放在django的缓存中(需设置缓存)
    scope = "user"  # 构造缓存中的key
    cache_format = 'throttle_%(scope)s_%(ident)s'
    # 设置访问频率,例如:1分钟允许访问10次
    # 其他:'s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day'
    THROTTLE_RATES = {"user": "10/m"}
    def get_cache_key(self, request, view):
        if request.user:
            ident = request.user.pk  # 用户ID
        else:
            ident = self.get_ident(request)  # 获取请求用户IP(去request中找请求头)
        # 登录throttle_user_2 # 未登录throttle_user_11.11.11.11ser_2
        return self.cache_format % {'scope': self.scope, 'ident': ident}
    def throttle_failure(self):
        wait = self.wait()
        detail = {
            "code": 1005,
            "data": "访问频率限制",
            'detail': "需等待{}s才能访问".format(int(wait))
        }
        raise ThrottledException(detail)
class OrderView(APIView):
    authentication_classes = [MyAuthentication, ]
    permission_classes = [PermissionA, ]
    throttle_classes = [MyRateThrottle, ]
    def get(self, request, *args, **kwargs):
        return Response({"code": 999, "data": "成功了"})

多个限流类


本质,每个限流的类中都有一个 allow_request 方法,此方法内部可以有三种情况:


返回True,表示当前限流类允许访问,继续执行后续的限流类。


返回False,表示当前限流类不允许访问,继续执行后续的限流类。所有的限流类执行完毕后,读取所有不允许的限流,并计算还需等待的时间。


抛出异常,表示当前限流类不允许访问,后续限流类不再执行。

全局配置

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES":["xxx.xxx.xx.限流类", ],
    "DEFAULT_THROTTLE_RATES": {
        "user": "10/m",
        "xx":"100/h"
    }
}

前六章小结

请求的封装


版本的处理


过程:选择版本处理类,获取用户传入的版本信息


结果:在 request.version = 版本、request.versioning_scheme=版本处理类的对象


认证组件,在视图执行之前判断用户是否认证成功。


过程:执行所有的认证类中的 authenticate 方法


返回None,继续执行后续的认证类(都未认证成功,request.user 和 auth有默认值,也可以全局配置)


返回2个元素的元组,中断


抛出 AuthenticationFailed,中断


结果:在 request.user 和 request.auth 赋值(后续代码可以使用)


权限


过程:执行所有的权限类的has_permission方法,只有所有都返回True时,才表示具有权限


结果:有权限则可以执行后续的视图,无权限则直接返回 自定义的错误信息


限流类

       本质,每个限流的类中都有一个 allow_request 方法,此方法内部可以有三种情况:


返回True,表示当前限流类允许访问,继续执行后续的限流类。


返回False,表示当前限流类不允许访问,继续执行后续的限流类。所有的限流类执行完毕后,读取所有不允许的限流,并计算还需等待的时间。


抛出异常,表示当前限流类不允许访问,后续限流类不再执行。

REST_FRAMEWORK = {
    "VERSION_PARAM": "v", 
    "DEFAULT_VERSION": "v1", 
    "ALLOWED_VERSIONS": ["v1", "v2", "v3"], 
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.QueryParameterVersioning",
    # 认证
    "UNAUTHENTICATED_USER": lambda: None,
    "UNAUTHENTICATED_TOKEN": lambda: None,
    "DEFAULT_AUTHENTICATION_CLASSES": ["app01.auth.TokenAuthentication", ],
    # 权限
    "DEFAULT_PERMISSION_CLASSES": ["app01.permission.PermissionA", ],
    # 限流 一般不会统一加 一半只在一些特殊的url加
    "DEFAULT_THROTTLE_CLASSES": ["app01.limits.MyRateThrottle", ],
    "DEFAULT_THROTTLE_RATES": { 
        "user": "5/m", 
        "xx": "100/h"
    },
}
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": "",
        }
    }
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
安全 Java 数据库
SpingSecurity框架重要知识点(理解)
SpingSecurity框架重要知识点(理解)
38 1
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十二天-增删改查封装使用1
前端学习笔记202305学习笔记第二十二天-增删改查封装使用1
38 0
|
8月前
|
JSON 前端开发 API
DRF框架学习(一)
DRF框架学习(一)
|
8月前
|
JSON 前端开发 数据安全/隐私保护
DRF框架学习(四)
DRF框架学习(四)
|
8月前
|
JSON API 数据库
DRF框架学习(二)
DRF框架学习(二)
|
8月前
|
JSON 前端开发 数据库
DRF框架学习(三)
DRF框架学习(三)
|
8月前
|
JSON 前端开发 Android开发
DRF框架使用时的一些注意点
DRF框架使用时的一些注意点
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十二天-增删改查封装使用
前端学习笔记202305学习笔记第二十二天-增删改查封装使用1
39 0
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十二天-增删改查封装使用4
前端学习笔记202305学习笔记第二十二天-增删改查封装使用4
27 0
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十二天-增删改查封装使用2
前端学习笔记202305学习笔记第二十二天-增删改查封装使用2
27 0