认证、权限、频率

数据准备

在前面说的 APIView 中封装了三大认证,分别为认证、权限、频率。认证即登录认证,权限表示该用户是否有权限访问接口,频率表示用户指定时间内能访问接口的次数

为了方便举例说明,事先定义好模型表

from django.db import models


# 图书跟作者:多对多,需要建立中间表,但是我们可以通过ManyToManyField自动生成,写在哪里都行
# 图书跟出版社:一对多,一个出版社,出版多本书,关联字段写在多的一方,写在Book
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


class Publish(models.Model):
    name = models.CharField(max_length=32)
    addr = models.CharField(max_length=32)


class Author(models.Model):
    name = models.CharField(max_length=32)
    phone = models.CharField(max_length=11)

# User与UserToken一对一关联,UserToken表用于储存用户登录的Token,进行相关操作需要带着token过来才能登录,不带就不能登录
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    user_type = models.IntegerField(choices=((1,'超级管理员'),(2,'普通管理员'),(3,'游客')))


class UserToken(models.Model):
    token = models.CharField(max_length=64)
    user = models.OneToOneField(to='User',on_delete=models.CASCADE, null=True)
 

认证

登录接口

自定义登录认证可以生成随机字符串,并添加进 UserToken 表中,每一次登录都会生成,若该字符串已存在则更新,不存在则新建。

# views.py

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

class UserView(ViewSet):
    @action(methods=['POST'], detail=False)
    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())     # 生成一个随机字符串
            # 在userToken表中存储一下:
       # 如果查找不到相关数据,则插入一条,能查到则按照defaults修改记录
UserToken.objects.update_or_create(user_id=user.pk, defaults={'token':token}) return Response({'code': '100', 'msg': '登录成功'}) else: return Response({'code':'101','msg':'用户名或密码错误'})

# urls.py

from app01 import views
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('user',views.UserView,'user') 

urlpatterns = [
    path(r'api/v1/',include(router.urls))
]

自定义认证

自定义认证表需要创建认证类,首先继承拓展 BaseAuthentication
导入语句:from rest_framework.authentication import BaseAuthentication

自定义认证类

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed  # 异常

class MyAuthentication(BaseAuthentication):
	# 重写 BaseAuthentication 中的 authenticate 方法
    def authenticate(self, request):
    	# 在请求头中获取用户登录的 token 字符串
        token = request.query_params.get('token')
        # 判断该字符串是否存在
        user_token = UserToken.objects.filter(token=token).first()
        if user_token:
        	# 返回的第一个值是当前登录用户,第二个值是 token
            return user_token.user, token
        else:
        	# 若不存在则报 AuthenticationFailed 异常
            raise AuthenticationFailed('请先登录')

全局使用

在配置文件中添加配置,DEFAULT_AUTHENTICATION_CLASSES 的值为列表,其包含认证类路径

REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.auth.MyAuthentication",]
}

局部使用

局部使用,只需要在视图类里加入:

authentication_classes = [MyAuthentication, ]

局部禁用

可以选择某一些方法可以认证,在视图类中添加 get_authenticators

authentication_classes = [ ]

代码示例

  • 查询所有不需要登录就能访问
  • 查询单个,需要登录才能访问
# 查询所有(不需要登录)
class BookView(ViewSetMixin,ListAPIView):
    queryset = Book.objects.all()
    serializer_class = Bookserializer
    authentication_classes = []

# 查询单个(需要登录)
class BookDetail(ViewSetMixin,RetrieveAPIView):
    queryset = Book.objects.all()
    serializer_class = Bookserializer
    authentication_classes = [MyAuthentication] 

总结流程

  • 写一个认证类,继承 BaseAuthentication
  • 重写 authenticate 方法,方法中编写逻辑,实现登录认证
  • 认证成功需要返回两个值,为登录用户和 token,
  • 认证不通过,抛 AuthenticationFailed 认证异常
  • 编写好认证类可以在局部使用,也可以在全局使用

内置认证类

-SessionAuthentication   之前老的 session 认证登录方式用,后期不用
-BasicAuthentication  	 基本认证方式
-TokenAuthentication  	 使用 token 认证方式,也可以自己写
 

可以在配置文件中配置全局默认的认证方案

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',  # session认证
        'rest_framework.authentication.BasicAuthentication',   # 基本认证
    )
}
 

也可以在每个视图中通过设置authentication_classess属性来设置

from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.views import APIView

class ExampleView(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    ...
 

权限

自定义权限

登录认证成功后,还需要认证权限,有一些接口需要指定权限才能访问。所以权限需要和登录认证相关联。每个人的权限在表中默认设为普通用户。

自定义权限需要继承 BasePermission 编写权限类
导入语句:from rest_framework.permissions import BasePermission

自定义权限类

from rest_framework.permissions import BasePermission
from .models import UserToken, MyUser

class MyPermission(BasePermission):
	# message 为认证失败提示信息
    message = ''

	# 需要重写 has_permission 方法,原方法默认返回 True
    def has_permission(self, request, view):
    	# 获取当前登录用户
        user = request.user
        # 获取当前用户权限类型
        user_type = user.user_type
        if user_type == 1:
        	# 权限符合返回 True
            return True
        else:
        	# 权限不符合,添加提示信息,并返回 False
            self.message = '你是: %s,权限不够' % user.get_user_type_display()
            return False
 补充:如果表模型中是choice,就可以通过 get_字段名_display() 拿到choice对应的中文
 

全局使用

全局使用也是在配置文件中添加

REST_FRAMEWORK={
    "DEFAULT_AUTHENTICATION_CLASSES":["app01.auth.MyAuthentication",],
    "DEFAULT_PERMISSION_CLASSES":["app01.auth.MyPermission",]
}
 

局部使用

局部使用,只需要在视图类里加入该权限类即可:

permission_classes = [MyPermission,]
 

选择使用

在视图类中添加 get_permissions 判断如果请求方式符合就去认证

permission_classes = [ ]
 

总结流程

  • 写一个权限类,继承 BasePermission
  • 重写 has_permission 方法,方法中编写逻辑,实现权限认证,在这方法中,request.user就是当前登录用户
  • 有权限返回 True,没有权限可以添加 message 并返回 False
  • 编写好权限类可以在局部使用,也可以在全局使用

内置权限类

from rest_framework.permissions import AllowAny,IsAuthenticated,IsAdminUser,IsAuthenticatedOrReadOnly

-AllowAny 					允许所有用户
-IsAdminUser  				校验是不是 auth 的超级管理员权限
-IsAuthenticated 			后面用,验证用户是否登录,登录后才有权限,没登录就没有权限
-IsAuthenticatedOrReadOnly 	了解即可
 

全局使用

可以在配置文件中全局设置默认的权限管理类,如下

REST_FRAMEWORK = {
    ....
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}
 

如果未指明,则采用如下默认配置

'DEFAULT_PERMISSION_CLASSES': (
   'rest_framework.permissions.AllowAny',
)
 

局部使用

也可以在具体的视图中通过 permission_classes 属性来设置,如下

from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

class ExampleView(APIView):
    permission_classes = (IsAuthenticated,)
    ...
 

频率

SimpleRateThrottle 内置频率类

from rest_framework.throttling import SimpleRateThrottle

class MyThrottle(SimpleRateThrottle):
  # 该属性作为键名在 setting 配置文件中使用
    scope = 'count_time'

  # 重写 get_cache_key 方法,该方法返回的值会被作为限制的依据
    def get_cache_key(self, request, view):
        return request.META.get('REMOTE_ADDR')  # 获取请求头中的IP地址

setting 配置

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_RATES": {
        # 频率类中scope对应的值
		'count_time': '3/m',  # 数字/s m h  d
    },
}
 

全局使用

REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_RATES": {
        # 频率类中scope对应的值
        'count_time': '3/m',  # 数字/s m h  d
    },
    'DEFAULT_THROTTLE_CLASSES': ['app01.auth.MyThrottle', ]
}
 

 局部使用

在视图类中添加,同样,局部禁用只要赋值空列表即可

throttle_classes = [MyThrottle, ]
 

 局部禁用

在视图类中添加,同样,局部禁用只要赋值空列表即可

throttle_classes = [ ]

总结流程

  • 继承 SimpleRateThrottle
  • 重写 get_cache_key 方法,该方法返回的值是限制的依据,例如按照 IP 地址来作为限制条件,设置每个 IP 地址访问的次数。需要注意的是,IP 地址在 request.META 中获取 'REMOTE_ADDR'
  • 接下来需要配置 setting ,需要设置访问的频率。首先需要设置属性 scope,该属性的值会作为频率的键名,在 setting 配置文件 REST_FRAMEWORK 中的 DEFAULT_THROTTLE_RATES 配置,键名是 scope,键值是字符串,格式为 'x/y',x 表示访问的次数,y 表示访问的时间区间(可以为 s(秒)、m(份)、h(时)、d(天)
  • 编写好频率类可以在局部使用,也可以在全局使用

AnonRateThrottle 内置频率类

AnonRateThrottle 内置频率类的功能:对于登录用户不限制次数,只未登录用户限制次数,限制的次数需要在配置文件中配置。使用也支持全局和局部

配置文件

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '3/m',
    }
}
 

 UserRateThrottle 内置频率类

UserRateThrottle 内置频率类的功能:限制登录用户的频率,限制的次数需要在配置文件中配置。也支持全局和局部使用

配置文件

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.UserRateThrottle'
    ),
    'DEFAULT_THROTTLE_RATES': {
        'user': '10/m'
    }
}
 

 自定义频率类

# 自定义的逻辑
#(1)取出访问者ip
#(2)判断当前ip不在访问字典里,添加进去,并且直接返回True,表示第一次访问,在字典里,继续往下走
#(3)循环判断当前ip的列表,有值,并且当前时间减去列表的最后一个时间大于60s,把这种数据pop掉,这样列表中只有60s以内的访问时间,
#(4)判断,当列表小于3,说明一分钟以内访问不足三次,把当前时间插入到列表第一个位置,返回True,顺利通过
#(5)当大于等于3,说明一分钟内访问超过三次,返回False验证失败
class MyThrottles():
    VISIT_RECORD = {}
    def __init__(self):
        self.history=None
    def allow_request(self,request, view):
        #(1)取出访问者ip
        # print(request.META)
        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掉,这样列表中只有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
    def wait(self):
        import time
        ctime=time.time()
        return 60-(ctime-self.history[-1])
 

其他

  • AnonRateThrottle
    限制所有匿名未认证用户,使用 IP 区分用户。
    使用 DEFAULT_THROTTLE_RATES[‘anon’] 来设置频次

  • UserRateThrottle
    限制认证用户,使用 User id 来区分。
    使用 DEFAULT_THROTTLE_RATES[‘user’] 来设置频次

  • ScopedRateThrottle
    限制用户对于每个视图的访问频次,使用 ipuser id

认证源码分析

认证顺序源码

三大认证的顺序是:登录 ----> 权限 ----> 频率 。
已知的是三大认证是在 APIView 中封装,其源码如下

dispatch

def dispatch(self, request, *args, **kwargs):
	...
    try:
    	# 三大认证
        self.initial(request, *args, **kwargs)
        ...
        
    except Exception as exc:
        ...

    ...
 

initial

def initial(self, request, *args, **kwargs):
	...
	# 认证
    self.perform_authentication(request)
    # 权限
    self.check_permissions(request)
    # 频率
    self.check_throttles(request)
 

在代码中由上而下执行,因此有了三大认证的顺序

局部认证分析

局部使用三大认证是在视图类中使用,例如 authentication_classes、permission_classes、throttle_classes,要想知道如何在视图类中配置就可以进行认证的可以分析其源码。


  • self.perform_authentication(request)
def perform_authentication(self, request):
    request.user
 
  1. 其返回的值是 request.user,这里的 request 是 APIView 重新分装过的新 request
  2. rest_framework.request Request 中查看 Request 源码,发现其有一个名为 user 的方法,如下所示

  • user
class Request:
	...
	@property
	def user(self):
	    if not hasattr(self, '_user'):
	        with wrap_attributeerrors():
	            self._authenticate()
	    return self._user
 
  1. 该方法使用了 property 装饰器,将 user 方法装饰成一个属性。
  2. 方法中进行了 if 判断,由于开始并没有 _user,所以会执行 _authenticate 方法
  3. 方法中的 self 表示的是 Request 的对象,执行的是 Request 的 _authenticate 方法

  • _authenticate
class Request:
    def _authenticate(self):
        for authenticator in self.authenticators:
            try:
                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.auth = user_auth_tuple
                return

        self._not_authenticated()
 
  1. for authenticator in self.authenticators 语句中的 self.authenticators 是我们在视图类上添加的认证类,但是是以: [认证类(), 认证类2()] 的形式存在。所以 authenticator 表示的是 认证类()
  2. user_auth_tuple = authenticator.authenticate(self) 调用认证类中的 authenticate 方法,返回的元组(一个是登录用户、一个是 token)用 user_auth_tuple 接收。
  3. 调用 authenticate 方法了但是需要注意的是总共传了俩个参数,该方法时绑定给对象的,会自动传入认证类本身,还有一个是 self,也就是 Request类的对象
  4. except exceptions.APIException: 捕获的是 APIException,我们抛出的是 AuthenticationFailed,但是由于其继承了 APIException,相当于也捕获了。
  5. self.user, self.auth = user_auth_tuple 如果返回了两个值,第一个值给了 request.user ,第二个值给了 request.auth。因此认证过后 request.user 会有当前用户。
  6. 留了一个点是为什么 self.authenticators 就是我们写的认证类并且还加了括号。

  • _init_
class Request:
    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        ...
        self.authenticators = authenticators or ()
 
  1. self.authenticators 是 Reqeust 在类实例化的时候传入的,如果不传就是空元组
  2. Request 的实例化也就是 dispatch 方法中包装了新的 Request
  • dispatch
request = self.initialize_request(request, *args, **kwargs)
 

  • initialize_request
class APIView(View):
	...
    def initialize_request(self, request, *args, **kwargs):
        """
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )
 
  1. authenticators=self.get_authenticators() 由 get_authenticators() 赋值

  • get_authenticators()
class APIView(View):
	...
    def get_permissions(self):
        return [permission() for permission in self.permission_classes]
 
  1. 在这里通过列表生成式,从 self.permission_classes 也就是我们在类中编写的认证类列表(查找顺序)中获取认证类并加括号调用。

权限源码分析

同样的查看在 dispatch 中 initial 方法里的 check_permissions 源码


check_permissions

class APIView(View):
	...
    def check_permissions(self, request):
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )
 
  1. for permission in self.get_permissions(): permission 表示的是权限类对象
  2. if not permission.has_permission(request, self): 判断 has_permission 方法返回的是 True 还是 False,如果为 False 则执行 permission_denied 方法报异常
  3. message=getattr(permission, 'message', None), 获取在视图类中编写的 message

get_permissions

def get_permissions(self):
    return [permission() for permission in self.permission_classes]
 

permission_denied

def permission_denied(self, request, message=None, code=None):
    if request.authenticators and not request.successful_authenticator:
        raise exceptions.NotAuthenticated()
    raise exceptions.PermissionDenied(detail=message, code=code)
 

频率源码分析

查看 check_throttles 源码


check_throttles

def check_throttles(self, request):
    throttle_durations = []
    for throttle in self.get_throttles():
        if not throttle.allow_request(request, self):
            throttle_durations.append(throttle.wait())

    if throttle_durations:
        durations = [
            duration for duration in throttle_durations
            if duration is not None
        ]

        duration = max(durations, default=None)
        self.throttled(request, duration)
 
  1. for throttle in self.get_throttles(): throttle 表示的是频率类对象,get_throttles() 表示的是频率类对象列表。
  2. if not throttle.allow_request(request, self): 获取对象的 allow_request 方法,返回 True 就是没有频率限制住,返回 False 就是被频率限制了

我们可以查看 SimpleRateThrottleallow_request 方法


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()

    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()
 
  1. if self.rate is None:rate 进行判断,rate 属性在其双下 _init_ 方法中实现

_init_

def __init__(self):
    if not getattr(self, 'rate', None):
		# 没有该属性调用 get_rate
        self.rate = self.get_rate()
    self.num_requests, self.duration = self.parse_rate(self.rate)
 
  1. 查看 get_rate 源码

get_rate

def get_rate(self):
    if not getattr(self, 'scope', None):
        msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
               self.__class__.__name__)
        raise ImproperlyConfigured(msg)

    try:
        return self.THROTTLE_RATES[self.scope]
    except KeyError:
        msg = "No default throttle rate set for '%s' scope" % self.scope
        raise ImproperlyConfigured(msg)
 
  1. get_rate 方法中获取 scope 若是没有写则直接抛出异常。
  2. 若有则去 THROTTLE_RATES 中取出 scope,THROTTLE_RATES 源码如下
THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
 
  1. 也就是说 rate 的值就是 scope 对应的值,类似于 5/m 表示频率。

parse_rate

def parse_rate(self, rate):
    if rate is None:
        return (None, None)

    num, period = rate.split('/')
    num_requests = int(num)
    duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
    return (num_requests, duration)
 
  1. num, period = rate.split('/')rate 进行切割,这里我们假设频率为 5/m,那么切割后的 num 为 5、period 为 m
  2. duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]] [period[0]] 只取出了第一个值然后去字典中取值,也就是说,定义 scope 的时候不一定要写 m,只要以 m 开头即可。
  3. 最后将 (num_requests, duration) 返回出去,也就是说,赋值给了 _init_ 中的 self.num_requests, self.duration

回到 allow_request 方法

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()

    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()
 
  1. self.key = self.get_cache_key(request, view) 调用 get_cache_key 方法,该方法需要自己重写的,用于指定判断的依据。
  2. self.history = self.cache.get(self.key, []) 从缓存中拿数据,取出的数据是时间的列表,类似于 [时间2, 时间1],没有则赋值空列表
  3. self.now = self.timer() timer() 是类属性,加括号调用了,获取时间。源码如下所示
timer = time.time
 
  1. self.history.pop() 把所有超过时间的数据都剔除,self.history 只剩限定时间内的访问时间
  2. if len(self.history) >= self.num_requests: 大于等于配置的次数执行 throttle_failure 返回 False,否则执行 throttle_success 把当前时间插入,并返回 True

 

posted @ 2023-02-07 20:43  莫~慌  阅读(39)  评论(0编辑  收藏  举报