66. drf之登录_认证_权限_频率组件

1. 登录接口

1.1 登录

新建django项目,app01的models.py

SQLite 的 varchar(max_length) 不强制长度校验,max_length 只是元数据,写入超长字符串直接全部存进去,完全不报错。
SQLite模型写 max_length=32,存 36 位带横杠 uuid 毫无报错。
换成 MySQL,会进行长度限制,直接抛出 Data too long 1406。

# 1.用户表,用来写登录接口
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    age = models.IntegerField()

# 2.用户token表,记录用户的登录情况
class UserToken(models.Model):
    # 字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
    token = models.CharField(max_length=64)
    user = models.OneToOneField(to=User, on_delete=models.CASCADE)

# 3.图书表
class Book(models.Model):
    name = models.CharField(max_length=32)
    publish = models.CharField(max_length=32)
    price = models.IntegerField()

将模型表迁移至数据库:makemigrations  migrate

app01的views.py

"""
登录接口
前端传入的数据类型:{username, password}
请求方式:post
后端视图类继承哪个基类:
    需要进行数据库操作,但是不需要序列化输入或反序列化校验
    继承APIView,还要自动生成路由---ViewSet
"""

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User, UserToken
import uuid

class LoginView(ViewSet):
    @action(methods=['post'], detail=False)  # False为操作所有,不需要pk
    def login(self, request):
        # 1.取出用户名密码
        username1 = request.data.get('username')
        password1 = request.data.get('password')
        # 2.校验,返回结果给前端
        user_exist = User.objects.filter(username=username1, password=password1).first()
        if user_exist:
            # 生成一个随机字符串,字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
            token_str = str(uuid.uuid4())
            # 保存到UserToken表  如果之前没有数据,则是新增,如果之前有数据即之前登录过则是修改
            # 根据当前用户名去UserToken表中查询token。如果查不到,则新增;如果能查到,则修改
            UserToken.objects.update_or_create(user=user_exist, defaults={'token': token_str})
            # 返回给前端登录成功
            return Response({'code': 100, 'msg': '登录成功', 'token': token_str})
        else:
            # 返回给前端用户名或密码错误
            return Response({'code': 101, 'msg': '用户名或密码错误'})

app01的urls.py

from rest_framework.routers import SimpleRouter
from .views import LoginView

router = SimpleRouter()
router.register(r'login11', LoginView, basename='login11')

urlpatterns = []
urlpatterns += router.urls

总路由

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/app01/', include('app01.urls')),
]

当数据库中没有基础数据时,视图类中会校验失败,返回用户名或密码错误,因此需要先在数据库中添加数据

image

登录成功,返回token

image

数据库中会新增对应token

image

再次登录时token会刷新

image

登录失败

image

1.2 图书表book相关接口

序列化组件app01的serializer.py

from .models import Book
from rest_framework import serializers

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

app01的views.py

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User, UserToken, Book
import uuid
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from .serializer import BookSerializer

# book接口查询一条
class BookDetailView(RetrieveAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# book接口新增一条
class BookView(CreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

app01的urls.py

from django.urls import path, include
from rest_framework.routers import SimpleRouter
from .views import LoginView, BookView, BookDetailView

router = SimpleRouter()
router.register(r'login11', LoginView, basename='login11')

urlpatterns = [
    path('book/', BookView.as_view()),
    path('book/<int:pk>/', BookDetailView.as_view()),
]
urlpatterns += router.urls

新增一条

image

image

查询一条

image

2. 认证组件

2.1 需求

如果未登录,可以查询一条,必须登录才能新增

根据APIView源码,在运行视图类函数之前,先运行了认证、权限、限流这三个组件

只需要根据以下步骤,就能完成登录的认证:

  1.写个类,继承BaseAuthentication

  2.在类中重写authenticate

    在函数内,取出前端传入的token,校验是否登录

    如果token正确,是登录用户,向前端返回值,往下运行

    如果token错误或数据库中没有数据,抛异常

  3.使用:在视图类配置或配置文件中配置  类似于之前的请求和响应局部配置与全局配置

2.2 认证组件

认证组件需要在一个单独的文件中,定义为app01的auth.py

from rest_framework.authentication import BaseAuthentication
from .models import UserToken
from rest_framework.exceptions import AuthenticationFailed

class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
        # 1.取出前端传入的token
        token = request.META.get('HTTP_TOKEN')
        # 2.校验token
        user_token = UserToken.objects.filter(token=token).first()
        if user_token:
            # 返回当前登录用户与token
            # 后续在request中通过request.user就会取出返回的第一个参数,即当前登录用户
            return user_token.user, token
        else:
            raise AuthenticationFailed('未登录')

2.3 局部配置认证组件

app01的views.py

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User, UserToken, Book
import uuid
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from .serializer import BookSerializer
from .auth import LoginAuth

# 登录接口
class LoginView(ViewSet):
    @action(methods=['post'], detail=False)  # False为操作所有,不需要pk
    def login(self, request):
        # 1.取出用户名密码
        username1 = request.data.get('username')
        password1 = request.data.get('password')
        # 2.校验,返回结果给前端
        user_exist = User.objects.filter(username=username1, password=password1).first()
        if user_exist:
            # 生成一个随机字符串,字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
            token_str = str(uuid.uuid4())
            # 保存到UserToken表  如果之前没有数据,则是新增,如果之前有数据即之前登录过则是修改
            # 根据当前用户名去UserToken表中查询token。如果查不到,则新增;如果能查到,则修改
            UserToken.objects.update_or_create(user=user_exist, defaults={'token': token_str})
            # 返回给前端登录成功
            return Response({'code': 100, 'msg': '登录成功', 'token': token_str})
        else:
            # 返回给前端用户名或密码错误
            return Response({'code': 101, 'msg': '用户名或密码错误'})

# book接口查询一条
class BookDetailView(RetrieveAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# book接口新增一条
class BookView(CreateAPIView):
    authentication_classes = [LoginAuth]  # 只要加了这个配置,以后这个类的所有函数,都需要在登录后才能运行
    queryset = Book.objects.all()
    serializer_class = BookSerializer

新增一条

image

给请求头加上已经登录用户的token

image

新增成功

image

2.4 全局配置认证组件

[1] 全局使用

rest_framework的源码settings.py中包含配置格式,按照这个格式写到项目的settings.py中

配置文件中全局配置之后对views.py中所有的接口都生效

在配置文件末尾添加

from app01.auth import LoginAuth
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'app01.auth.LoginAuth'
    ],
}

以上在项目的配置文件导入模块会导致项目启动不了

image

去掉导入模块,使用app01.auth.LoginAuth即可

[2] 局部禁用

全局配置认证组件之后,认证组件中的所有接口都需要认证

只需在该视图类中将authentication_classes属性设置为空列表即可实现局部禁用。

class UserView(ViewSet): 
    authentication_classes = []       

以上例子中,全局配置认证之后,注册功能也需要登录后才能注册,显然不合理

2.5 补充

一旦通过认证,在request中就有当前登录用户
def get(self, request):
    print(request.user.name,'访问了接口')

3. 权限组件

3.1 概念

用户登录之后,不同的角色权限不同。  管理员、普通用户

权限设计:
ACL(Access Control List)访问控制列表
RBAC(Role-Based Access Control)基于角色的访问控制
ABAC(Attribute-Based Access Control)基于属性的访问控制

3.2 权限组件使用步骤(固定用法)

[1] 创建权限类

创建一个自定义的权限类,继承BasePermission。

[2] 在类中重写has_permission函数

has_permission函数中完成权限的校验
如果有权限,返回True
如果没有权限,返回False  通过self.message='字符串'设置错误提示信息

如果不使用self.message设置错误提示信息,则使用系统默认的英文提示

[3] 把权限类配置在视图类上,视图类就会受权限类的控制

局部配置:视图类中  permission_classes = [CommonPermission]
全局配置:项目的settings.py中
  'DEFAULT_PERMISSION_CLASSES': [
  'rest_framework.permissions.AllowAny',
  ],
局部禁用:视图类中  permission_classes = [ ]

3.3 代码示例

[1] 需求

用户登录之后,必须是vip才能对图书进行新增

app01的models.py中给用户表加用户角色

class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    age = models.IntegerField()
    role = models.IntegerField(choices=((1, '普通用户'), (2, '纯路人'), (3, 'vip')))

makemigrations迁移至数据库中,必须设置默认值

image

在数据库中再添加一条记录

image

[2] 权限组件

权限组件需要在一个单独的文件中,定义为app01的permission.py
from rest_framework.permissions import BasePermission

class VipPerm(BasePermission):
    def has_permission(self, request, view):
        # 在auth组件中return了当前用户,或通过auth之后在request中就有当前登录用户
        # 校验是否为vip,有权限则返回True
        if request.user.role == 3:
            return True
        else:
            self.message = f'你不是vip,你是{request.user.get_role_display()}'  # 根据键获取对应的值
            return False

[3] 使用权限组件

app01的views.py

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User, UserToken, Book
import uuid
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from .serializer import BookSerializer
from .permission import VipPerm
from .auth import LoginAuth

# 登录接口
class LoginView(ViewSet):
    authentication_classes = []

    @action(methods=['post'], detail=False)  # False为操作所有,不需要pk
    def login(self, request):
        # 1.取出用户名密码
        username1 = request.data.get('username')
        password1 = request.data.get('password')
        # 2.校验,返回结果给前端
        user_exist = User.objects.filter(username=username1, password=password1).first()
        if user_exist:
            # 生成一个随机字符串,字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
            token_str = str(uuid.uuid4())
            # 保存到UserToken表  如果之前没有数据,则是新增,如果之前有数据即之前登录过则是修改
            # 根据当前用户名去UserToken表中查询token。如果查不到,则新增;如果能查到,则修改
            UserToken.objects.update_or_create(user=user_exist, defaults={'token': token_str})
            # 返回给前端登录成功
            return Response({'code': 100, 'msg': '登录成功', 'token': token_str})
        else:
            # 返回给前端用户名或密码错误
            return Response({'code': 101, 'msg': '用户名或密码错误'})

# book接口查询一条
class BookDetailView(RetrieveAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# book接口新增一条
class BookView(CreateAPIView):
    authentication_classes = [LoginAuth]  # 需要先通过登录校验才能进行权限校验
    permission_classes = [VipPerm]  # 只要加了这个配置,以后这个类的所有函数,都需要在校验权限后才能运行
    queryset = Book.objects.all()
    serializer_class = BookSerializer

[4] 测试--校验权限 

(1) 未登录时新增图书

image

(2) 登录普通用户新增图书

登录

image

新增图书先带上token

image

权限校验失败

image

错误的token

image

(3) 登录vip用户新增图书

登录

image

新增图书先带上token

image

新增成功

image

3.4 权限组件练习

普通用户可以操作book的查所有和查一条,vip用户可以操作book的所有接口

登录组件app01的auth.py

from rest_framework.authentication import BaseAuthentication
from .models import UserToken
from rest_framework.exceptions import AuthenticationFailed

class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
        # 1.取出前端传入的token
        token = request.META.get('HTTP_TOKEN')
        # 2.校验token
        user_token = UserToken.objects.filter(token=token).first()
        if user_token:
            # 返回当前登录用户与token
            # 后续在request中通过request.user就会取出返回的第一个参数,即当前登录用户
            return user_token.user, token
        else:
            raise AuthenticationFailed('未登录')

app01的models.py

from django.db import models

# Create your models here.

# 1.用户表,用来写登录接口
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    age = models.IntegerField()
    role = models.IntegerField(choices=((1, '普通用户'), (2, '纯路人'), (3, 'vip')))

# 2.用户token表,记录用户的登录情况
class UserToken(models.Model):
    # 字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
    token = models.CharField(max_length=64)
    user = models.OneToOneField(to=User, on_delete=models.CASCADE)

# 3.图书表
class Book(models.Model):
    name = models.CharField(max_length=32)
    publish = models.CharField(max_length=32)
    price = models.IntegerField()

权限组件app01的permission.py

from rest_framework.permissions import BasePermission

class RolePerm(BasePermission):
    def has_permission(self, request, view):
        # 在auth组件中return了当前用户,或通过auth之后在request中就有当前登录用户
        # 校验普通用户有对应的权限则返回True,是vip用户也返回True
        if request.user.role == 1:  # 普通用户
            if view.action in ['list', 'retrieve']:  # 操作是查询所有和查询一条
                return True
            else:
                return False
        elif request.user.role == 3:  # vip用户,所有权限都返回True
            return True

序列化组件app01的serializer.py

from .models import Book
from rest_framework import serializers

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

app01的views.py

"""
登录接口
前端传入的数据类型:{username, password}
请求方式:post
后端视图类继承哪个基类:
    需要进行数据库操作,但是不需要序列化输入或反序列化校验
    继承APIView,还要自动生成路由---ViewSet
"""

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User, UserToken, Book
import uuid
from .serializer import BookSerializer
from .models import Book
from .permission import RolePerm
from .auth import LoginAuth
from rest_framework.viewsets import ModelViewSet

# 登录接口
class LoginView(ViewSet):
    authentication_classes = []

    @action(methods=['post'], detail=False)  # False为操作所有,不需要pk
    def login(self, request):
        # 1.取出用户名密码
        username1 = request.data.get('username')
        password1 = request.data.get('password')
        # 2.校验,返回结果给前端
        user_exist = User.objects.filter(username=username1, password=password1).first()
        if user_exist:
            # 生成一个随机字符串,字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
            token_str = str(uuid.uuid4())
            # 保存到UserToken表  如果之前没有数据,则是新增,如果之前有数据即之前登录过则是修改
            # 根据当前用户名去UserToken表中查询token。如果查不到,则新增;如果能查到,则修改
            UserToken.objects.update_or_create(user=user_exist, defaults={'token': token_str})
            # 返回给前端登录成功
            return Response({'code': 100, 'msg': '登录成功', 'token': token_str})
        else:
            # 返回给前端用户名或密码错误
            return Response({'code': 101, 'msg': '用户名或密码错误'})

# 查询、新增所有接口使用ModelViewSet
class BookView(ModelViewSet):
    authentication_classes = [LoginAuth]  # 使用登录组件
    permission_classes = [RolePerm]  # 使用权限组件
    queryset = Book.objects.all()
    serializer_class = BookSerializer

app01的urls.py

from rest_framework.routers import SimpleRouter
from .views import LoginView, BookView

router = SimpleRouter()
router.register(r'login11', LoginView, basename='login11')
router.register(r'books', BookView, basename='books')

urlpatterns = []
urlpatterns += router.urls

未登录不能操作接口

image

image

使用普通用户登录

image

普通用户可以查询一条

image

普通用户可以查询所有

image

普通用户无法新增图书

image

image

登录VIP用户

image

vip用户可以新增图书

image

vip用户也可以查询一条和查询所有

vip用户删除id为5的图书

image

image

4. 频率组件

4.1 概念

限制访问频率
  比如某个接口,一分钟只能访问5次,超过了就需要等待
  按IP地址限制
  按用户id限制  
  按设备id限制(移动端)

4.2 频率组件的使用步骤(固定用法)

[1] 创建频率类

创建一个自定义的频率类,继承SimpleRateThrottle(大部分场景)或BaseThrottle(基础场景)
这个类用于限制对某些操作或资源的访问频率

[2] 在类中重写get_cache_key函数

这个函数决定了如何从请求中获取缓存键值,以便在缓存中存储和检索频次信息。
以下信息作为缓存键值的组成部分:
用户身份:可以使用用户的唯一标识符或者请求中的某些认证信息。
资源标识:如果需要对不同的资源进行频次限制,可以加入资源标识。
操作类型:如果需要对不同的操作类型进行频次限制,可以加入操作类型。
根据具体情况,将这些信息组合起来构成一个唯一的缓存键值,并在get_cache_key函数中返回。
总结:函数返回什么,就以什么做频率限制

[3] 自定义频率类中写一个类属性,名称自定义

在频次类中,添加一个自定义名称的类属性。
这个属性用于在缓存中存储频率信息。

class IpThrottle(SimpleRateThrottle):
    rate = '3/m'  # s秒 m分钟 h小时 d天
    def get_cache_key(self, request, view):
        # 返回客户端ip,即以ip做限制
        return request.META.get('REMOTE_ADDR')

在使用时,通过IpThrottle.rate来访问这个自定义属性。

[4] 使用频率类

局部配置:视图类中

class BookView(APIView):
    throttle_classes = [SimpleRate_Throttle]

全局配置:项目的settings.py中

'DEFAULT_THROTTLE_CLASSES': [
    'app01.my_throttling.SimpleRate_Throttle'
],

局部禁用:视图类中

 throttle_classes = [ ]

4.3 SimpleRateThrottle 和 BaseThrottle

BaseThrottle:最底层抽象基类
SimpleRateThrottle:继承自 BaseThrottle,已经封装好了通用频率、缓存、时间戳逻辑,大部分场景使用这个。

BaseThrottle(ABC)
    └── SimpleRateThrottle(BaseThrottle)
         ├─ AnonRateThrottle
         └─ UserRateThrottle

1. BaseThrottle(基础抽象类)
只定义接口,完全没有实现任何限流算法、缓存、频率解析。

必须实现的方法

from rest_framework.throttling import BaseThrottle

class MyThrottle(BaseThrottle):
    def allow_request(self, request, view):
        """必须实现:返回True放行 / False拦截"""
        pass

    def wait(self):
        """可选:返回还需要等待多少秒,给429响应用;返回None表示不提供等待时间"""
        pass

什么时候用 BaseThrottle

  需要完全自定义限流逻辑,不走"次数 / 时间窗口"这套模型。
  举例:
    根据数据库表里剩余调用额度限流;
    根据黑名单直接拦截;
    调用第三方接口判断是否放行;
    不使用 Django Cache 存储计数。
缺点:
  自己要写全部:计数、存储、时间窗口、过期、解析速率字符串;
  重复造工具,普通频率限流不要直接继承它。

2. SimpleRateThrottle(简单速率节流,项目最常用)
已经实现好了:
  1. 解析速率配置 scope→ 从 settings 读取 5/min这种字符串
  2. 使用 Django Cache 存储访问时间戳列表
  3. 实现滑动时间窗口算法
  4. 实现allow_request()、wait()
只需手动编写两个地方:
  1. 设置 scope(对应 settings 里面的速率 key)
  2. 重写 get_cache_key(request, view):返回用来做限流的唯一标识(IP /user_id/ 手机号)

from rest_framework.throttling import SimpleRateThrottle

class SmsThrottle(SimpleRateThrottle):
    scope = "sms"  # 对应 DEFAULT_THROTTLE_RATES["sms"]

    def get_cache_key(self, request, view):
        phone = request.data.get("phone")
        if not phone:
            return None  # 返回None,代表不限流
        return self.cache_format % {"ident": phone}

get_cache_key()只负责产出 key;判断频率、过期清理缓存全部由基类处理。

适合场景:
  绝大多数基于「某一个标识 + 单位时间最多 N 次」的限流:
  IP 限流
  用户 ID 限流
  手机号、设备号限流

局限:
  强依赖 Django Cache;
  固定是滑动窗口模型;
  如果限流规则不是 “X 次 / Y 时间”,就不适合。

对比项BaseThrottleSimpleRateThrottle
层级 顶层抽象基类 继承 BaseThrottle,封装通用速率逻辑
是否实现 allow_request ❌ 需要自己全部写 ✅已实现,不用重写
速率解析(5/min ❌ 无,自己解析 ✅通过scope自动读取 settings 配置
缓存存储计数 ❌ 无,自己选择存储介质 ✅默认使用 Django Cache(Redis / 内存)
滑动窗口算法 ❌ 自己实现 ✅内置滑动窗口
需要重写什么 allow_request(),可选wait() 只重写 get_cache_key() + 设置 scope
适用场景 完全自定义限流逻辑,非标准速率模型 标准的「某个标识单位时间 N 次」限流
DRF 内置子类 AnonRateThrottle、UserRateThrottle

关键源码小细节(帮助理解)

1. SimpleRateThrottle.allow_request()内部做的事情:
  (1). 通过scope拿到 rate 速率配置;
  (2). 调用get_cache_key()拿到缓存 key;
  (3). cache 取出该 key 的时间戳列表;
  (4). 剔除窗口外过期时间;
  (5). 判断当前数量是否超过最大次数;
  (6). 没超限:把当前时间戳写回 cache;超限返回 False。
2. 返回 get_cache_key = None:SimpleRateThrottle会直接返回 True,跳过限流。

怎么选(开发准则)

1. 只要是:按某个标识,限制单位时间访问次数 → 优先继承 SimpleRateThrottle(99% 业务接口、短信接口)
2. 如果:
  不从 cache 读计数,从 MySQL 读额度;
  不是 “X 次每 Y 时间” 模型;
  需要接入外部风控服务;
    直接继承 BaseThrottle,自己完整实现 allow_request()。

举一个 BaseThrottle 极简示例(数据库额度限流)

from rest_framework.throttling import BaseThrottle

class QuotaDBThrottle(BaseThrottle):
    def allow_request(self, request, view):
        # 从数据库读取用户剩余调用次数
        user = request.user
        if not user.is_authenticated:
            return True
        quota = UserQuota.objects.filter(user=user).first()
        if quota.remain_count <= 0:
            return False
        quota.remain_count -=1
        quota.save()
        return True

    def wait(self):
        return None

这种不走时间窗口,走数据库剩余额度,就不适合 SimpleRateThrottle。

4.4 频率组件中类的属性名介绍

名称类别是否允许子类修改默认值说明 & 注意事项
scope 类属性 ✅ 可改属性名不能改 None 作为 key,从 DRF 配置REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']读取速率;
 
⚠️只要rate不为 None,scope 直接失效;
 
不配置对应 key 会抛ImproperlyConfigured
rate 类属性 ✅ 可改属性名不能改 None 硬编码限流速率,格式N/second|minute|hour|day
 
优先级高于 scope;
 
适合本地测试,生产优先用 scope 放到 settings 统一管理
cache_format 类属性 ✅ 可改写 throttle_%(ident)s 缓存 key 模板;%(ident)s会被get_cache_key()返回值替换;
 
多项目共用 Redis 时,修改此值用来加 key 前缀,避免 key 冲突,日常基本不用改

 

模式子类写法速率来源
模式 1(生产推荐) 只写scope 读取 settings 中DEFAULT_THROTTLE_RATES对应 key
模式 2(测试快速) 只写rate 硬编码写成固定,不走 settings 配置
错误模式 scoperate同时写 rate生效,scope完全无效
配套最小代码演示

频率组件:

from rest_framework.throttling import SimpleRateThrottle

class SmsThrottle(SimpleRateThrottle):
    scope = "sms"
    cache_format = "demo:throttle:%(ident)s"

    def get_cache_key(self, request, view):
        phone = request.data.get("phone")
        if not phone:
            return None
        return self.cache_format % {"ident": phone}

项目的settings.py

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_RATES": {
        "sms": "1/min"
    }
}

4.5 代码示例

[1] 需求

按ip地址限制,同一个ip地址,1分钟只能请求3次

[2] 频率组件

频率组件需要在一个单独的文件中,定义为app01的throttle.py

from rest_framework.throttling import SimpleRateThrottle

# 1分钟只能访问3次
class IpThrottle(SimpleRateThrottle):
    rate = '3/m'  # s秒 m分钟 h小时 d天
    def get_cache_key(self, request, view):
        # 返回客户端ip,即以ip做限制
        return request.META.get('REMOTE_ADDR')

拓展:假如返回id

#返回用户id--必须先登录
    return request.user.id

[3] 使用频率组件

app01的views.py

from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User, UserToken, Book
import uuid
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from .serializer import BookSerializer
from .permission import VipPerm
from .auth import LoginAuth
from .throttle import IpThrottle

# 登录接口
class LoginView(ViewSet):
    authentication_classes = []

    @action(methods=['post'], detail=False)  # False为操作所有,不需要pk
    def login(self, request):
        # 1.取出用户名密码
        username1 = request.data.get('username')
        password1 = request.data.get('password')
        # 2.校验,返回结果给前端
        user_exist = User.objects.filter(username=username1, password=password1).first()
        if user_exist:
            # 生成一个随机字符串,字符总数:36 个字符(32 位十六进制 + 4 个-分隔符)
            token_str = str(uuid.uuid4())
            # 保存到UserToken表  如果之前没有数据,则是新增,如果之前有数据即之前登录过则是修改
            # 根据当前用户名去UserToken表中查询token。如果查不到,则新增;如果能查到,则修改
            UserToken.objects.update_or_create(user=user_exist, defaults={'token': token_str})
            # 返回给前端登录成功
            return Response({'code': 100, 'msg': '登录成功', 'token': token_str})
        else:
            # 返回给前端用户名或密码错误
            return Response({'code': 101, 'msg': '用户名或密码错误'})

# book接口查询一条
class BookDetailView(RetrieveAPIView):
    throttle_classes = [IpThrottle]  # 只要加了这个配置,以后这个类的所有函数,都需要在校验频率后才能运行
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# book接口新增一条
class BookView(CreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

[4] 测试--校验频率

由于频率组件加在查询接口,1分钟内最多只能查询3次,

image

超过次数将受到限制

image

全局配置频率组件:

如果在项目的settings.py中全局配置频率组件,这个项目的所有接口都受到频率限制,登录、查询、新增接口任意组合1分钟只能访问3次

4.6 练习

继承BaseThrottle 写频率限制

自定义的逻辑:

(1) 取出访问者的ip

(2) 判断当前ip如果不在访问字典里,添加进去,并且返回True,表示第一次访问;如果在字典里,继续往下运行

(3) 循环判断当前ip的列表,如果有值,当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间

(4) 判断,当列表小于3,说明1分钟以内访问不足3次,把当前时间插入到列表第一个位置,返回True,顺利通过

(5) 当大于等于3,说明1分钟内访问超过3次,返回False验证失败

频率组件app01的throttle.py

from rest_framework.throttling import BaseThrottle
import time

class IpThrottle(BaseThrottle):
    # 这个字典用来存储不同ip地址访问的时间{ip1:[时间1], ip2:[时间1, 时间2]}
    VISIT_RECORD = {}
    def __init__(self):
        self.history = None  # 存储某个用户的访问时间列表,即用来存储以上字典中的某一个键对应的值

    def allow_request(self, request, view):  # 继承BaseThrottle必须重写这个函数
        # 如果没超频率,返回True
        # 如果超了频率,返回False
        # (1) 取出访问者的ip
        ip = request.META.get('REMOTE_ADDR')
        # (2)判断当前ip如果不在访问字典里,添加进去,并且返回True,表示第一次访问;如果在字典里,继续往下运行
        ctime = time.time()
        if ip not in self.VISIT_RECORD:
            # 不在字典中,表示第一次访问
            self.VISIT_RECORD[ip] = [ctime]
            return True
        # (3)循环判断当前ip的列表,如果有值,当前时间减去列表的最后一个时间,如果大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间
        self.history = self.VISIT_RECORD.get(ip)
        while self.history and ctime - self.history[-1] > 60:  # 只要大于60s循环pop
            self.history.pop()
        # (4)判断,当列表小于3,说明1分钟以内访问不足3次,把当前时间插入到列表第一个位置,返回True,顺利通过
        # (5)当大于等于3,说明1分钟内访问超过3次,返回False验证失败
        if len(self.history) < 3:
            self.history.insert(0, ctime)
            return True
        else:
            return False

    def wait(self):  # 必须要重写wait函数
        ctime = time.time()
        # 返回用户访问还需要等待的时间
        return 60 - (ctime - self.history[-1])  

app01的views.py

from .serializer import BookSerializer
from .models import Book
from rest_framework.generics import ListAPIView
from .throttle import IpThrottle

# 查询所有
class BookView(ListAPIView):
    throttle_classes = [IpThrottle]  # 使用频率组件
    queryset = Book.objects.all()
    serializer_class = BookSerializer

app01的urls.py

from django.urls import path
from .views import BookView

urlpatterns = [
    path('books/', BookView.as_view()),
]

查询所有

image

1分钟内访问超过3次之后

image

 

 

posted @ 2026-08-26 23:50  pythondjango  阅读(6)  评论(0)    收藏  举报