drf框架自带一些组件,完成认证、权限、频率控制等功能。
rest_framework.views的APIView实际上里面的as_view()调用了原生django的,但是它有自己的dispatch方法,在dispatch方法中的self.initial(request, *args, **kwargs)里面定义了三个组件。
一、认证组件
认证组件主要用来对用户的登录状态做校验。比如某些数据是需要登录才能查看和操作的。
使用时的写法与配置:
- 先创建一个认证的类,继承BaseAuthentication(一般认证的代码单独写一个文件)
- 局部使用:在视图类中——authentication_classes=[MyAuth,]
- 全局使用:在配置文件中配置
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.MyAuths.MyAuth",] }
4. 局部禁用:比如配置了全局,某个视图不想用,可以在视图中写个空——authentication_classes = [ ]
认证时,由于是前后端分离开发,无法知道移动端是什么,所以不能使用cookie和session,因此需要使用一个新的东西来标识用户身份——token。
具体看看token:
由于服务器端要保存大量的session,造成了压力,所以就想到了服务端如何能够在不保存session的情况下对来访的用户做验证呢?
如图,我们可以给用户发送一个token,里面包含他的id,然后利用一个加密算法,再加上一个只有我自己知道的密钥,生成一个签名,然后把这个签名连同数据一起发回给用户,服务端并不保存这个token;
等用户下次来访问的时候,带着这个token过来,然后我把这个token里面的数据和签名拿出来,然后对数据和我的密钥以原来的加密算法加密,再去对比这个加密的结果是否和签名的结果一致,如果一致,说明此用户已经登录过了,并且我可以直接拿到用户的id;如果不一致,说明数据被人篡改过了,就给客户端回复一个未通过认证即可。
然后用户需要一个唯一的id,这个可以通过很多方法生成,这里先来看看uuid
uuid是128位的全局唯一标识符(univeral unique identifier),通常用32位的一个字符串的形式来表现。
python中自带了uuid模块来进行uuid的生成和管理工作。
uuid的作用:
很多应用场景需要一个id,但是又不要求这个id 有具体的意义,仅仅用来标识一个对象。常见的用处有数据库表的id字段;用户session的key值;前端的各种UI库,因为它们通常需要动态创建各种UI元素,这些元素需要唯一的id, 这时候就需要使用UUID了。例如:一个网站在存储视频、图片等格式的文件时,这些文件的命名方式就可以采用 UUID生成的随机标识符,避免重名的出现。
UUID主要有五个算法,也就是五种方法来实现:
python的uuid模块提供的UUID类和函数uuid1(),uuid3(),uuid4(),uuid5() 来生成1, 3, 4, 5各个版本的UUID ( 需要注意的是:python中没有uuid2()这个函数)。
uuid.uuid1(node clock_seq) # 基于时间戳 # 使用主机ID, 序列号, 和当前时间来生成UUID, 可保证全球范围的唯一性. # 但由于使用该方法生成的UUID中包含有主机的网络地址, 因此可能危及隐私. # 该函数有两个参数, 如果 node 参数未指定, 系统将会自动调用 getnode() 函数来获取主机的硬件(mac)地址. # 如果 clock_seq 参数未指定系统会使用一个随机产生的14位序列号来代替. import uuid print(uuid.uuid1()) uuid.uuid3(namespace, name) # 通过计算名字和命名空间的MD5散列值得到,保证了同一命名空间中不同名字的唯一性, # 和不同命名空间的唯一性,***但同一命名空间的同一名字生成相同的uuid****。 print(uuid.uuid3(uuid.NAMESPACE_URL,'python')) print(uuid.uuid3(uuid.NAMESPACE_URL,'python')) uuid.uuid4() : 基于随机数 # 通过随机数来生成UUID. 使用的是伪随机数有一定的重复概率. print(uuid.uuid4()) uuid.uuid5(namespace, name) # 通过计算命名空间和名字的SHA-1散列值来生成UUID, 算法与 uuid.uuid3() 相同 print(uuid.uuid5(uuid.NAMESPACE_URL,'python'))
- 1 Python中没有基于 DCE 的,所以uuid2可以忽略
- 2 uuid4存在概率性重复,由无映射性
- 3 若在Global的分布式计算环境下,最好用uuid1
- 4 若有名字的唯一性要求,最好用uuid3或uuid5
了解了token和uuid生成唯一id,下面我们来看看认证组件的写法:
先来创建两张表(这里没有使用缓存数据库,先用mysql保存一下用户的随机串,未使用真正的token机制):
from django.db import models
class User(models.Model):
name=models.CharField(max_length=32)
pwd=models.CharField(max_length=64)
user_type=models.IntegerField(choices=((1,"超级管理员"),(2,"普通管理员"),(3,"2b用户")),default=3)
#跟User表做一对一关联
class Token(models.Model):
user=models.OneToOneField(to='User')
token = models.CharField(max_length=64)
认证首先要重写authenticate方法:
from rest_framework.authentication import BaseAuthentication
from app01 import models
from rest_framework.exceptions import AuthenticationFailed
class MyAuth(BaseAuthentication):
def authenticate(self, request):
# 这里我用postman发请求的时候直接在url后面带了token
token = request.GET.get('token')
token_obj = models.Token.objects.filter(token=token).first()
if token_obj:
# 有值表示登录了,把当前登录的user对象token_obj.user返回
return token_obj.user, token_obj
else:
# 没有值就抛异常
raise AuthenticationFailed('您没有登录')
然后到视图中使用(局部使用):
from rest_framework.views import APIView
from app01.MyAuths import MyAuth
from rest_framework.response import Response
from app01 import models
from django.core.exceptions import ObjectDoesNotExist
import uuid
class Login(APIView):
# 局部禁用
# authentication_classes = []
def post(self, request):
response = {'code': 100, 'msg': '登陆成功'}
name = request.data.get('name')
pwd = request.data.get('pwd')
try:
# .get(),有且只有一条才不报错,其他都抛异常
user = models.User.objects.filter(name=name, pwd=pwd).get()
# 登录成功,需要去Token表中存数据,利用uuid生成一个随机的id
token = uuid.uuid4()
models.Token.objects.update_or_create(user=user, defaults={'token':token})
response['token'] = token
except ObjectDoesNotExist as e:
response['code'] = 101
response['msg'] = '用户名或密码错误'
except Exception as e:
response['msg'] = str(e)
return Response(response)
# 用户必须登录之后才能获取所有图书接口
class Books(APIView):
# 可以写多个认证类
authentication_classes = [MyAuth, ]
def get(self, request):
# request.user就是当前登录的用户
print(request.user.name)
# 这里就不到数据库取数据了
return Response('返回了所有图书')
class Publish(APIView):
def get(self, request):
return Response('返回了所有出版社信息')
路由:
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^books/', views.Books.as_view()),
url(r'^login/', views.Login.as_view()),
url(r'^publish/', views.Publish.as_view()),
]
这里我给图书的查询做了登录校验,出版社的没有做。利用postman在不登录的情况下发一个请求试试:
出版社信息正常返回。
图书的要求登录。那么我们利用postman发送数据直接登陆试试。
以下是登录成功后返回的信息:
可以看到,返回给我们一个token,这时候我们再带上这个token去访问图书接口就可以成功访问。
认证源码分析:
我们的CBV继承了APIView类,APIView类中的dispatch方法中有self.initial方法,这个方法包含了认证、权限和频率控制。
self.perform_authentication(request)就是认证的方法
可以看到,这个方法内部调用了request的user方法,必须明确的是,request是封装后的request。所以我们要去Request类中找user方法。
一个个认证类的对象是在reuqest对象实例化的时候传入的。
APIView中的get_authenticators,通过列表推导式生成一个个的认证类对象,然后传入request对象中
自己写一个不存数据库的token验证:
自己瞎写的,只是把id作为有效的加密部分,只是给自己做个参考。
认证:
from rest_framework.authentication import BaseAuthentication
from app01 import models
from rest_framework.exceptions import AuthenticationFailed
import hashlib
def get_md5(info):
md5 = hashlib.md5()
md5.update('23784325174'.encode('utf-8'))
md5.update(info.encode('utf-8'))
md5.update('236556219'.encode('utf-8'))
return md5.hexdigest()+'|'+info
class Au(BaseAuthentication):
def authenticate(self, request):
token = request.META.get('HTTP_TOKEN')
name = request.GET.get('name')
user_obj = models.User.objects.filter(name=name).first()
if get_md5(str(user_obj.pk)) == token:
return user_obj,token
else:
raise AuthenticationFailed('您没有登录')
视图:
from rest_framework.views import APIView
from app01.MyAuths import Au
from rest_framework.response import Response
from app01 import models
import hashlib
# 获取token
def get_md5(info):
md5 = hashlib.md5()
md5.update('23784325174'.encode('utf-8'))
md5.update(info.encode('utf-8'))
md5.update('236556219'.encode('utf-8'))
return md5.hexdigest()+'|'+info
# 登录接口
class Login(APIView):
def post(self, request):
response = {'code': 100, 'msg': '登陆成功'}
name = request.data.get('name')
pwd = request.data.get('pwd')
try:
user = models.User.objects.filter(name=name, pwd=pwd).get()
id = user.id
signal = get_md5(str(id))
response['token'] = signal
except Exception as e:
print(e)
return Response(response)
# 用户必须登录之后才能获取所有图书接口
class Books(APIView):
# 可以写多个认证类
authentication_classes = [Au, ]
def get(self, request):
# request.user就是当前登录的用户
print(request.user.name)
# 这里就不到数据库取数据了
return Response('返回了所有图书')
利用postman发请求验证:用户信息直接带在GET请求里,token放在请求头里。
注意:请求头里的东西这么取:request.META.get("HTTP_键")
二、权限组件
权限组件用来验证某个用户是否拥有某个权限。比如一般运维只有管理服务器的简单权限,而高级运维有root管理员权限。
之前建表的时候,在用户里面创建了不同权限的用户,下面我们根据权限来判断用户是否有权干某事。
权限类使用顺序:先用视图类中的权限类,再用settings里配置的权限类,最后用默认的权限类
先去写一个权限组件 ,需要继承BasePermission类,重写has_permission(self, request, view)方法
from rest_framework.permissions import BasePermission
class MyPermission(BasePermission):
# 通过message可以自己定制发给前台的信息
message = '您不是超级用户,无法查看图书'
def has_permission(self, request, view):
# 因为权限是在认证之后执行的,所以可以取到request.user
user_type = request.user.get_user_type_display()
if user_type == '超级管理员':
# 这里需要返回True或False
return True
return False
然后到视图中使用,这里注意,他跟认证的使用是一样的
- 局部使用:permission_classes = [MyPermission, ]
- 全局使用:
REST_FRAMEWORK={ "DEFAULT_AUTHENTICATION_CLASSES":["app01.MyAuths.Authentication",], "DEFAULT_PERMISSION_CLASSES":["app01.MyAuths.MyPermission",] }
- 局部禁用:permission_classes = [ ]
from rest_framework.views import APIView
from app01.MyAuths import MyPermission
from rest_framework.response import Response
# 用户必须登录之后且是超级管理员才能获取所有图书接口
class Books(APIView):
# 可以写多个认证类
authentication_classes = [MyAuth, ]
# 只有超级管理员才能访问该接口
permission_classes = [MyPermission, ]
def get(self, request):
# request.user就是当前登录的用户
print(request.user.name)
# 这里就不到数据库取数据了
return Response('返回了所有图书')
接下来用postman发请求:
先用超级管理员发
然后用普通用户发:
权限源码分析:
循环拿到一个个权限类的对象,执行我们自己重写的has_permission方法。
如果验证不通过,则返回异常。
注意:这里就是为什么我们可以写一个message="错误信息",然后发送到前台了
三、频率控制
3.1、使用
第一步:
- 写一个频率类,继承SimpleRateThrottle
- 重写get_cache_key,返回self.get_ident(request)
- 一定要记住配置一个scope=字符串
第二步:
- 在settings中配置
REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES':{ 'limit':'3/m' # 3/m指的是一分钟限制3次访问 } }
源码中的对应关系如下,所以在配置是可以写minutes,但是后面只取索引0,前面是字典对应关系。
局部使用:
- 在视图中: throttle_classes = [自己写的频率认证类名, ]
全局使用:
- 在settings配置文件中:
REST_FRAMEWORK = { # 这里路径是频率类所在的路径 'DEFAULT_THROTTLE_CLASSES': ['app01.MyThrottle.Throttle'], 'DEFAULT_THROTTLE_RATES':{ 'limit':'3/m' # 3/m指的是一分钟限制3次访问 } }
局部禁用:
- 在视图中: throttle_classes = [ ]
频率控制类:
from rest_framework.throttling import SimpleRateThrottle
# 频率控制类
class Throttle(SimpleRateThrottle):
scope = 'limit'
def get_cache_key(self, request, view):
"""
返回什么值就以什么过滤。比如返回id,就按id过滤
return self.get_ident(request)
"""
# 下面的方式是以ip做过滤条件
return request.META.get('REMOTE_ADDR')
路由:
url(r'^books/', views.Books.as_view())
视图:
from rest_framework.views import APIView
from rest_framework.response import Response
from app01.MyThrottle import Throttle
class Books(APIView):
# 局部使用
# throttle_classes = [Throttle, ]
# 局部禁用
throttle_classes = []
def get(self, request):
return Response('返回了所有图书')
3.2、自定义频率类(继承BaseThrottle,重写allow_request方法)
from rest_framework.throttling import BaseThrottle
class Throttle(BaseThrottle):
VISIT_RECORD = {}
def __init__(self):
self.history = None
# 自定义控制每分钟访问多少次,允许访问返回true,不允许访问返回false
def allow_request(self, request, view):
# (1)取出访问者的ip,{ip1:[第二次访问时间,第一次访问时间],ip2:[]}
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 = self.VISIT_RECORD.get(ip)
# (3)循环判断当前ip的列表,有值,且当前时间减去最早时间大于60s的都pop掉
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
def wait(self):
import time
ctime = time.time()
return 60 - (ctime-self.history[-1])
3.3、源码分析
来看一下SimpleRateThrottle类的源码分析
因为我们写频率控制类,继承它之后更加方便。只需要重写get_cache_key来获取用户ip之类的即可。
再看其他方法