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的值,来源于app01\utils\auth.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_drf\app01\utils下面创建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_drf\app01\utils\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

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次

 

再次测试,效果如下:

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

 

posted @ 2018-08-03 15:41  肖祥  阅读(1064)  评论(0编辑  收藏  举报