drf-前篇
# 分离与不分离
1.Django的架构 浏览器--> wsgi(accept、recv、send) 中间件 路由 视图 数据库 模版
2.前后端分离与不分离 模版是谁渲染
step1> 浏览器向 前端服务器 发送请求 --> 前端服务器 返回html+css+js给浏览器,浏览器呈现空荡荡的页面,没有数据
step2> 浏览器运行js代码向 后端服务器 发送ajax请求 --> 后端服务器 返回json格式的数据 --> 浏览器通过js代码将数据渲染到页面上
前端:前端代码 eg:网页、APP、微信小程序 -- 常见技术 vue.js react.js angular.js
后端:API -- Django Django-restframeworks
2
3
4
5
6
7
之前写过很多Django项目,以Django举个简单例子: (不分离、部分分离、分离)
Ps: 同理 用户信息修改时 跳转新窗口修改 与 在当前页面出现弹出框进行修改...
架构 | 区别 |
---|---|
纯Django | 什么工作都得后端渲染好 浏览器负责展示即可 |
Django+Ajax | 浏览器的DOM操作分担了后端的一些渲染工作 |
前后端分离 | 关于前端的内容后端都不管啦!! 后端只提供json数据.. 前端工作分给 前端服务器 + 浏览器的DOM操作.. |
# cbv源码
InfoView,as_view(**initkwargs)(request,id=2)
当路由匹配成功后, view加括号执行, drf 在里面干了几件事: (在drf的dispatch方法里干的).
dispatch中依次是 二次封装request -- [try] 认证、权限 -- 反射执行视图函数 [except] 捕获向上抛出的异常 -- 返回
★ 敲黑板!!
看上述简易版的,也可以自己大概捋清是怎么一回事!! 无非就是闭包、反射、继承关系.
① 先警醒一点: cls是类InfoView, self是类InfoView的实例!
self = cls(**initkwargs) # --!! 准确点说,self是InfoView的实例!!传入的cls是类InfoView!
类APIView的实例,**initkwargs意味着在路由里写代码时,path('info/', views.InfoView.as_view()),
"★" 这个as_view(),括号里是可以传递参数进去的,用作类实例化.
"★" 更有意思的是,APIView里面没有__init__,APIView继承View,所以类实例化时,它用的View的__init__
view的__init__是这样写的,很值得学习.
"""
class View:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
"""
2
3
4
5
6
7
8
9
10
11
② 再提醒一点:
as_view()里的**initkwargs用作视图类实例化时构造函数里的参数;
view()里的*args、**kwarg可接收动态路由上传递的参数.
"★" 通过源码,可以得知 在drf的视图函数中,可以通过 self.args、self.kwargs 获取路由传递的参数!!
"""
def dispatch(self, request, *args, **kwargs):
self.args = args
self.kwargs = kwargs
"""
2
3
4
5
6
7
8
9
③ 再注意几点:
- Django和DRF的CBV写法,都是闭包 + 反射 "★是一个类方法的闭包哦! -- as_view" 返回的都是 view函数..
但DRF得到的是 免除CSRF认证的view..
- 类方法里使用super,super函数的第二个参数是cls.
- request是何时传的? 路由匹配成功时.和动态路由参数一起传过去的,request是位置参数,所以需先传.
1> "★" dispatch中依次是 二次封装request -- [try] 认证、权限 -- 反射执行视图函数 [except] 捕获向上抛出的异常
"""
"★" # dispatch一开始就将request进行了二次封装,往下的request都是drf中的reuqest
request = self.initialize_request(request, *args, **kwargs) # 二次封装request
self.initial(request, *args, **kwargs) # 认证、权限、限流
"""
2> 其实Django的as_view里的代码 self.setup(request, *args, **kwargs)
setup方法里,就执行了 self.args = args self.kwargs = kwargs 不重要,不知道也无妨.
2
3
4
5
6
7
8
9
10
11
12
13
14
关于二次封装的request需注意的点:
0> 在dispath里,二次封装request后, 有这样一行代码 self.request = request
★ 意味着在视图类的任一实例方法里,都可通过 self.request 使用drf的request!!
1> 二次封装的request self.request.query_params 本质就是 self._request.GET
@property
def query_params(self):
return self._request.GET
2> 一定要注意是哪个对象!! # 若不写这个try..except..抛出异常说的是Django的request中没有,写了后,变成了drf的request中没有.
class Request:
def __getattr__(self, attr):
try:
return getattr(self._request, attr)
except AttributeError:
return self.__getattribute__(attr)
2
3
4
5
6
7
8
9
10
11
12
13
最后思考两个问题:
1. 思考下,路由如何传递的参数?
1> path('auth/<int:pk>/', views.auth),若网址是127.0.0.1:8000/auth/1/ 是以关键字参数的形式将 pk=1 传递给auth函数
"★" auth函数可以通过 def auth(request,pk):.. 或者 def auth(request,*args,**kwargs):.. 进行接收.
Ps:pk会在kwargs里.
2> ★★★ 在Django和drf的CBV里,都会执行view “加括号执行”,也就是说,匹配成功后
会将路由中的参数给到 def view(request,*args,**kwargs)里;
然后再将参数传递给 self.dispatch(request,*args,**kwargs) 函数;
然后再通过反射传递给了相应的get、post..方法. handler(request,*args,**kwargs)
<底层是怎么加括号的,路由上的参数怎么就传递到view函数的*args和**kwargs里的,不必深究.>
2. 思考下,Django和drf的request有何不同?
Django的request -- 包含请求相关的所有数据
drf的request -- 在APIView的dispatch方法中进行了二次封装!(包含原来的request,也新添加了一些东西)
Ps:在视图中想获取原来request的值,可以通过 request._request.GET request._request.POST
2
3
4
5
6
7
8
9
10
11
12
13
# 认证源码
包含 二次封装request + 认证
在开发API过程中, 有些功能需要 登录/token登陆凭证 才能访问, 有些无需登录. drf中的认证组件主要就是用来实现此功能!
在进行 "反射执行视图函数" 的操作 之前, 会先进行 二次封装request 和 认证/权限/限流 的操作!!
大体流程简单说:
1> 在dispatch方法中,进行了request请求的二次封装,封装的过程中进行了认证组件的加载!!
<其本质就是实例化每个认证类的对象并封装到drf的request中>.
2> 进行认证,认证成功,将认证类的authenticate方法返回的元祖赋值给了drf的request的 user和auth!!
只要认证过程不抛出异常, 就可以/才能 执行视图函数; 三大认证或者视图函数 他们抛出的异常都能在dispatch中顺利捕获到!
认证源码的切入点是这一行代码 request.user.
认证成功后返回的元祖 self.user,self.auth = (用户信息,token信息) 此处的self是二次封装的request对象
在二次封装request即进行Request实例化时,就将 多个认证类的实例传进去啦!!
# △ 实现认证.
1. 在settings中配置匿名用户 <其实不写也行drf默认配置里写的有>
"""
REST_FRAMEWORK = {
"UNAUTHENTICATED_USER": None, # 设置 request.user
"UNAUTHENTICATED_TOKEN": None, # 设置 request.auth
}
"""
2.写认证类,重写authenticate和authenticate_header方法..
3.在视图类中进行认证类的应用! (一般这些认证类会作用于该视图类中的所有接口)
-- 若很多视图类都需进行相同的认证类的应用,那么可以进行全局配置减少这类代码的书写!
REST_FRAMEWORK = {
"UNAUTHENTICATED_USER": None,
"UNAUTHENTICATED_TOKEN": None,
"DEFAULT_AUTHENTICATION_CLASSES": [
"ext.auth.QueryParamsAuthentication",
"ext.auth.HeaderAuthentication",
"ext.auth.NoAuthentication",
],
-- 不管怎样,不需要认证的 视图类 养成好习惯,写上authentication_classes = []
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# △ drf认证的应用场景.
特别注意!! 当我们使用drf的全局配置时, 认证组件的类不能放在视图view.py中, 会因为导入APIView导致循环引用!!
1> 局部&全局配置 -- 底层本质原理就是oop的继承,属性的查找顺序
案例1: 项目要开发3个接口,其中1个是无需登录就能访问的接口、其余2个是必须登录才能访问的接口. -- 局部配置
案例2: 项目要开发100个接口,其中1个是无需登录就能访问的接口、其余99个是必须登录才能访问的接口.. -- 全局配置!!
2> 多个认证类
案例3: 后端drf项目支持很多应用, 比如网页、小程序、app, 他们都可以访问后端某个接口.访问时需要认证,获取token信息.
一般来说, 我们可以约定这些应用的token信息通过url传递, 但前端开发不同意..
它非要在网页中将token放到url、小程序和app中将token放到请求头中.. 这就必须让后端的接口在认证时兼顾这三种情况.
<其实上面应用场景描述中说 接口,不是很严谨. 是初学者的认知>
3> "★★★"
Q:只要给视图类配置了认证,那么该视图类的 所有方法/所有视图函数 都得经过认证后才能执行!
若我们自己写的视图类中有get、post、put等方法,只需要对put方法进行认证,怎么办?/ 即get、post方法不需要认证就可以执行.
(每个请求都会执行一遍截图中的认证源码 该请求是get请求就不可能是post请求put等其它请求类型.Hhh‘废话文学’)
A: - 那么在认证类中判断请求类型是否是PUT,只对其进行认证即可!!
- 或者再写个视图类,单独实现put方法,加上认证,就完了!
- 还有种方法,重写源码中的 perform_authentication方法!
def perform_authentication(self, request):
if request.method == "PUT":
# 正常的进行认证
request.user
# 提示: 不需要认证,就是匿名用户呗.
request.user = None
request.auth = None
- 最优的方法!!推荐 重写get_authenticators方法 "一切的一切都服务于这一次的请求"
def get_authenticators(self):
if self.request.method == "PUT":
return [auth() for auth in self.authentication_classes]
return []
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# △ drf认证的关键伪代码
此处我们需重写authenticate方法: To do 用户认证
用户认证过程: 1.读取请求传递过来的token 2.校验该token的合法性
authenticate方法的返回值有三种:
1> 认证成功,返回元祖
在后续认证的相关源码中,该元祖的两个值会分别赋值给request.user、request.auth
一般request.user存储用户信息;request.auth存储Token信息
2> 认证失败,抛出异常,返回错误信息
3> 返回None 涉及到多个认证类 [类1,类2,类3,类4]
会依次执行认证类的authenticate方法,若当前类的authenticate方法返回None,会逐一往后找
直到某个类认证成功或失败,往后的认证类的authenticate方法都不再执行
若这些认证类[都]返回None,会默认返回一个匿名用户! request.user的值为None,表明是匿名访问
"""关键的伪代码如下:
for authenticator in [认证类,认证类,认证类]:
user_auth_tuple = authenticator.authenticate() # 此处抛出了异常也不会往下执行啦.
if user_auth_tuple:
self.user, self.auth = user_auth_tuple
return
"""
简单来说,执行每个认证类里的authenticate方法
- 一旦某个认证类认证成功或者认证过程抛出异常导致认证失败,不会再管后续的认证类;
- 只有返回None,才会继续往后执行下一个认证类里的authenticate方法;
(简而言之,这么些个认证类,只要有一个认证成功或失败,后续的认证类就不用管了)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# △ drf认证的默认配置
DEFAULTS = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
}
==>
- 关于drf默认配置的认证类,他们皆返回的是None!
- 关于drf默认的认证配置 --> UNAUTHENTICATED_USER 和 UNAUTHENTICATED_TOKEN
1. 若我们不重新设置UNAUTHENTICATED_USER,那么 匿名用户访问时/所有认证类都返回None 时,
在视图函数里打印 request.user 其值是AnonymousUser
2. 若我们重新设置UNAUTHENTICATED_USER,将其值设置为None,那么 匿名用户访问时/所有认证类都返回None时,
在视图函数里打印 request.user 其值是None
- 若我们 设置 authentication_classes = [] 不进行认证, 跟匿名用户访问得到的request.user、request.auth一样.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# △ 认证返回的http状态码
认证类里,认证不通过,报错 raise AuthenticationFailed({"status": False, "error": "认证失败"})
若在认证类里重写了authenticate_header方法,那么返回http的状态码是401 unauthoried未经授权;
若没有重写的话,返回的http状态码是403 forbidden 禁止;
2
3
# △ 认证失败,返回的结果
# 注意此处,传递给AuthenticationFailed的是一个字符串或传递一个字典,认证失败时,返回的结果是不一样的!!
raise AuthenticationFailed("认证失败!") ===> {"detail":"认证失败!"}
raise AuthenticationFailed({"code": 1002, "error": "认证失败"}) ===> {"code": 1002, "error": "认证失败"}
看dispatch中异常处理的源码就明白了!
2
3
4
5
# 权限源码
在开发API过程中, 有些接口的访问必须同时满足条件A、B、C; 有些接口的访问只需满足其中任意一个条件..
要实现该需求, 就得用到drf的权限..
前者是且关系, drf的权限默认支持; 后者是或关系,可扩展通过自定义来实现!!
(比如角色,该接口只有员工或经理才能访问"或关系")
像登陆接口是不需要进行认证和权限的,登陆的视图类中需写上, authentication_classes = []
permission_classes = []
# □ 实现权限
1. 写权限类,重写has_permission方法.
2. 在视图类中进行权限类的应用! -- 局部配置 (一般这些权限类会作用于该视图类中的所有接口)
-- 若很多视图类都需进行相同的视图类的应用,那么可以进行全局配置减少这类代码的书写!
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"ext.per.MyPermission1",
"ext.per.MyPermission2",
"ext.per.MyPermission3",
],
}
-- 不管怎样,不需要权限的 视图类 养成好习惯,写上permission_classes = []
2
3
4
5
6
7
8
9
10
11
12
13
# □ drf权限的应用场景
1. 按照上面“实现权限”的步骤 / 初学者的认知
-1- 权限是"且"的关系
一个权限类就是一个条件,视图类A中的所有接口都是在满足[条件1,条件2]时才能访问;
视图类B中的所有接口都是在满足[条件3,条件4]时才能访问!!
-2扩展的- 权限是"或"的关系
用户登陆后/认证成功后, 访问接口会进行有关角色的权限校验. 举个例子, 用户有三种角色, 总监/经理/员工 ,
视图类A中的所有接口只能总监或经理访问; 视图类B中的所有接口只能经理或员工访问!!
2. "★★★" (权限是"且"的关系)若想视图类A中的
GET接口不需要进行,POST接口权限满足条件1和满足条件2才能访问、PUT接口满足条件3和满足条件4才能访问. 如何实现呢??
最优解,在视图类里重写get_permissions方法!! 关键在于return哪些权限类 Hhh
def get_permissions(self):
if self.request.method == "GET":
return []
elif self.request.method == "POST":
return [MyPermission1(),MyPermission2()]
elif self.request.method == "PUT":
return [MyPermission3(),MyPermission4()]
else:
return [permission() for permission in self.permission_classes]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# □ 认证-权限-视图函数
就认证-权限而言,看下方流程图, 从认证到权限, 只要能走到权限, 当权限通过后即可到视图函数!!
只不过要分析以何种方式走到权限的, 不同的方式 request.user、request.auth的值 是不同的!
上面截图中,匿名访问的两种情况,本质都是配置的所有的认证类都返回None!!
关于raise怎样的错误信息,是剖析源码分析出来的!!
提示几点:<怕是只有自己能看懂Hhh>
- 当permission_classes = []时,权限的关键源码中for循环直接执行完啦,check_permissions方法直接返回None,相当于不进行权限!
- 执行permission_denied方法 >> 代表权限不通过
- 当authentication_classes = []时,条件 request.authenticators 为假!!
2
3
4
5
6
7
# □ drf权限的关键伪代码
权限组件=[权限类,权限类,权限类]
循环权限组件,依次执行每个权限类里的has_permission方法 会执行所有的权限类
1> 默认情况下/源码的默认逻辑,得保证所有权限类的has_permission方法 都 返回True,才表明权限验证通过.
(权限组件默认得所有都成功也就是项目中的某个请求的访问得同时满足A条件、B条件、C条件
and且;)
2> 研究源码后,可以进行扩展+自定义,将权限的验证逻辑变成 or或的关系!!
灵活应用. A、B、C条件,只需满足A条件 或 B条件 或C条件. ★★
def initial(self, request, *args, **kwargs):
self.perform_authentication(request) # 认证
self.check_permissions(request) # 权限
def check_permissions(self, request):
for permission in self.get_permissions(): # 提醒:注意permission_classes=[]的情况, 不进行权限即权限直接通过了!
if not permission.has_permission(request, self):
self.permission_denied(
request,
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
def permission_denied(self, request, message=None, code=None):
# print(request.user, request.auth, request._authenticator)
if request.authenticators and not request.successful_authenticator:
# -- 权限不通过 且 匿名访问
raise Exception({"code": 1003, "error": "NotAuthenticated 没有提供身份验证凭据!"})
# -- 权限不通过且成功访问 、权限不通过且不进行认证
raise Exception({"code": 1004, "error": "PermissionDenied 没有执行该操作的权限!"})
"""
permission.has_permission(request, self)
这就是为啥在编写权限类时要重写has_permission方法!!并且重写的该方法要返回bool值!
传入的self是当前的视图类的实例对象! 这个设计细品,能感觉很巧妙.
◆ 这意味着,
So, obj = 视图类(), 此时obj通过点语法能取到的东西,权限类的has_permission方法中的view参数都可以取到!!
比如 在视图类里定义一个类变量name='dc' 在权限类的has_permission方法中就可以通过view.name就可以取到!
>> 类变量、__init__里给类实例化变量定制的属性、类中的绑定方法、类中的非绑定方法等. 复习下oop的知识就清楚了!!
"""
---- 回顾下认证组件
认证组件=[认证类,认证类,认证类]
循环认证组件,依次执行每个认证类里的authenticate方法
- 一旦某个认证类认证成功或者认证过程抛出异常导致认证失败,不会再管后续的认证类;
- 只有返回None,才会继续往后执行下一个认证类里的authenticate方法;
简而言之,这么些个认证类,只要有一个认证成功或失败,后续的认证类就不用管了
<None None None raise 只要有一个成功即可> -- 此时,就类似于 or或的关系!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# □ drf权限的默认配置
DEFAULTS = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
}
==>
- 若啥也不做,drf的配置文件里就有一个默认的权限类,它返回的是True!
2
3
4
5
6
7
# □ 权限不通过,返回结果
默认的权限校验失败错误提示信息是这样的.
{"detail": "You do not have permission to perform this action."} # 没有执行该操作的权限!
可以在权限类中自定义错误信息:
- 在权限类中编写一个类变量 message = {"status": 1003, "msg": "无权访问1"}, message的值是啥,不通过返回的结果就是啥!!
2
3
4
5
# □ 扩展-"或"关系
★这样的话, 若视图类OrderView继承APIView, 权限的验证就是且的关系; 若继承BaseApiView, 权限的校验就是或的关系!!
class BaseApiView(APIView):
def check_permissions(self, request):
"""若是or或关系,返回True,表明权限校验成功、返回False,会将错误信息收集起来,当所有权限条件都无一通过,权限校验失败."""
no_permissions_obj = []
for permission in self.get_permissions():
if permission.has_permission(request, self):
return # -- 有一个条件满足,就直接结束这个函数啦!!
no_permissions_obj.append(permission)
else:
self.permission_denied(
request,
message=getattr(no_permissions_obj[0], 'message', None),
code=getattr(no_permissions_obj[0], 'code', None)
)
class OrderView(BaseApiView):
permission_classes = [MyPermission1, MyPermission2, MyPermission3]
def get(self, request):
return Response("OrderView-get")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# □ 认证权限&中间件
理清下述的生命周期!! ★★★
请求进来,先经过中间件
1.依次执行完所有中间件的process_request方法
2.进行路由匹配,找到了路由对应的视图函数!!(FBV) 若是drf的CBV,找到的是那个闭包中的函数view 注:此view是免除了csrf认证的!
(▼ 注意!该函数此时是没有执行的!!)
3.依次执行所有中间件的process_view方法
4.接着,执行闭包中的函数view view()
闭包中的view干了几件事
1> 创建CBV视图类的实例化对象 self = cls()
2> 执行self.disptch 注:视图类是继承APIView的,self中没有disptch,APIView中有
2.1> 请求封装
2.2> 进行认证、权限的处理
5.最后,执行所有中间件的process_response方法
2
3
4
5
6
7
8
9
10
11
12
13
# 限流源码
限流/限制访问频率.
开发过程中,某个接口不想让用户访问过于频繁,就需要用到限流机制.
# ◇ 实现限流
1. mac安装redis,启动redis 并在Django配置文件中配置redis
2. 写限流类,
-1- 限流类继承SimpleRateThrottle,重写get_cache_key -- 该方法的返回值将作为redis 一条记录的key值
-2- 速率相关的设置 二者选其一
- 设置类变量scope eg: scope="xxx"
看配置文件中的DEFAULT_THROTTLE_RATES,(优先是django项目的配置文件,再是drf的配置文件)
若DEFAULT_THROTTLE_RATES字典中没有跟scope值对应的键,那么还得设置类变量 THROTTLE_RATES = {"xxx":"5/m"}
- 设置类变量rate eg: rate="5/m"
3. 在 视图类 中应用限流类 -- 局部配置 (一般这些限流类会作用于该视图类中的所有接口)
当然限流类也可以像认证和权限一样进行全局配置!! "几乎不会进行全局配置."
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'ext.throttle.MyThrottle1',
'ext.throttle.MyThrottle2',
],
}
注: 速率在项目配置文件中的配置,当然若你不想这么做 在限流类里设置类变量 THROTTLE_RATES 也是可以的!!
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'xxx': "5/m", # 对应 类变量scope值为"xxx" 的限流类
'yyy': "2000/d", # 对应 类变量scope值为"yyy" 的限流类
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ◇ 限流的应用场景
-细说- 细说下限流
<已知条件>
MyThrottle1 -- "throttle_MyThrottle1_1":[时间戳1,时间戳2...] 5/m
MyThrottle2 -- "throttle_MyThrottle2_2":[时间戳1,时间戳2...] 3/m
视图类A有 GET类型的接口1 和 POST类型的接口2 ; 视图类B有 GET类型的接口3 和 POST类型的接口4
★: "throttle_MyThrottle1_1"是限流类MyThrottle1在redis中的key值/唯一标识,此值就是限流类里get_cache_key方法的返回值!!
<应用>
++ 按照初学者的认知 ++
① 视图类A局部配置了限流类 throttle_classes = [MyThrottle1,MyThrottle2]
==> 那么,MyThrottle1和MyThrottle2都会作用于视图类A下的所有接口(接口1和接口2),
无论浏览器访问接口1还是接口2,都会消耗每个限流类的一个次数
(即throttle_MyThrottle1_1的列表新增一个时间戳,throttle_MyThrottle1_1的列表新增一个时间戳)
② 视图类A和视图B都局部配置了限流类 throttle_classes = [MyThrottle1,MyThrottle2]
若项目中仅有视图类A和视图类B,那么,就相当于[MyThrottle1,MyThrottle2]进行了全局配置!
==> 意味着,MyThrottle1和MyThrottle2都会作用于 接口1,接口2,接口3,接口4
同理,访问1-4的任一接口,MyThrottle1和MyThrottle2的可用次数都会减1
★ 综上,关于限流,我们绝大多数场景下都选择进行局部配置! 而且每个限流类的get_cache_key方法返回的唯一标识都应是不一样的!!
++ 真实的应用场景里 ++
Q1: [场景1] 视图类A中的接口1不需要限流,接口2需要限流.
A1: 视图类A中重写get_throttles方法!! 借助self.request.method来实现.
Q2: [场景2] 视图类A的接口1限流频率 3/m、视图类A的接口2限流频率 5/m+200/h(即不仅要限制每分钟的请求次数,还需要限制每小时的)
A2: 视图类A中重写get_throttles方法!! 借助self.request.method来实现.
5/m+200/h 给接口2配置两个限流类就可以实现!
Q3: [场景3] 视图类A中的接口1在认证成功后限流频率是10/m, 匿名用户/未认证时限流频率是2/m
A3: 重写get_throttles方法!! 借助self.request.method + self.request.user来实现.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ◇ 限流核心逻辑
这个限流核心逻辑在源码中的体现就是:
SimpleRateThrottle类里的allow_request方法!! 限流类会继承SimpleRateThrottle类, 并调用allow_request方法.
假设限制 3次/10min <"用户ID" 维护一个 该用户ID曾经访问该接口的时间点列表/访问记录>
栗如- "9123563":[16:43, 16:42, 16:30, 15:20]
我们设定新加入的时间点是插入到列表下标为0的位置.
-1-> 获取当前时间 16:45
-2-> 当前时间-10分钟=计数开始时间 16:35
-3-> 剔除列表中时间点小于 16:35
的数据, 列表中剩余的数据 [16:43, 16:42]
-4-> 计算列表的长度. 若超过,错误; 若未超过,可以访问.
len([16:43, 16:42]) < 3
所以可以访问, 并将16:45这个时间点放到列表中 [16:45, 16:43, 16:42]
若超过了,就需算最早的访问记录失效需要多久,公式: 时间间隔 - (当前时间戳 - 历史中最早访问的时间戳)
你剖析allow_request方法的源码,你可以发现,关于 redis中某条数据 'xxx':[时间戳1,时间戳2] 的更新,有两个操作:
1> 首先就会检查 列表中的时间戳 有没有过期! 过期了的就剔除掉.
2> 每当我往列表里新增一个时间戳(一般是从下标0的位置插入),该条数据自动删除的时间就会更新
- 自动删除的时间更新为 新增的那个时间戳+时间间隔!
- 新增的那个肯定是最新的,这个新增的都过期了,列表中的其他时间戳肯定都过期了!!
2
3
4
5
# ◇ 思考等待时间.
★假设速率是 100/h , 它保证的是 <当前时间戳-3600s>到<当前时间戳>这一段时间 最多点击100次.
而不是! 凌晨到1点可点击100次, 1点到2点可点击100次.. 这种理解是错误的.
已知条件 -- 假设视图类A下仅有接口1,视图类A局部配置了限流类1,限流类1的速率配置是 100/h
极端场景 -- 我们以一个极端的例子进行分析:
浏览器在凌晨首次访问了接口1,过了59分钟后,从59分1s开始狂点接口1,当点击达到99次后,再次点击
会提示 下次访问还需等待多久 等待时间是 第一次访问时间经过1个小时还需多久 比如5s
5s后,我们再次点击 会提示 下次访问还需等待多久 等待时间是 第二次访问时间经过1个小时还需多久
以此类推.
骚操作:出现限流提示后,你不管它,等待1h后再点击,就可以再狂点100次啦!
2
3
4
5
6
7
# ◇ 思考唯一标识
我们先达成共识: 一个限流类对应一个唯一标识, 作为redis一条数据的k值!!
例如:以短信验证登陆为例.
短信平台自身限制每个账号1小时只能发10次验证;
但这限制的不够严谨.攻击者可以找上万个手机号,向后端的短信登陆接口发送请求,消耗短信数量.增加了维护的费用.
So,仅仅依靠短信平台的限制来防止"盗刷"是远远不够的.还需加入其他限制手段!
1> IP限制 (哪怕攻击者找了10万个手机号,也需要切换IP
2> 验证码 (减缓验证的速度
3> 像什么商品信息页面,加入防爬虫的机制
限制访问频率,我们需要一些东西来 [构建/组成] 唯一标识
- 已登陆的用户,唯一标识> 用户信息主键(ID、用户名
- 匿名用户,唯一标识> IP
扩展“暂且了解即可”:针对IP,访问者可以通过代理IP来绕过IP的限制.
SO,现在很多网页和程序在IP限制的基础上,还会加入js算法以增加访问接口的难度(不是直接向该接口发送个json数据那么简单啦)
IP+JS算法(若想破解该JS算法,需要逆向)
2
3
4
5
6
7
8
9
10
11
12
13
14
# ◇ 限流约定俗成
剖析限流组件的源码, 总结了以下规律!
-1- 继承SimpleRateThrottle的限流类需要干些什么?
1. 必须得重写get_cache_key方法! -- 返回值将会作为redis的键
2. 速率/限流频率 相关的设置 二者选其一
- 设置类变量scope eg: scope="xxx"
若配置文件的DEFAULT_THROTTLE_RATES字典中没有跟scope值对应的键,那么还得设置类变量 THROTTLE_RATES = {"xxx":"5/m"}
- 设置类变量rate eg: rate="5/m"
-2- 若限流类的频率设置为None 那么相当于该限流没设置;
-3- 若唯一标识的值为None/get_cache_key方法返回值为None,那么相当于该限流没设置;
2
3
4
5
6
7
8
9
# ◇ def限流的关键伪代码
def check_throttles(self, request):
"""检查请求是否应受到限制. 如果请求受到限制,则引发相应的异常"""
throttle_durations = []
# self.get_throttles() 得到所有限流类的实例化对象列表 将其循环. 注意:此处的类实例化,需看__init__
for throttle in self.get_throttles(): # 提醒:注意throttle_classes=[]的情况, 不进行限流即限流直接通过了!
# 注:调用的是SimpleRateThrottle里的allow_request,判断是否应该对该请求进行节流
if not throttle.allow_request(request, self):
throttle_durations.append(throttle.wait())
if throttle_durations:
# changes, see #1438
durations = [
duration for duration in throttle_durations
if duration is not None
]
duration = max(durations, default=None)
self.throttled(request, duration) # 该方法会抛出异常,告知最大需要等待多久
简单来说:
1> 当所有限流类的实例化对象执行throttle.allow_request(request, self)都返回True时,
那么throttle_durations列表为空,意味着限流的验证结束/check_throttles限流方法执行完毕
接着往下就是执行视图函数. - So,都返回True,就不限流呗.
2> 只要有一个throttle.allow_request(request, self)返回False,就会限流
将 <需要等待的时间 throttle.wait()计算需要等待的时间> 加入throttle_durations列表中
当有多个限流类的实例化对象返回False时,throttle_durations列表中就会有多个值!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ◇ drf限流的默认配置
DEFAULTS = {
'DEFAULT_THROTTLE_CLASSES': [],
}
==> 那就是默认大家都不限流呗!
2
3
4
# ◇ 扩展-自定义错误格式
限流不通过,默认返回的错误提示信息是这样的:
{"detail": "Request was throttled. Expected available in 56 seconds."}
自定义错误提示信息关键在于源码中的这个方法:
# 源码中,它只是传递了 最大等待的时间, 提示信息的格式是固定了的!!
def throttled(self, request, wait):
raise exceptions.Throttled(wait)
2
3
Q: 我们想自定义错误信息的格式!
A: 源码中使用的 exceptions.Throttled 本质就是一个继承 APIException的类.
关键在于exceptions.Throttled 类里的__init__
的最后一行代码, 其余的都可以不用管.
我们可以自己写一个类来继承 APIException!!
from rest_framework import exceptions
from rest_framework import status
class ThrottleException(exceptions.APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_code = "throttled"
def __init__(self, wait, code=None):
if not wait:
msg = 'Request was throttled.'
else:
msg = f"需等待{int(wait)}s后才能访问!"
detail = {
"code": 1005,
"data": "访问频繁",
"wait": msg,
}
super().__init__(detail, code)
class LoginView(APIView):
authentication_classes = []
permission_classes = []
throttle_classes = [MyThrottle, ]
def post(self, request):pass
def throttled(self, request, wait):
raise ThrottleException(wait)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# ◇ 扩展-allow_request的返回
allow_request方法默认返回值只有True和Fasle, 我们新增一种 raise.
源码里allow_request方法
- 返回True,表示当前限流类允许访问 >> check_throttles 里的for循环继续执行 后续限流类的allow_request方法;
- 返回False,表示当前限流类不允许访问,会计算等待时间加入throttle_durations列表中
>> check_throttles 里的for循环继续执行 后续限流类的allow_request方法;
当循环完毕,会找到throttle_durations列表中的最大等待时间,返回限流用户提示信息!
上述只是源码提供的流程,我们还可以分析源码,进行扩展.扩展/自定义第三种情况.
check_throttles 当前限流类不允许访问时,不返回False,而是直接抛出异常APIException类型的异常
该异常的抛出会导致循环结束,程序终止,也就是说, '[<后续的限流类都不会执行,不会管啦>]'.
异常会被APIView.dispatch里的try..except..捕获到!!
2
3
4
5
6
7
8
9
10
具体如何实现扩展呢? 关键在于重写 throttle_failure 方法!!
from rest_framework import exceptions
from rest_framework import status
from rest_framework.throttling import SimpleRateThrottle
class ThrottleException(exceptions.APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_code = "throttled"
def __init__(self, wait, code=None):
if not wait:
msg = 'Request was throttled.'
else:
msg = f"需等待{int(wait)}s后才能访问!"
detail = {
"code": 1005,
"data": "访问频繁",
"wait": msg,
}
super().__init__(detail, code)
# 至此,限流类就可以有两个选择, 继承SimpleRateThrottle 或者继承RaiseRateThrottle
# 前者allow_request方法只能返回True和Fasle,后者allow_request方法可返回True、False以及raise!
class RaiseRateThrottle(SimpleRateThrottle):
def throttle_failure(self):
wait = self.wait
raise ThrottledException(wait)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# ◇ 补充
'rest_framework.throttling.UserRateThrottle',
'rest_framework.throttling.AnonRateThrottle',
这两个drf写好了的限流类,我看了下源码,还蛮有意思的,看的时候注意几点:
-1- request.user.is_authenticated
匿名用户时,request.user是AnonymousUser;
认证成功request.user通常是当前登陆用户对象,我们需要在其model类中添加is_authenticated方法(此处暂时没进行验证)
-2- 限流类的get_ident是获取ip地址.
AnonRateThrottle 用于限制未认证用户"匿名用户"的请求频率,主要根据用户的 IP地址来确定用户身份
UserRateThrottle 用于限定认证用户"认证成功、匿名用户"的请求频率 >> 认证成功-id ; 匿名用户-ip
注: 限流规则复杂了,drf的默认限流就不好使了,只能根据需求自己写限流.
2
3
4
5
6
7
8
9
10
11
12