目录
一、认证组件
需求:
有的接口需要登录后才能访问,有的接口,不登录就能访问——这就是登录认证的限制
简单实现的方式:写一个登录接口,返回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表是一对一外键关联,存储用户登录状态用的 [这个表可以没有,如果没有,把字段直接写在User表上也可以]'
class UserToken(models.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(methods=['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 = SimpleRouter()
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 认证组件使用步骤
- 1、需要写一个认证类,因此我们需要在应用中另外创建一个py文件编写认证类,需要继承BaseAuthentication这个类
通过查看源码我们可以发现有个authenticate方法需要我们重写,否则就会报错,这就是我们需要编写认证功能的类。
class BaseAuthentication:
def authenticate(self, request):
raise NotImplementedError(".authenticate() must be overridden.")
def authenticate_header(self, request):
pass
- 2、重写authenticate方法,在该方法在中实现登录认证
token在哪带的?如何认证它是登录了的?
用token来判断是否登陆,登陆了在访问的时候带上token,目前阶段我们直接在地址栏中携带token的数据,后面可以在请求头中添加token的数据
- 3、如果认证成功,返回两个值【返回None或两个值(固定的:当前登录用户,token)】
- 4、认证不通过,用AuthenticationFailed类抛异常
代码如下:
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
- 5、认证类的使用
当我们编写好了认证类中的认证代码,接着就需要导入到视图层然后使用他。
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_classes = [LoginAuth] # 需要写一个认证类,需要咱们自行编写
- 6、局部使用和全局使用
局部使用:只在某个视图类中使用【当前视图类管理的所有接口】
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 = []
- 7、测试路由参考
1.3 整体代码
views.py
# 查询所有
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')
- 原生djangozhong ,取出前端传入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也会自动识别到的。
- 5、接着我们在视图类中以类似前面两个组件的方式使用即可(测试路由也基本一样)
- 6、局部使用和全局使用
局部使用:只在某个视图类中使用【当前视图类管理的所有接口】
from .throttling 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 .throttling 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.filter()
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 = [SearchFilter]
# 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(需要修改的部分代码)
class Book(models.Model):
name = models.CharField(max_length=32, default='xx')
price = models.IntegerField()
publish = models.ForeignKey(to='Publish', on_delete=models.CASCADE) # 留住,还有很多
authors = models.ManyToManyField(to='Author')
def publish_detail(self):
return {'name': self.publish.name, 'addr': self.publish.addr}
def author_list(self):
l = []
for author in self.authors.all():
l.append({'name': author.name, 'phone': author.phone})
return l
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:按什么排序,需要指定
# 注:必须基于排序规则下进行分页
# 1)如果接口配置了OrderingFilter过滤器,那么url中必须传ordering
# 1)如果接口没有配置OrderingFilter过滤器,一定要在分页类中声明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:三种分页方式中第三种游标分页效率最高,因为他不能根据页数自行跳转,只能上一页或者下一页进行跳转。这也导致了他的数据量小,所以相比前需要算出所有页面的网址的两种方式,他的速度更快。
六、认证,权限,频率源码分析(了解)
6.1 权限类的执行源码
权限的源码执行流程
-写一个权限类,局部使用,配置在视图类的,就会执行权限类的has_permission方法,完成权限校验
之前我们在学习drf的apiview的源码中,了解到在执行视图类方法之前执行了三大认证,同时两者都是处于dispatch异常捕获结构中的(需要注意这里的dispatch是APIView的方法,不是view中的dispatch方法)。
-APIView类的497行左右, self.initial(request, *args, **kwargs)---》执行3大认证
# APIView类的399行左右:
def initial(self, request, *args, **kwargs):
# 能够解析的编码,版本控制(知道有这作用就好了,不用管,重点是下面部分)
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# 认证组件的执行位置
self.perform_authentication(request)
# 权限组件 [读它]
self.check_permissions(request)
# 频率组件
self.check_throttles(request)
接着我们点进权限组件的执行源码查看
# APIView的326 左右
def check_permissions(self, request):
# self.get_permissions()----》[CommonPermission(),]
# permission 是我们配置在视图类上权限类的对象
for permission in self.get_permissions():
# 权限类的对象,执行has_permission,这就是为什么我们写的权限类要重写has_permission方法
# self 是视图类的对象,就是咱们自己的的权限类的has_permission的view参数(具体的解释看下面)
if not permission.has_permission(request, self):
# has_permission方法的返回值是True或是False,如果return 的是False,就会走这里,走这里的结果是没有权限的情况
# 如果配了多个权限类,第一个没过,直接不会再执行下一个权限类了
self.permission_denied(
request,
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
'在check_permissions中主要是一个for循环,而for循环的对象是get_permissions方法得到的结果,因此我们进来看看他是起了啥作用'
# APIView的274行左右 get_permissions
def get_permissions(self):
# self.permission_classes 是咱们配置在视图类上的列表,里面是一个个的权限类,没加括号就是没调用,他这里就相当于把这些权限类都加括号执行,把最后获取到的对象放到列表中返回出来
# permission_classes = [CommonPermission]
# [CommonPermission(),] 本质返回了权限类的对象,放到列表中
return [permission() for permission in self.permission_classes]
'分析完之后回到上面'
'我们编写的权限类'
# 写权限类,写一个类,继承基类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
'我们在这里重新定义了has_permission方法,在上面的源码中调用了这个方法,但是他的参数中,第二个参数是self,从我们定义的参数来看,我们可以知道这个self绑定给了view'
'看完之后还是回到上面'
总结:
-
APIView中的dispatch方法,dispatch方法内部执行了initial方法进行认证权限频率这三大认证,initial方法的倒数第二行执行了权限校验---》self.check_permissions(request)
-
里面取出配置在视图类上的权限类,实例化得到对象,一个个执行对象的has_permission方法,如果返回False,就直接结束,不再继续往下执行,权限就认证通过
-
如果视图类上不配置权限类:permission_classes = [CommonPermission],会使用配置文件的api_settings.DEFAULT_PERMISSION_CLASSES
使用顺序是优先使用视图类中的局部配置,再优先使用项目配置文件中的配置,其次使用drf内置配置文件中的配置
6.2 认证源码分析
前面部分跟权限类的分析流程一样,不做详细解释了,到执行initial方法处为止。
# 之前读过:drf的apiview,在执行视图类的方法之前,执行了3大认证----》dispatch方法中的
-497行左右, self.initial(request, *args, **kwargs)---》执行3大认证
# APIView类的399行左右:
def initial(self, request, *args, **kwargs):
# 能够解析的编码,版本控制(知道有这作用就好了,不用管,重点是下面部分)
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# 认证组件的执行位置【读它】
self.perform_authentication(request)
# 权限组件
self.check_permissions(request)
# 频率组件
self.check_throttles(request)
'我们进入认证组件的源码进行分析'
# APIView的316行左右
def perform_authentication(self, request):
request.user #咱们觉得它是个属性,其实它是个方法,包装成了数据属性
# Request类的user方法(用了APIView或是GenericAPIView后,在dispatch中重新定义了request,用Request产生的对象包装了一些,可以看第三天的博客) Request的源码的219行左右
@property
def user(self):
if not hasattr(self, '_user'):
'咋们先看这里没有找到_user属性时执行的代码,可以看到执行了_authenticate方法,而当前的对象是Request类产生的,因此我们要去Request类中查找这个方法'
with wrap_attributeerrors():
self._authenticate()
return self._user
# self 是Request的对象,找Request类的self._authenticate() 373 行
def _authenticate(self):
# self.authenticators 跟我们在权限类的源码分析中见到的形式不一样,但是我们可以猜测两者的作用应该是相似的,就是我们配置在视图类上认证类的一个个对象,放到列表中
# 点进源码我们发现他是在Request类初始化的时候,传入的(看下面)
for authenticator in self.authenticators:
try:
# 而异常捕获内的代码也跟之前一样,是用于获取认证类中的重写的方法的结果返回了两个值。
# 他有两种返回结果,第一种结果时返回两个值,第一个值是当前登录用户,第二个的token,只走这一个认证类,后面的不再走了
# 第二种结果是可以返回None,会继续执行下一个认证类
# 同时我们也可以看到,如果报错了就会直接停止运行,有返回值的时候会把值获取,然后返回出去
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
# 解压赋值(当我们在获取到认证类校验之后的结果后,会到下方的代码这里进行解压赋值):
#self.user=当前登录用户,self是当次请求的新的Request的对象
#self.auth=token
self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
# self.authenticators 去Request类的init中找 152行左右
def __init__(self, request, parsers=None, authenticators=None,
negotiator=None, parser_context=None):
.....
self.authenticators = authenticators or ()
.....
# 什么时候调用Reqeust的__init__?---》APIVIew的dispatch上面的492行的:request = self.initialize_request(request, *args, **kwargs)-----》385行----》
def initialize_request(self, request, *args, **kwargs):
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
'我们可以看到就是在给老的request进行包装的时候传入的这个参数'
'接着我们点进get_authenticators方法中查看源码'
def get_authenticators(self):
return [auth() for auth in self.authentication_classes]
'这里我们可以看到,跟权限类的源码中相似的部分出现了,就是把视图类中使用的认证类进行加括号然后存放到列表中返回'
'回到上面'
# 总结:
1 配置在视图类上的认证类,会在执行视图类方法之前执行,在权限认证之前执行
2 自己写的认证类,可以返回两个值或None
3 后续可以从request.user 取出当前登录用户(前提是你要在认证类中返回)
6.3 频率源码分析
前面部分跟权限类的分析流程一样,不做详细解释了,到执行initial方法处为止。
# 之前读过:drf的apiview,在执行视图类的方法之前,执行了3大认证----》dispatch方法中的
-497行左右, self.initial(request, *args, **kwargs)---》执行3大认证
# APIView类的399行左右:
def initial(self, request, *args, **kwargs):
# 能够解析的编码,版本控制(知道有这作用就好了,不用管,重点是下面部分)
self.format_kwarg = self.get_format_suffix(**kwargs)
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# 认证组件的执行位置
self.perform_authentication(request)
# 权限组件
self.check_permissions(request)
# 频率组件【读它】
self.check_throttles(request)
# APIView 的352行
def check_throttles(self, request):
throttle_durations = []
#self.get_throttles() 这个方法我们也不能一下看出他是什么作用的,但是在这个位置,必然跟前面两个的源码中的代码的作用是类似的,因此我们也要点进源码进行查看(见下方)
# 配置在视图类上的频率类的对象,放到列表中
# 每次取出一个频率类的对象,执行allow_request方法,这里allow_request方法是存在的,因为我们继承的是SimpleRateThrottle,他内部已经写了一格allow_request方法。如果我们继承BaseThrottle可以在源码中发现allow_request方法是需要我们自行重写的,不然会报错
# 我们回顾频率类中我们自行重写的方法并没有对频率进行校验,因此我们也可以在源码中看出是allow_request方法进行了校验。
# allow_request方法的返回值如果是False,访问频率超过了限制,不能再走了
# allow_request方法的返回值如果是True,没有超出访问频率限制,可以直接往后
for throttle in self.get_throttles():
if not throttle.allow_request(request, self):
throttle_durations.append(throttle.wait())
'这里的wait()方法和后面的代码的作用可以看接下去的自定义频率类进行对比理解,不展开具体分析了'
if throttle_durations:
# Filter out `None` values which may happen in case of config / rate
# changes, see #1438
durations = [
duration for duration in throttle_durations
if duration is not None
]
duration = max(durations, default=None)
self.throttled(request, duration)
'在get_throttles这个方法中我们可以看到他确实是跟之前一样的作用,把频率类加上括号然后套在列表中返回出来'
def get_throttles(self):
return [throttle() for throttle in self.throttle_classes]
'返回上面'
'allow_request方法(讲解不详细,后文有详细的分析)'
def allow_request(self, request, view):
if self.rate is None:
return True
'这里的两个属性通过英文意思我猜测就是检测是否配置了频率要求'
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
self.history = self.cache.get(self.key, [])
self.now = self.timer()
'通过自定义频率类的学习,我们可以得知这里的字典是存储频率的检测的时间和计算频率的参数'
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
return self.throttle_success()
'这里就是根据时间范畴对字典中的数据值进行检测,并返回检测结果(布尔值)'
# 总结:
-我们写的频率类:继承BaseThrottle,重写allow_request,在内部判断,如果超频了,就返回False,如果没超频率,就返回True
6.4 自定义频率类(了解)
这里咋们了解逻辑,会用代码即可
class SuperThrottle(BaseThrottle):
VISIT_RECORD = {}
'从下面的逻辑中我们发现需要有一个访问字典存储ip和被访问的时间,因此需要在这里定义'
def __init__(self):
self.history = None
'这里的history属性同样是因为下方的代码中需要用到才定义的'
def allow_request(self, request, view):
# 自己写逻辑,判断是否超频
# (1)取出访问者ip
# (2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问,在字典里,继续往下走 {ip地址:[时间1,时间2,时间3,时间4]}
# (3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间,
# (4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
# (5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败
# (1)取出访问者ip
ip = request.META.get('REMOTE_ADDR')
import time
ctime = time.time()
# (2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问
if ip not in self.VISIT_RECORD:
self.VISIT_RECORD[ip] = [ctime, ]
return True
# self.history = [时间1]
self.history = self.VISIT_RECORD.get(ip,[])
# (3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间,
while self.history and ctime - self.history[-1] > 60:
self.history.pop()
# (4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
# (5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败
if len(self.history) < 3:
self.history.insert(0, ctime)
return True
else:
return False
'当我们不写下方的wait方法时,我们会发现跟SimpleRateThrottle中编写的allow_request方法有点不同,没有那串英文展示还有多久时间可以继续访问,这里的wait方法就是用于展示那句话的'
def wait(self):
import time
ctime = time.time()
return 60 - (ctime - self.history[-1])
6.5 SimpleRateThrottle源码分析
# 写一个频率类,重写allow_request方法,在里面实现频率控制
# SimpleRateThrottle---》allow_request
def allow_request(self, request, view):
# 这里就是通过配置文件和scop取出 频率限制是多少,比如一分钟访问5此
if self.rate is None:
return True
# 返回了ip,就以ip做限制
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
# 下面的逻辑,跟咱们写的一样
self.history = self.cache.get(self.key, [])
self.now = self.timer()
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
return self.throttle_success()
# SimpleRateThrottle的init方法
def __init__(self):
if not getattr(self, 'rate', None):
# self.rate= '5、h'
self.rate = self.get_rate()
# 这行代码我们不能直接看懂他的意思,因此需要分析源码(接着看下方的代码)
# 5 36000
self.num_requests, self.duration = self.parse_rate(self.rate)
'在SimpleRateThrottle的init方法中我们发现rate属性是用get_rate方法获取的'
# SimpleRateThrottle的get_rate() 方法
def get_rate(self):
'这一块代码就是判断我们有没有设置频率限制(scope属性),如果没有有会报错'
if not getattr(self, 'scope', None):
msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
self.__class__.__name__)
raise ImproperlyConfigured(msg)
try:
# self.scope 是 lqz 字符串
# 而scope的相关配置信息是需要我们在配置文件中自行设置的
# 源码就是把他绑定到配置文件中的存有配置信息的字典上:THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
# return '5/h'
return self.THROTTLE_RATES[self.scope]
except KeyError:
msg = "No default throttle rate set for '%s' scope" % self.scope
raise ImproperlyConfigured(msg)
'看完之后回到上面的双下init先'
# SimpleRateThrottle的parse_rate 方法
def parse_rate(self, rate):
# 这里举例rate的值是'5/h'
# 这里的if是用于处理有scope配置的时候,但配置内容为空的时候的处理方式
if rate is None:
return (None, None)
# num =5
# period= 'hour'
# 这里很明显就是进行字符串切割
num, period = rate.split('/')
# num_requests=5
# 这里就是获取频率中的访问次数限制
num_requests = int(num)
# 这里就是对频率的时间限制,因为他是用首个字符来进行时分秒的区分的,所以我们之前学习频率组件的时候说时分秒的字符开头对就可以,后面随便写
duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
# (5,36000),这里就是返回频率限制的具体内容了
return (num_requests, duration)
七、基于APIView编写分页
分页功能,只有查询所有才有
ps:编写的时候可以参照ListAPIView中继承的那个list方法进行参考
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
基于APIView编写的list
class BookView(ViewSetMixin, APIView):
'这里的方法名称必须是list,因为路由在自动自动创建的时候映射也是自动配置的'
def list(self, request):
books = Book.objects.all()
# 使用步骤
# 1 实例化得到一个分页类的对象
paginator = CommonLimitOffsetPagination()
# 2 调用分页类对象的paginate_queryset方法来完成分页(因为源码写了三个参数我们也写三个参数,最后一个参数是view,又因为我们就是在视图配中编写,所以写self就好了),返回的page是 要序列化的数据,分页好的
page = paginator.paginate_queryset(books, request, self)
if page is not None:
serializer = BookSerializer(instance=page, many=True)
# 我们可以发现源码中显示调用了get_serializer方法,这就是调用序列化类,因此这里我们也可以直接调用序列化类
# 3 返回数据,调用paginator的get_paginated_response方法
# return paginator.get_paginated_response(serializer.data)
# 通过查看他的源码我们发现改用Response返回数据的原理(看下方源码和分析)
return Response({
'total': paginator.count,
'next': paginator.get_next_link(),
'previous': paginator.get_previous_link(),
'results': serializer.data
})
'这里的字典中的键,可以自定义命名,一旦我们更改了这些名字,在postman中接收到的数据的键的名称也会更改'
第二步中需要研究的源码
page = self.paginate_queryset(queryset)
'这里的这个paginate_queryset方法是需要去GenericAPIView中查找的,代码如下:'
@property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
'我们不难发现这个被伪装的paginator其实就是产生一个分页类的对象'
def paginate_queryset(self, queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
'在最后我们看到了一个熟悉的单词paginate_queryset,这里就相当于是使用分页类产生的对象传入数据进行校验'
第三步中需要研究的源码
def get_paginated_response(self, data):
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
'这里我们可以看到这里跟第二步其实差不多,是获取序列化类产生的对象,然后返回相应的结果,我们通过之前的学习得知返回的结果肯定是Response封装的,因此肯定就是get_paginated_response方法中进行了封装'
def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
('results', data)
]))
'然后我们在视图类中写了这个方法之后,点进来会看到上方的这个源码,他就是封装了返回的数据的函数,因此我们可以直接给他进行替换,然后对格式进行自定义'