drf-day7——认证组件、权限组件、频率组件、过滤排序、分页
一. 认证组件
需求:
有的接口需要登录后才能访问,有的接口,不登录就能访问——这就是登录认证的限制
简单实现的方式:写一个登录接口,返回token,以后只要带着token过来,就是登录了,不带,就没有登录。条件如下:
- 查询所有不需要登录就可以访问
- 查询单个 需要登录才能访问
1.1 登录接口
# 认证是基于登录的接口上面操作的 所以前戏编写一个简单的登录接口 # models.py class User(models.Model): # 简易的用户信息账号密码 username = models.CharField(max_length=32) password = models.CharField(max_length=32) def __str__(self): return self.username # 跟User表是一对一外键关联,存储用户登录状态用的 class UserToken(model.Model): user = models.OneToOneField(to='User', on_delete=models.CASCADE) # 一对一关联 token = models.CharField(max_length=32, null=True) # 如果用户没有登录则没有值 如果登录则有值 # views.py '登录接口功能: 自动生成路由+登录功能, 不用序列化,因为继承ViewSet' class UserView(ViewSet): @action(method=['POST'], detail=False, url_path='login',url_name='login') def login(self, request): username = request.data.get('username') password = request.data.get('password') user = User.objects.filter(username=username,password=password).first() # 比对用户和密码 if user: token = str(uuid.uuid4()) # uuid4 随机获得用不重复的字符串 机制跟Cookie中的验证码一样 # 在userToken表中存储一下: 1 从来没有登录过,插入一条 2 登陆过,修改记录 UserToken.objects.update_or_create(defaults={'token':token}, user=user) # 通过user去UserToken表中查数据, 如果能查到,使用defaults的数据更新,如果查不到,直接通过user和defaults的数据新增 # kwargs 传入的东西查找,能找到,使用defaults的更新,否则新增一条 return Response({'code': 100,'msg':'登陆成功', 'token':token}) else: return Response({'code': 101, 'msg': '用户名或密码错误'}) # urls.py from rest_framework.routers import SimpleRouter, DefaultRouter router = SimperRouter() router.register('users', views.UserView, 'users') urlpatterns += router.urls '''一个简单的登录接口就写好了 每次登录都会更新Token 相当于登录了之前的设备就无效了'''
update_or_create源码如下
def update_or_create(self, defaults=None, **kwargs): defaults = defaults or {} self._for_write = True with transaction.atomic(using=self.db): try: obj = self.select_for_update().get(**kwargs) except self.model.DoesNotExist: params = self._extract_model_params(defaults, **kwargs) obj, created = self._create_object_from_params(kwargs, params, lock=True) if created: return obj, created for k, v in defaults.items(): setattr(obj, k, v() if callable(v) else v) obj.save(using=self.db) return obj, False
1.2 认证组件使用步骤
- 需要写一个认证类, 因为我们需要在应用中另外创建一个py文件编码认证类, 需要继承BaseAuthentication这个类
通过查看源码我们发现有个authenticate方法需要我们重写,否则就会报错,这就是我们需要编写认证功能的类
class BaseAuthentication: def authenticate(self, request): raise NotImplementedError(".authenticate() must be overridden.") def authenticate_header(self, request): pass
- 重写authenticate方法,在该方法中实现登录认证
token在哪里带?如何认证它是登录了的
用token来判断是否登录,登录了在访问的时候带上token, 目前阶段我们在地址栏中携带了token的数据, 后面可以在请求头中添加token的数据
- 如果认证成功,返回两个值【返回None或两个值(固定的: 当登录用户, token)】
- 认证不通过,用AuthenticationFailed类抛异常
代码如下:
authticate.py(认证类)
# 自己写的认证类, 继承某个类 from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from .models import UserToken class LoginAuth(BaseAuthentication): def authenticate(self, request): # 重写方法 # 这这里实现认证, 如果是登录的,继续往后走返回两个值,如果不是抛异常 # 请求中是否携带token, 判断是否登录, 放在地址栏中 token = request.query_params.get('token',None)# 查找是否有token这个变量名的值,如果没有就返回None,默认好像返回的是数字 if token: # 前端传入token了,去表中查,如果能查到,登录了,返回两个值[固定的:当前登录用户,token] user_token = UserToken.objects.filter(token=token).first() if user_token: return user_token.user, token else: # 没有登录抛异常 raise AuthenticationFailed('token认证失败') else: raise AuthenticationFailed('token没传') # 前端传入的请求头中的数据从哪里取? GET, body, POST,data
- 认证类的使用
当我们编写好认证类中的认证代码,接着导入到视图使用
from rest_framework.generics import ListAPIView,RetrieveAPIView from rest_framework.viewsets import ViewSetMixin from .authenticate import LoginAuth # 查询所有 class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer class BookDetailView(ViewSetMixin, RetrieveAPIView): queryset = Book.objects.all() serializer_class = BookSerializer authentication_class = [LoginAuth] # 认证类
- 局部使用和全局使用
局部使用:只在某个视图类中使用【当前视图类管理的所有接口】
class BookDetailView(ViewSetMixin, RetrieveAPIView): authentication_classes = [LoginAuth]
全局使用:在配置文件settings.py中编写,全局所有接口都生效
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES':['app01.authenticate.LoginAuth'] }
注意事项:不要在配置文件中乱导入不使用的东西,否则会报错,但是在导入类似认证类这样的文件时,可以写上导入的代码然后再修改,最后写进配置中,这样可以减少错误。
局部禁用:(登陆接口很明显是不需要校验是否登陆的,因此有了这个局部禁用的需求,我们把他的authentication_classes配置成空就是局部禁用)
class BookDetailView(ViewSetMixin, RetrieveAPIView): authentication_classes = []
1.3 整体代码
# 查询所有 class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer # 查询单个 class BookDetailView(ViewSetMixin, RetrieveAPIView): queryset = Book.objects.all() serializer_class = BookSerializer authentication_classes = [LoginAuth] # 需要写一个认证类,需要咱们自行编写
authenticate.py(认证类)
# 自己写的认证类,继承某个类 from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from .models import UserToken class LoginAuth(BaseAuthentication): def authenticate(self, request): # 在这里实现认证,如果是登录的,继续往后走返回两个值,如果不是抛异常 # 请求中是否携带token,判断是否登录,放在地址栏中 token = request.query_params.get('token', None) # 查找是否有token这个变量名的值,如果没有就返回None,默认好像返回的是数字 if token: # 前端传入token了,去表中查,如果能查到,登录了,返回两个值[固定的:当前登录用户,token] user_token = UserToken.objects.filter(token=token).first() if user_token: return user_token.user, token else: # 没有登录抛异常 raise AuthenticationFailed('token认证失败') else: raise AuthenticationFailed('token没传') # 前端传入的请求头中的数据从哪取? GET,body,POST,data
urls.py
from django.contrib import admin from django.urls import path, include from app01 import views from rest_framework.routers import SimpleRouter router = SimpleRouter() # 后面这个少的用的多, router.register('user', views.UserView, 'user') router.register('books', views.BookView, 'books') router.register('books', views.BookDetailView, 'books') urlpatterns = [ path('admin/', admin.site.urls), path('api/v1/', include(router.urls)), ]
1.4 认证时cookie的获取方式
- 当前端从地址栏中返回cookie时,就跟我们代码中返回token时一样的,获取方式:
request.query_params.get('token')
- 原生django中,取出前端传入cookie(即前端直接传入cookie),从哪里取?
request.COOKIE.get('sessionid')
- 后期如果想从请求头中取cookie
request.META.get('HTTP_TOKEN')
二. 权限组件
2.1 需求分析
在一些软件中即便我们登录了有些接口,还是不能访问,因为没有权限
这里我们的需求是:
-查询单个需要超级管理员才能访问 -查询所有,所有登录用户都能访问
而我们的User表中并没有字段用户区分用户权限,因此这里我们需要手动创建一个新的user_type字段:
class User(models.Model): username = models.CharField(max_length=32) password = models.CharField(max_length=32) user_type = models.IntegerField(choices=((1, '超级管理员'), (2, '普通用户'), (3, '2B用户')), default=2)
2.2 权限的使用
- 1、跟认证类的使用类似,需要先创建一个py文件编写权限类,需要继承BasePermission父类
- 2、这里我们需要重写has_permission方法,在该方法中实现权限认证,在这方法中,request.user就是当前登录用户(简单来说就是因为认证组件校验的时候返回的是user)
- 3、如果有权限,返回True。没有权限 返回False,并给当前对象产生一个定制的返回信息:self.message=' '
- 4、在视图层中导入使用(测试的时候路由跟认证组件的一样)
- 5、局部使用和全局使用
局部使用:只在某个视图类中使用【当前视图类管理的所有接口】
class BookDetailView(ViewSetMixin, RetrieveAPIView): permission_classes = [CommonPermission]
全局使用: 在配置文件settings.py中编写,全局所有接口都生效
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES':[ 'app01.permissions.CommonPermission', ] }
局部禁用:局部配置中的配置信息改成空就可以设置成局部禁用
class BookDetailView(ViewSetMixin, RetrieveAPIView): permission_classes = []
2.3 代码
整体部分不需要修改,跟认证组件末尾的代码一样,需要修改的代码如下:
models.py(修改User表的配置后需要重新进行数据库迁移)
class User(models.Model): username = models.CharField(max_length=32) password = models.CharField(max_length=32) user_type = models.IntegerField(choices=((1, '超级管理员'), (2, '普通用户'), (3, '2B用户')), default=2)
permission.py(权限类)
# 写权限类, 写一个类, 继承基类BasePermission, 重写has_permission方法,在方法中实现权限认证,如果有权限return True, 如果没有权限, 返回False from rest_framework.permissions import BasePermission class CommonPermission(BasePermission): def has_permission(self, request, view): # 实现权限的控制 ---> 知道当前登录用户是谁? 当前用户登录是 request.user if request.user.user_type == 1: return True else: # 没有权限,向对象中放一个属性 message # 如果表模型中, 使用了choice,就可以通过 get_字段名_display() 拿到choice对应的中文 self.message = '您是【%S】, 您没有权限' % request.user.get_user_type_display() return False
views.py
别的部分不需要改,主要是查询单个部分需要改
from .permissions import CommonPermission class BookDetailView(ViewSetMixin, RetrieveAPIView): queryset = Book.objects.all() serializer_class = BookSerializer # authentication_classes = [LoginAuth] # 需要写一个认证类,需要咱们写 permission_classes = [CommonPermission]
三. 频率组件
需求分析:
我们需要控制某个接口被访问的频率(次数)
通常来说我们是想着某个ip的访问次数
3.1 使用步骤
- 1、跟前两个组件类似,需要先创建一个py文件编写频率类,需要继承SimpleRateThrottle父类
- 2、重写get_cache_key方法(可以在源码中发现在这个方法不重写会报错),这个方法返回什么,就以什么做限制,比如我们返回ip,就以用户id做限制
- 3、编写完方法后我们在频率类中还需要配置一个scope属性,这个属性影响着配置文件中的使用
- 4、在settings.py中添加配置,设置限制的方式
'DEFAULT_THROTTLE_RATES': { 'lqz': '5/h', },
我们在scope属性中写什么, 这里的配置中的key就是什么,后面的value是频率限制的方式 有以下几种:3/m(分) 3/h(小时) 3/s(秒) 3/d(天), 这里只要开头字母符合条件即可,****写成mxxx也会自动识别
- 接着我们在视图类中以类似前面两个组件的方式使用即可(测试路由也基本一样)
- 局部使用和全局使用
局部使用:只在某个视图类中使用【当前视图类管理的所有接口】
from .throtting import CommonThrottle class BookDetailView(ViewSetMixin, RetrieveAPIView): throttle_classes = [CommonThrottle]
全局使用:在配置文件中settings.py编写,全局所有接口都生效
REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': ['app01.throttling.CommonThrottle'], }
局部禁用:局部配置中的配置信息改成空就可以设置成局部禁用
class BookDetailView(ViewSetMixin, RetrieveAPIView): throttle_classes = []
3.2 代码
throttling.py(频率类)
# 频率类 不继承BaseThrottle 继承继承SimpleRateThrottle,少写代码 from rest_framework.throttling import BaseThrottle, SimpleRateThrottle class CommonThrottle(SimpleRateThrottle): # 类属性,属性值随便写 # 配置文件中配置 scope = 'lqz' def get_cache_key(self, request, view): # 返回什么,就以什么做频率限制【可以返回ip 或用户ID】 # 客户端的ip地址从哪里拿? return request.META.get('REMOTE_ADDR') # 以ip做限制 # return request.user.pk # 以用户id做限制
settings.py
# 以后这个配置项,就是该项目drf的自有配置 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { 'lqz': '5/h', }, 'DEFAULT_THROTTLE_CLASSES': ['app01.throttling.CommonThrottle'], } '最后这个是全局配置的配置代码'
views.py
from .throtting import CommonThrottle class BookDetailView(ViewSetMixin, RetrieveAPIView): queryset = Book.objects.all() serializer_class = BookSerializer # authentication_classes = [LoginAuth] # 需要写一个认证类,需要咱们写 # permission_classes = [CommonPermission] throttle_classes = [CommonThrottle]
四.过滤排序
restful规范中,要求了 请求地址中带过滤条件
5个接口中,只有一个接口需要有过滤和排序,就是查询所有解耦
举例:
# 查询 所有图书接口,查询以 红 开头的所有图书
4.0 继承APIView自己写(伪代码)
class BookView(APIView): def get(self,request): search = request.query_params.get('search') books = Book.objects.fiter()
4.1 内置过滤类的使用【使用前提是继承GenericAPIView】
内置过滤类,必须继承GenericAPIView及其子类,才能使用这种方法---》配置过滤类的方式 from rest_framework.filters import SearchFilter, OrderingFilter class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer # SearchFilter内置的, 固定用法,模糊匹配 # 就有过滤功能 指定按照哪个字段过滤 filter_backends = [SerachFilter] # search_fields = ['name'] # 可以按名字模糊匹配 search_fields = ['name','price'] # 可以按名字模糊匹配或价格模糊匹配 # 可以使用的搜索方式 http://127.0.0.1:8000/api/v1/books/?search=红 # name或price中只要有红就会搜出来 # 继承APIView如何写 完全自己写, 麻烦,但是清晰(功能都是自己写出来的)
4.2 使用第三方django-filter实现过滤
需要安装: django-filter模块
from django_filters.rest_framework import DjangoFilterBackend '导入模块,这个类就是一个过滤类,在使用的时候我们不再使用search_fields,而是使用filterset_fields,需要注意的是这个第三方模块是完整匹配(即匹配内容一模一样才会被匹配上)' class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [] authentication_classes = [] throttle_classes = [] filter_backends = [DjangoFilterBackend] filterset_fields = ['name','price'] # 支持完整匹配 name=聊斋11&price=933 # 支持的查询方式 http://127.0.0.1:8000/api/v1/books/?price=939 http://127.0.0.1:8000/api/v1/books/?price=939&name=红楼猛
4.3 自己定制过滤类实现过滤(类似前三个组件)
需求举例: 查询价格大于100的所有图书
4.3.1 使用步骤
- 第一步: 定义一个过滤类, 继承BaseFilterBackend, 重写filter_queryset方法
class CommonFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): # 在里面实现过滤,返回qs对象 就是过滤后的数据 price_gt = request.query_params.get('price_gt',None) if price_gt: qs = queryset.filter(price__gt=int(price_gt)) return qs else: return queryset
- 第二步:配置在视图类上,路由等配置跟之前的一样
from django_filters.rest_framework import DjangoFilterBackend class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer filter_backends = [CommonFilter] # 可以定制多个,从左往右,依次执行
- 测试是使用的路由
http://127.0.0.1:8000/api/v1/books/?price_gt=100
4.3.2 代码
filter.py(自定义过滤类)
from rest_framework.filters import BaseFilterBackend class CommonFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): # 在里面实现过滤,返回qs对象,就是过滤后的数据 price_gt = request.query_params.get('price_gt', None) if price_gt: qs = queryset.filter(price__gt=int(price_gt)) return qs else: return queryset
views.py
from .filter import CommonFilter class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [] authentication_classes = [] throttle_classes = [] filter_backends = [CommonFilter] # 可以定制多个,从左往右,依次执行 ordering_fields = ['price','id']
注意事项:
1、因为最开始的时候我们给这个价格字段在模型层中设置的是CharField字段,因此会导致我们过滤类中出现问题,因为CharField字段不支持双下划线的gt方法(mysql神器的双下划线部分知识点)
2、这里我们介绍了三种过滤类的使用,我们也要想到这方面,就是这三种过滤类是可以混着一起用上的
models.py(需要修改的部分代码)
4.4 排序的使用
from .filter import CommonFilter from rest_framework.filters import OrderingFilter class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [] authentication_classes = [] throttle_classes = [] filter_backends = [OrderingFilter, CommonFilter] # 可以定制多个,从左往右,依次执行 ordering_fields = ['price','id'] # 支持的查询方法(ordering这个单词是固定的,后面的字段前面加上'-'号就可以反向排序,同时排序也可以写多个排序字段): http://127.0.0.1:8000/api/v1/books/?ordering=price http://127.0.0.1:8000/api/v1/books/?ordering=-price http://127.0.0.1:8000/api/v1/books/?ordering=-id,price
五.分页
- 分页功能,只有查询所有接口,才有分页
- drf内置了三个分页器,对应三种分页方式
- 内置的分页类不能直接使用,需要继承,定制一些参数后才能使用
使用步骤
- 步骤一:创建一个py文件编写分页用到的自定义类,分页的三个类并不能直接使用,需要我们进行配置
- 步骤二:编写这个自定义类
- 步骤三:导入视图类中,并添加配置
代码
page.py(自定义分页类)
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination # 网页用它 class CommonPageNumberPagination(PageNumberPagination): page_size = 2 # 每页显示2条 page_query_param = 'page' # page=10 查询第10页的数据,每页显示2条 page_size_query_param = 'size' # page=10&size=5 查询第10页,每页显示5条 max_page_size = 5 # 每页最大显示10条 ''' page_size 每页数目 page_query_param 前端发送的页数关键字名,默认为”page” page_size_query_param 前端发送的每页数目关键字名,默认为None max_page_size 前端最多能设置的每页数量 ''' # LimitOffset class CommonLimitOffsetPagination(LimitOffsetPagination): default_limit = 3 # 每页显示2条 limit_query_param = 'limit' # limit=3 取3条 offset_query_param = 'offset' # offset=1 从第一个位置开始,取limit条 max_limit = 5 # offset=3&limit=2 0 1 2 3 4 5 ''' default_limit 默认限制,默认值与PAGE_SIZE设置一直 limit_query_param limit参数名,默认’limit’ offset_query_param offset参数名,默认’offset’ max_limit 最大limit限制,默认None ''' # app 用下面 class CommonCursorPagination(CursorPagination): cursor_query_param = 'cursor' # 查询参数 page_size = 2 # 每页多少条 ordering = 'id' # 排序字段 ''' cursor_query_param:默认查询字段,不需要修改 page_size:每页数目 ordering:按什么排序,需要指定 '''
views.py
# 分页功能 必须是继承GenericAPIView ,如果是继承APIView,要自己写(你写) from .page import CommonPageNumberPagination as PageNumberPagination from .page import CommonLimitOffsetPagination as LimitOffsetPagination from .page import CommonCursorPagination as CommonCursorPagination class BookView(ViewSetMixin, ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [] authentication_classes = [] throttle_classes = [] # 之前的东西一样用 ,内置的分页类不能直接使用,需要继承,定制一些参数后才能使用 # pagination_class = PageNumberPagination #基本分页方式(基本是这种,网页端):http://127.0.0.1:8000/api/v1/books/?page=2&size=3 # pagination_class = LimitOffsetPagination # 偏移分页 http://127.0.0.1:8000/api/v1/books/?limit=4&offset=1 # 从第一条开始,取4条 pagination_class = CommonCursorPagination # 游标分页,只能下一页,上一页,不能跳到中间,但它的效率最高,大数据量分页,使用这种较好
ps1:我们不难发现,今天讲解的组件,除了分页都是用列表来传需要使用的类的,这里我们也可以点到rest_frameword的源码中查看,我们会发现他的配置中分页的配置默认为None
ps2:三种分页方式中第三种游标分页效率最高,因为他不能根据页数自行跳转,只能上一页或者下一页进行跳转。这也导致了他的数据量小,所以相比前需要算出所有页面的网址的两种方式,他的速度更快。
六、作业
# 编写认证类,权限类,频率类,登录接口 -所有接口,一分钟访问10次 -查询所有图书和查询单条图书不登录可以访问 -新增,删除,修改必须登录后才能访问 -删除功能只有超级管理员能删除 # 三种过滤方式实现能够搜索图书,带排序 ------------------------------------------- # 高级---部分同学做 -继承BaseThrottle,重写allow_request,实现按ip,一分钟只能访问3次 #(1)取出访问者ip #(2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问,在字典里,继续往下走 #(3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间, #(4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过 #(5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败 # 基于APIView写分页
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构