• python 全栈开发,Day97(Token 认证的来龙去脉,DRF认证,DRF权限,DRF节流)


    昨日内容回顾

    1. 五个葫芦娃和三行代码
    APIView(views.View)
        1. 封装了Django的request
            - request.query_params  --> 取URL中的参数
            - request.data          --> 取POST和PUT请求中的数据
            
        2. 重写了View中的dispatch方法
            dispatch方法
    通用类(generics)
        GenericAPIView
            - queryset
            - serializer_class
    
    混合类(mixins)
        - ListModelMixin       --> list     
        - CreateModelMixin     --> create
        - RetrieveModelMixin   --> retrieve
        - DestroyModelMixin    --> destroy
        - UpdateModelMixin     --> update
    
        CommentView(GenericAPIView, ListModelMixin, CreateModelMixin):
            def get():
                return self.list()
                
            def post():
                return self.create()
            
    偶数娃:
        CommentView(ListCreateAPIView):
            queryset = ...
            serializer_class = ...
    
    奇数娃
        CommentDetail(RetrieveUpdateDestroyAPIView):
            queryset = ...
            serializer_class = ...
    
    套娃:
        Comment(ModelViewSet):
            queryset = ...
            serializer_class = ...
    View Code

    APIView和ModelViewSet,该如何取舍。看需求。如果用ModelViewSet,只能按照它要求的格式来走
    如果想加入一点个性化的数据,比如{"code":0,"msg":None}还是得需要使用APIView

    一、Token 认证的来龙去脉

    摘要

    Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位

    为什么要用 Token?

    而要回答这个问题很简单——因为它能解决问题!

    可以解决哪些问题呢?

    1. Token 完全由应用管理,所以它可以避开同源策略

    2. Token 可以避免 CSRF 攻击

    3. Token 可以是无状态的,可以在多个服务间共享

    Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

    时序图表示

    使用 Token 的时序图如下:

    1)登录

    2)业务请求

    关于token的详细信息,请参考链接:

    https://blog.csdn.net/maxushan001/article/details/79222271

    二、DRF 认证

    前提

    还是依然使用昨天的项目about_drf3

    定义一个用户表和一个保存用户Token的表,models.py完整代码下:

    from django.db import models
    
    
    # Create your models here.
    
    
    # 文章表
    class Article(models.Model):
        title = models.CharField(max_length=32, unique=True, error_messages={"unique": "文章标题不能重复"})
        # 文章发布时间
        # auto_now每次更新的时候会把当前时间保存
        create_time = models.DateField(auto_now_add=True)
        # auto_now_add 第一次创建的时候把当前时间保存
        update_time = models.DateField(auto_now=True)
        # 文章的类型
        type = models.SmallIntegerField(
            choices=((1, "原创"), (2, "转载")),
            default=1
        )
        # 来源
        school = models.ForeignKey(to='School', on_delete=models.CASCADE)
        # 标签
        tag = models.ManyToManyField(to='Tag')
    
    
    # 文章来源表
    class School(models.Model):
        name = models.CharField(max_length=16)
    
    
    # 文章标签表
    class Tag(models.Model):
        name = models.CharField(max_length=16)
    
    
    # 评论表
    class Comment(models.Model):
        content = models.CharField(max_length=128)
        article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
        user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
    
    
    # 用户信息表
    class UserInfo(models.Model):
        username = models.CharField(max_length=16, unique=True)
        password = models.CharField(max_length=32)
    
        type = models.SmallIntegerField(
            choices=((1, '普通用户'), (2, 'VIP用户')),
            default=1
        )
    
    
    # token
    class Token(models.Model):
        token = models.CharField(max_length=128)
        user = models.OneToOneField(to='UserInfo',on_delete=models.CASCADE)
    View Code

    token单独分一个表,是因为它是在原有用户表的功能扩展。不能对一个表无限的增加字段,否则会导致表原来越臃肿

    在前后端分离的架构中,前端使用ajax请求发送给后端,它不能使用cookie/session。那么后端怎么知道这个用户是否登录了,是否是VIP用户呢?使用token就可以解决这个问题!

    使用2个命令生成表。

    makemigrations 将models.py的变更做记录
    migrate 将变更记录转换为sql语句,并执行

    python manage.py makemigrations
    python manage.py migrate

    增加2条记录,使用navicast软件打开sqlite数据库,执行以下sql

    INSERT INTO app01_userinfo ("id", "username", "password", "type") VALUES (1, 'zhang', 123, 1);
    INSERT INTO app01_userinfo ("id", "username", "password", "type") VALUES (2, 'wang', 123, 2);

    app01_token表用来存放token的,它永久的身份令牌。在服务器自动生成的!

    视图

    修改views.py,完整代码如下:

    from django.shortcuts import render, HttpResponse
    from app01 import models
    from app01 import app01_serializers  # 导入自定义的序列化
    from rest_framework.viewsets import ModelViewSet
    from rest_framework.views import APIView
    from rest_framework.response import Response
    # Create your views here.
    
    # 生成Token的函数
    def get_token_code(username):
        """
        根据用户名和时间戳生成用户登陆成功的随机字符串
        :param username: 字符串格式的用户名
        :return: 字符串格式的Token
        """
        import time
        import hashlib
        timestamp = str(time.time())  # 当前时间戳
        m = hashlib.md5(bytes(username, encoding='utf8'))
        m.update(bytes(timestamp, encoding='utf8'))  # update必须接收一个bytes
        return m.hexdigest()
    
    
    # 登陆视图
    class LoginView(APIView):
        """
        登陆检测视图
        1. 接收用户发过来(POST)的用户名和密码数据
        2. 校验用户名密码是否正确
            - 成功就返回登陆成功(发Token)
            - 失败就返回错误提示
        """
    
        def post(self, request):  # POST请求
            res = {"code": 0}
            # 从post里面取数据
            username = request.data.get("username")
            password = request.data.get("password")
            # 去数据库查询
            user_obj = models.UserInfo.objects.filter(
                username=username,
                password=password,
            ).first()
            if user_obj:
                # 登陆成功
                # 生成Token
                token = get_token_code(username)
                # 将token保存
                # 用user=user_obj这个条件去Token表里查询
                # 如果有记录就更新defaults里传的参数, 没有记录就用defaults里传的参数创建一条数据
                models.Token.objects.update_or_create(defaults={"token": token}, user=user_obj)
                # 将token返回给用户
                res["token"] = token
            else:
                # 登录失败
                res["code"] = 1
                res["error"] = '用户名或密码错误'
            return Response(res)
    
    
    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
    View Code

    路由

    修改app01_urls.py,删除多余的代码

    from django.conf.urls import url
    from app01 import views
    
    urlpatterns = [
        url(r'login/$', views.LoginView.as_view()),
    ]
    
    from rest_framework.routers import DefaultRouter
    
    router = DefaultRouter()
    # 注册路由,表示路径comment对应视图函数CommentViewSet
    router.register(r'comment', views.CommentViewSet)
    urlpatterns += router.urls
    View Code

    使用postman发送post登录

    查看返回结果,code为0表示登录成功,并返回一个token

    查看表app01_token,就会多一条记录

    postman访问评论,它是可以任意访问的

     

    DRF认证源码流程

    DRF认证源码流程,请参考链接:

    https://www.cnblogs.com/haiyan123/p/8419872.html  (后半段没有写)

    https://www.cnblogs.com/derek1184405959/p/8712206.html  (后半段写了)

    执行流程图解

    图片来源: https://www.cnblogs.com/renpingsheng/p/7897192.html

    定义一个认证类

    现在有一个需求,只有登录的用户,才能对评论做修改

    在app01(应用名)目录下创建目录utils,在此目录下创建auth.py

    """
    自定义的认证类都放在这里
    """
    from rest_framework.authentication import BaseAuthentication
    from app01 import models
    from rest_framework.exceptions import AuthenticationFailed
    
    
    class MyAuth(BaseAuthentication):
    
        def authenticate(self, request):  # 必须要实现此方法
            if request.method in ['POST', 'PUT', 'DELETE']:
                token = request.data.get("token")
                # 去数据库查询有没有这个token
                token_obj = models.Token.objects.filter(token=token).first()
                if token_obj:
                    # token_obj有2个属性,详见models.py中的Token。
                    # return后面的代码,相当于分别赋值。例如a=1,b=2等同于a,b=1,2
                    # return多个值,返回一个元组
                    #在rest framework内部会将这两个字段赋值给request,以供后续操作使用
                    return token_obj.user, token  # self.user, self.token = token_obj.user, token
                else:
                    raise AuthenticationFailed('无效的token')
            else:
                return None, None
    View Code

    视图级别认证

    修改views.py,完整代码如下:

    from django.shortcuts import render, HttpResponse
    from app01 import models
    from app01 import app01_serializers  # 导入自定义的序列化
    from rest_framework.viewsets import ModelViewSet
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from app01.utils.auth import MyAuth  # app01.utils.auth表示app01目录下的utils下的auth.py
    
    # Create your views here.
    
    # 生成Token的函数
    def get_token_code(username):
        """
        根据用户名和时间戳生成用户登陆成功的随机字符串
        :param username: 字符串格式的用户名
        :return: 字符串格式的Token
        """
        import time
        import hashlib
        timestamp = str(time.time())  # 当前时间戳
        m = hashlib.md5(bytes(username, encoding='utf8'))
        m.update(bytes(timestamp, encoding='utf8'))  # update必须接收一个bytes
        return m.hexdigest()
    
    
    # 登陆视图
    class LoginView(APIView):
        """
        登陆检测视图
        1. 接收用户发过来(POST)的用户名和密码数据
        2. 校验用户名密码是否正确
            - 成功就返回登陆成功(发Token)
            - 失败就返回错误提示
        """
    
        def post(self, request):  # POST请求
            res = {"code": 0}
            # 从post里面取数据
            username = request.data.get("username")
            password = request.data.get("password")
            # 去数据库查询
            user_obj = models.UserInfo.objects.filter(
                username=username,
                password=password,
            ).first()
            if user_obj:
                # 登陆成功
                # 生成Token
                token = get_token_code(username)
                # 将token保存
                # 用user=user_obj这个条件去Token表里查询
                # 如果有记录就更新defaults里传的参数, 没有记录就用defaults里传的参数创建一条数据
                models.Token.objects.update_or_create(defaults={"token": token}, user=user_obj)
                # 将token返回给用户
                res["token"] = token
            else:
                # 登录失败
                res["code"] = 1
                res["error"] = '用户名或密码错误'
            return Response(res)
    
    
    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
        authentication_classes = [MyAuth, ]  # 局部使用认证方法MyAuth
    View Code

     发送一个空的post请求,返回结果如下:

    发送一个错误的token

    返回结果:

    注意:这个信息是由raise AuthenticationFailed('无效的token')触发的。

    如果想在MyAuth类-->authenticate方法-->代码else中触发别的信息,也同样需要定义raise

    发送正确的token

    返回结果,出现以下信息,说明已经通过了认证

    全局级别认证

    要想让每一个视图都要认证,可以在settings.py中配置

    REST_FRAMEWORK = {
        # 表示app01-->utils下的auth.py里面的MyAuth类
        "DEFAULT_AUTHENTICATION_CLASSES": ["app01.utils.auth.MyAuth", ]
    }

    修改views.py,注释掉CommentViewSet中的authentication_classes

    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
        # authentication_classes = [MyAuth, ]  # 局部使用认证方法MyAuth
    View Code

    再次测试上面的3种请求方式,效果同上!

    三、DRF权限

    权限源码流程

    请参考链接:

    http://www.cnblogs.com/derek1184405959/p/8722212.html

    举例1

    只有VIP用户才能看的内容。

    自定义一个权限类

    has_permission

    注意:当返回一个对象时,才会触发

    什么对象呢?json对象!why?

    在CommentViewSet视图中,它会返回一个json数据

    http://127.0.0.1:8000/api/comment/  它会返回一个json列表

    http://127.0.0.1:8000/api/comment/1 它会返回一个json对象。

    当使用了权限类后,类中有has_permission,就有触发

    举例:

    在目录app01-->utils下面新建文件permission.py

    """
    自定义的权限类
    """
    from rest_framework.permissions import BasePermission
    
    
    class MyPermission(BasePermission):
        def has_permission(self, request, view):
            """
            判断该用户有没有权限
            """
            # 判断用户是不是VIP用户
            # 如果是VIP用户就返回True
            # 如果是普通用户就返回False
            print('我要进行自定义的权限判断啦....')
            print(request)
            print(request.user)
            return True
    View Code

    视图级别配置

    修改views.py,指定permission_classes

    from django.shortcuts import render, HttpResponse
    from app01 import models
    from app01 import app01_serializers  # 导入自定义的序列化
    from rest_framework.viewsets import ModelViewSet
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from app01.utils.auth import MyAuth  # app01.utils.auth表示app01目录下的utils下的auth.py
    from app01.utils.permission import MyPermission
    
    # Create your views here.
    
    # 生成Token的函数
    def get_token_code(username):
        """
        根据用户名和时间戳生成用户登陆成功的随机字符串
        :param username: 字符串格式的用户名
        :return: 字符串格式的Token
        """
        import time
        import hashlib
        timestamp = str(time.time())  # 当前时间戳
        m = hashlib.md5(bytes(username, encoding='utf8'))
        m.update(bytes(timestamp, encoding='utf8'))  # update必须接收一个bytes
        return m.hexdigest()
    
    
    # 登陆视图
    class LoginView(APIView):
        """
        登陆检测视图
        1. 接收用户发过来(POST)的用户名和密码数据
        2. 校验用户名密码是否正确
            - 成功就返回登陆成功(发Token)
            - 失败就返回错误提示
        """
    
        def post(self, request):  # POST请求
            res = {"code": 0}
            # 从post里面取数据
            username = request.data.get("username")
            password = request.data.get("password")
            # 去数据库查询
            user_obj = models.UserInfo.objects.filter(
                username=username,
                password=password,
            ).first()
            if user_obj:
                # 登陆成功
                # 生成Token
                token = get_token_code(username)
                # 将token保存
                # 用user=user_obj这个条件去Token表里查询
                # 如果有记录就更新defaults里传的参数, 没有记录就用defaults里传的参数创建一条数据
                models.Token.objects.update_or_create(defaults={"token": token}, user=user_obj)
                # 将token返回给用户
                res["token"] = token
            else:
                # 登录失败
                res["code"] = 1
                res["error"] = '用户名或密码错误'
            return Response(res)
    
    
    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
        # authentication_classes = [MyAuth, ]  # 局部使用认证方法MyAuth
        permission_classes = [MyPermission, ]  # 局部使用权限方法
    View Code

    发送get请求

    查看Pycharm控制台输出:

    我要进行自定义的权限判断啦....
    <rest_framework.request.Request object at 0x000002576A780FD0>
    None

    发现用户为None,它没有触发has_permission

    访问单个评论,返回单个json对象

    查看Pycharm控制台输出:

    我要进行自定义的权限判断啦....
    <rest_framework.request.Request object at 0x000002576A780FD0>
    这是在自定义权限类中的has_object_permission
    1

    它触发了has_permission,并输出了一段话

    注意:json对象中,它增加一个属性user,值为null。为什么会增加呢?

    因为在源码中,Request有个user方法,加 @property。它对返回结果做了在再次封装!

    详情,请参考上面的权限源码流程

    普通用户

    发送post请求,写一个正确的token,用zhang用户的token

    查看Pycharm控制台输出:

    我要进行自定义的权限判断啦....
    <rest_framework.request.Request object at 0x000002576A9852B0>
    UserInfo object

    此时得到了一个用户对象

    修改permission.py,获取用户名以及用户类型

    """
    自定义的权限类
    """
    from rest_framework.permissions import BasePermission
    
    
    class MyPermission(BasePermission):
        def has_permission(self, request, view):
            """
            判断该用户有没有权限
            """
            # 判断用户是不是VIP用户
            # 如果是VIP用户就返回True
            # 如果是普通用户就返回False
            print('我要进行自定义的权限判断啦....')
            print(request)
            print(request.user.username)
            print(request.user.type)
            return True
    View Code

    再次发送同样的post请求,再次查看Pycharm控制台输出

    <rest_framework.request.Request object at 0x000001D893AC4048>
    zhang
    1

    居然得到了zhang和1。为什么呢?为什么request.user.username就能得到用户名呢?

    我来大概解释一下,先打开这篇文章:

    https://www.cnblogs.com/derek1184405959/p/8712206.html

    我引用里面几句话

    Request有个user方法,加 @property 表示调用user方法的时候不需要加括号“user()”,可以直接调用:request.user

    在rest framework内部会将这两个字段赋值给request,以供后续操作使用

    return (token_obj.user,token_obj)

    上面的return的值,来源于app01utilsauth.py里面的MyAuth类中的return token_obj.user, token

    简单来说,通过认证之后,它会request进行再次封装,所以调用request.user时,得到了一个对象

    这个对象就是执行models.Token.objects.filter(token=token).first()的结果

    如果ORM没有查询出结果,它就一个匿名用户!

    修改permission.py,如果是VIP返回True,否则返回False

    """
    自定义的权限类
    """
    from rest_framework.permissions import BasePermission
    
    
    class MyPermission(BasePermission):
        def has_permission(self, request, view):
            """
            判断该用户有没有权限
            """
            # 判断用户是不是VIP用户
            # 如果是VIP用户就返回True
            # 如果是普通用户就返回False
            print('我要进行自定义的权限判断啦....')
            # print(request)
            print(request.user.username)
            print(request.user.type)
            if request.user.type == 2:  # 是VIP用户
                return True
            else:
                return False
    View Code

    修改settings.py,关闭全局级别认证

    REST_FRAMEWORK = {
        # 表示app01-->utils下的auth.py里面的MyAuth类
        # "DEFAULT_AUTHENTICATION_CLASSES": ["app01.utils.auth.MyAuth", ]
    }
    View Code

    使用VIP用户wang登录

    查看返回结果,它返回wang的token

    查看表app01_token,它现在有2个记录了

    复制zhang的token,发送一条评论

    查看返回结果,提示您没有执行此操作的权限

    英文看不懂,没关系,定义成中文就行了

    修改permission.py,定义message

    """
    自定义的权限类
    """
    from rest_framework.permissions import BasePermission
    
    
    class MyPermission(BasePermission):
        message = '您没有执行此操作的权限!'
        def has_permission(self, request, view):
            """
            判断该用户有没有权限
            """
            # 判断用户是不是VIP用户
            # 如果是VIP用户就返回True
            # 如果是普通用户就返回False
            print('我要进行自定义的权限判断啦....')
            # print(request)
            print(request.user.username)
            print(request.user.type)
            if request.user.type == 2:  # 是VIP用户
                return True
            else:
                return False
    View Code

    再次发送,返回结果如下:

    VIP用户

    将token改成VIP用户测试

    查看返回结果

     

    修改permission.py,定义发送类型

    """
    自定义的权限类
    """
    from rest_framework.permissions import BasePermission
    
    
    class MyPermission(BasePermission):
        message = '您没有执行此操作的权限!'
        def has_permission(self, request, view):
            """
            判断该用户有没有权限
            """
            # 判断用户是不是VIP用户
            # 如果是VIP用户就返回True
            # 如果是普通用户就返回False
            print('我要进行自定义的权限判断啦....')
    
            if request.method in ['POST', 'PUT', 'DELETE']:
                print(request.user.username)
                print(request.user.type)
                if request.user.type == 2:  # 是VIP用户
                    return True
                else:
                    return False
            else:
                return True
    View Code

    发送正确的值

    查看返回结果

    查看表app01_comment记录

     

    全局级别设置

    修改settings.py,增加一行

    REST_FRAMEWORK = {
        # 表示app01-->utils下的auth.py里面的MyAuth类
        # "DEFAULT_AUTHENTICATION_CLASSES": ["app01.utils.auth.MyAuth", ]
        "DEFAULT_PERMISSION_CLASSES": ["app01.utils.permission.MyPermission", ]
    }

    修改views.py,注释局部的

    from django.shortcuts import render, HttpResponse
    from app01 import models
    from app01 import app01_serializers  # 导入自定义的序列化
    from rest_framework.viewsets import ModelViewSet
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from app01.utils.auth import MyAuth  # app01.utils.auth表示app01目录下的utils下的auth.py
    from app01.utils.permission import MyPermission
    
    # Create your views here.
    
    # 生成Token的函数
    def get_token_code(username):
        """
        根据用户名和时间戳生成用户登陆成功的随机字符串
        :param username: 字符串格式的用户名
        :return: 字符串格式的Token
        """
        import time
        import hashlib
        timestamp = str(time.time())  # 当前时间戳
        m = hashlib.md5(bytes(username, encoding='utf8'))
        m.update(bytes(timestamp, encoding='utf8'))  # update必须接收一个bytes
        return m.hexdigest()
    
    
    # 登陆视图
    class LoginView(APIView):
        """
        登陆检测视图
        1. 接收用户发过来(POST)的用户名和密码数据
        2. 校验用户名密码是否正确
            - 成功就返回登陆成功(发Token)
            - 失败就返回错误提示
        """
    
        def post(self, request):  # POST请求
            res = {"code": 0}
            # 从post里面取数据
            username = request.data.get("username")
            password = request.data.get("password")
            # 去数据库查询
            user_obj = models.UserInfo.objects.filter(
                username=username,
                password=password,
            ).first()
            if user_obj:
                # 登陆成功
                # 生成Token
                token = get_token_code(username)
                # 将token保存
                # 用user=user_obj这个条件去Token表里查询
                # 如果有记录就更新defaults里传的参数, 没有记录就用defaults里传的参数创建一条数据
                models.Token.objects.update_or_create(defaults={"token": token}, user=user_obj)
                # 将token返回给用户
                res["token"] = token
            else:
                # 登录失败
                res["code"] = 1
                res["error"] = '用户名或密码错误'
            return Response(res)
    
    
    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
        authentication_classes = [MyAuth, ]  # 局部使用认证方法MyAuth
        # permission_classes = [MyPermission, ]  # 局部使用权限方法
    View Code

    验证

    使用普通用户测试

     查看返回结果

    举例2

    只要评论的作者是自己,就可以删除,否则不行!

    表结构

    修改models.py,在评论表中,增加一个字段user

    class Comment(models.Model):
        content = models.CharField(max_length=128)
        article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
        user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
    View Code

    使用2个命令生成表。

    python manage.py makemigrations
    python manage.py migrate

    修改表,增加2个user_id

    has_object_permission

    修改permission.py,增加has_object_permission

    它比上面的has_permission方法多了一个obj
    它是操作的对象,比如评论对象

    """
    自定义的权限类
    """
    from rest_framework.permissions import BasePermission
    
    
    class MyPermission(BasePermission):
        message = '您没有执行此操作的权限!'
        def has_permission(self, request, view):
            """
            判断该用户有没有权限
            """
            # 判断用户是不是VIP用户
            # 如果是VIP用户就返回True
            # 如果是普通用户就返回False
            print('我要进行自定义的权限判断啦....')
            return True
            # if request.method in ['POST', 'PUT', 'DELETE']:
            #     print(request.user.username)
            #     print(request.user.type)
            #     if request.user.type == 2:  # 是VIP用户
            #         return True
            #     else:
            #         return False
            # else:
            #     return True
    
        def has_object_permission(self, request, view, obj):
            """
            判断当前评论用户的作者是不是你当前的用户
            只有评论的作者才能删除自己的评论
            """
            print('这是在自定义权限类中的has_object_permission')
            print(obj.id)
            if request.method in ['PUT', 'DELETE']:
                if obj.user == request.user:
                    # 当前要删除的评论的作者就是当前登陆的用户
                    return True
                else:
                    return False
            else:
                return True
    View Code

     使用普通用户的token发送delete类型的请求

    查看返回结果 

     使用VIP用户的token发送

     查看返回结果,为空,表示删除成功

     

     查看表app01_comment,发现少了一条记录

    四、DRF节流

    节流也称之为限制

    DRF节流源码分析

    请参考链接:

    http://www.cnblogs.com/derek1184405959/p/8722638.html

    自定义限制类

    对IP做限制,60秒只能访问3次

    在about_drfapp01utils下面创建throttle.py

    """
    自定义的访问限制类
    """
    from rest_framework.throttling import BaseThrottle, SimpleRateThrottle
    import time
    
    D = {}  # {'127.0.0.1': [1533302442, 1533302439,...]}
    
    
    class MyThrottle(BaseThrottle):
    
        def allow_request(self, request, view):
            """
            返回True就放行,返回False表示被限制了...
            """
            # 1. 获取当前访问的IP
            ip = request.META.get("REMOTE_ADDR")
            print('这是自定义限制类中的allow_request')
            print(ip)
            # 2. 获取当前的时间
            now = time.time()
            # 判断当前ip是否有访问记录
            if ip not in D:
                D[ip] = []  # 初始化一个空的访问历史列表
            # 高端骚操作
            history = D[ip]
            while history and now - history[-1] > 10:
                history.pop()
            # 判断最近一分钟的访问次数是否超过了阈值(3次)
            if len(history) >= 3:
                return False
            else:
                # 把这一次的访问时间加到访问历史列表的第一位
                D[ip].insert(0, now)
                return True
    View Code

    代码解释:

    request.META.get("REMOTE_ADDR")  获取远程IP

    D  存储的值,类似于

    "192.168.1.2":["17:06:45","12:04:03","12:04:01"]

    最后一个元素,就是最先开始的时间

    for循环列表,不能对列表做更改操作!所以使用while循环

    while history and now - history[-1] > 10:
        history.pop()

    history是历史列表,history[-1] 表示列表最后一个元素

    history and now - history[-1] > 10 表示当历史列表中有元素,并且当前时间戳减去最后一个元素的时间戳大于10的时候,执行history.pop(),表示删除最后一个元素

    当历史列表为空时,或者小于差值小于10的时候,结束循环。

    视图使用

    修改views.py

    from django.shortcuts import render, HttpResponse
    from app01 import models
    from app01 import app01_serializers  # 导入自定义的序列化
    from rest_framework.viewsets import ModelViewSet
    from rest_framework.views import APIView
    from rest_framework.response import Response
    from app01.utils.auth import MyAuth  # app01.utils.auth表示app01目录下的utils下的auth.py
    from app01.utils.permission import MyPermission
    from app01.utils.throttle import MyThrottle
    
    # Create your views here.
    
    # 生成Token的函数
    def get_token_code(username):
        """
        根据用户名和时间戳生成用户登陆成功的随机字符串
        :param username: 字符串格式的用户名
        :return: 字符串格式的Token
        """
        import time
        import hashlib
        timestamp = str(time.time())  # 当前时间戳
        m = hashlib.md5(bytes(username, encoding='utf8'))
        m.update(bytes(timestamp, encoding='utf8'))  # update必须接收一个bytes
        return m.hexdigest()
    
    
    # 登陆视图
    class LoginView(APIView):
        """
        登陆检测视图
        1. 接收用户发过来(POST)的用户名和密码数据
        2. 校验用户名密码是否正确
            - 成功就返回登陆成功(发Token)
            - 失败就返回错误提示
        """
    
        def post(self, request):  # POST请求
            res = {"code": 0}
            # 从post里面取数据
            username = request.data.get("username")
            password = request.data.get("password")
            # 去数据库查询
            user_obj = models.UserInfo.objects.filter(
                username=username,
                password=password,
            ).first()
            if user_obj:
                # 登陆成功
                # 生成Token
                token = get_token_code(username)
                # 将token保存
                # 用user=user_obj这个条件去Token表里查询
                # 如果有记录就更新defaults里传的参数, 没有记录就用defaults里传的参数创建一条数据
                models.Token.objects.update_or_create(defaults={"token": token}, user=user_obj)
                # 将token返回给用户
                res["token"] = token
            else:
                # 登录失败
                res["code"] = 1
                res["error"] = '用户名或密码错误'
            return Response(res)
    
    
    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
        authentication_classes = [MyAuth, ]  # 局部使用认证方法MyAuth
        # permission_classes = [MyPermission, ]  # 局部使用权限方法
        throttle_classes = [MyThrottle, ]  # 局部使用限制方法
    View Code

    使用postman发送GET请求

    疯狂的点击SEND按钮,多发送几次

    提示请求达到了限制

     等待十几秒,就可以访问了

    全局使用

    修改settings.py

    REST_FRAMEWORK = {
        # 表示app01-->utils下的auth.py里面的MyAuth类
        # "DEFAULT_AUTHENTICATION_CLASSES": ["app01.utils.auth.MyAuth", ],
        #"DEFAULT_PERMISSION_CLASSES": ["app01.utils.permission.MyPermission", ],
        "DEFAULT_THROTTLE_CLASSES": ["app01.utils.throttle.MyThrottle", ]
    }
    View Code

    修改views.py,注释掉代码

    class CommentViewSet(ModelViewSet):
        queryset = models.Comment.objects.all()
        serializer_class = app01_serializers.CommentSerializer
        authentication_classes = [MyAuth, ]  # 局部使用认证方法MyAuth
        permission_classes = [MyPermission, ]  # 局部使用权限方法
        # throttle_classes = [MyThrottle, ]  # 局部使用限制方法
    View Code

    再次测试,效果同上!

    使用内置限制类

    修改about_drfapp01utils hrottle.py

    """
    自定义的访问限制类
    """
    from rest_framework.throttling import BaseThrottle, SimpleRateThrottle
    # import time
    #
    # D = {}  # {'127.0.0.1': [1533302442, 1533302439,...]}
    #
    #
    # class MyThrottle(BaseThrottle):
    #
    #     def allow_request(self, request, view):
    #
    #         """
    #         返回True就放行,返回False表示被限制了...
    #         """
    #         # 1. 获取当前访问的IP
    #         ip = request.META.get("REMOTE_ADDR")
    #         print('这是自定义限制类中的allow_request')
    #         print(ip)
    #         # 2. 获取当前的时间
    #         now = time.time()
    #         # 判断当前ip是否有访问记录
    #         if ip not in D:
    #             D[ip] = []  # 初始化一个空的访问历史列表
    #         # 高端骚操作
    #         history = D[ip]
    #         while history and now - history[-1] > 10:
    #             history.pop()
    #         # 判断最近一分钟的访问次数是否超过了阈值(3次)
    #         if len(history) >= 3:
    #             return False
    #         else:
    #             # 把这一次的访问时间加到访问历史列表的第一位
    #             D[ip].insert(0, now)
    #             return True
    
    class MyThrottle(SimpleRateThrottle):
    
        scope = "rate"  # rate是名字,可以随便定义!
    
        def get_cache_key(self, request, view):
            return self.get_ident(request)
    View Code

    注意:scope是关键字参数

    get_cache_key 的名字不能变动

    self.get_ident(request)  表示远程IP地址

    全局配置

    修改settings.py

    REST_FRAMEWORK = {
        # 表示app01-->utils下的auth.py里面的MyAuth类
        # "DEFAULT_AUTHENTICATION_CLASSES": ["app01.utils.auth.MyAuth", ]
        "DEFAULT_PERMISSION_CLASSES": ["app01.utils.permission.MyPermission", ],
        "DEFAULT_THROTTLE_CLASSES": ["app01.utils.throttle.MyThrottle", ],
        "DEFAULT_THROTTLE_RATES": {
            "rate": "3/m",
        }
    }

    注意:rate对应的是throttle.py里面MyThrottle定义的scope属性的值

    3/m 表示1分钟3次

    再次测试,效果如下:

    它还会返回倒计时的时间!

  • 相关阅读:
    [Windows Server 2012] 安装PHP+MySQL方法
    [Windows Server 2012] 安装IIS8.5及FTP
    [Windows Server 2012] IIS自带FTP配置方法
    护卫神,服务器安全专家!
    ie9 UpdateModel失败
    打包自己的nuget时,设置安装此nuget改写web.config
    Roslyn 1.2.0.0 的改变
    用自定义routes把不同的querystring名对应到action同一个参数上
    JetBrains dotCover 2.0 破解研究(注册机)
    mvc4 中的 AuthorizeAttribute
  • 原文地址:https://www.cnblogs.com/xiao987334176/p/9414486.html
Copyright © 2020-2023  润新知