drf之认证、权限、频率

发布时间 2023-12-27 21:55:40作者: jntmwl

认证组件

需求:

有的接口需要登录后才能访问,有的接口,不登录就能访问——这就是登录认证的限制

简单实现的方式:写一个登录接口,返回token,以后只要带着token过来,就是登录了,不带,就没有登录。条件如下:

  • 查询所有不需要登录就能访问
  • 查询单个,需要登录才能访问

登录接口

# 认证是基于登录的接口上面操作的 所以前戏编写一个简单的登录接口

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、需要写一个认证类,因此我们需要在应用中另外创建一个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、测试路由参考

整体代码

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

]

认证时cookie的获取方式

  • 当前端从地址栏中返回cookie时,就跟我们代码中返回token是一样的,获取方式:
request.query_params.get('token')
  • 原生djangozhong ,取出前端传入cookie(即前端直接传入cookie),从哪取的?
request.COOKIE.get('sessionid')
  • 后期如果想从请求头中取cookie
request.META.get('HTTP_TOKEN')

权限组件

需求分析:

在一些软件中即便我们登陆成功了有些接口,还是不能访问,因为没有权限。

这里我们的需求是:

	-查询单个需要超级管理员才能访问
    -查询所有,所有登录用户都能访问

而我们的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)

权限的使用

  • 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 = [] 

代码

整体部分不需要修改,跟认证组件末尾的代码一样,需要修改的代码如下:

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的访问次数

使用步骤

  • 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 = [] 

代码

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]

权限组件源码分析

# 0 目标
	1 为什么写一个类继承BasePermission,重写has_permission
    	-可以不继承这个类,只重写这个方法也可以
    2 权限类中 self.message  会返回给前端
    3 局部配置:放在视图类中:permission_classes = [CommonPermission]
    4 全局配置,配置在配置文件中也可以--》视图类中就没有
    	就会使用:permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
        优先从你项目的配置文件配置的DEFAULT_PERMISSION_CLASSES取,如果没有
        就会去drf的配置文件DEFAULT_PERMISSION_CLASSES取
# 1 从哪里开始找?
	-继承APIView后,权限是在执行视图类的方法之前执行的
    -执行视图类的方法---》dispath中
        def dispatch(self, request, *args, **kwargs):

        try:
            # 三大认证:认证,权限,频率
            self.initial(request, *args, **kwargs)
            ####执行视图类的方法开始###
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            response = handler(request, *args, **kwargs)
            ####执行视图类的方法结束###
        except Exception as exc:
            response = self.handle_exception(exc)
        return self.response
    
#2 找到APIView的initial
    def initial(self, request, *args, **kwargs):
        self.perform_authentication(request)#认证
        self.check_permissions(request)#权限 
        self.check_throttles(request)#频率
        
# 3 self.check_permissions(request)---APIView
    def check_permissions(self, request):
        # self是 视图类的对象
        #self.get_permissions [CommonPermission(),]--->咱们配置的一个个权限类的对象,放到列表中
        # permission 权限类的对象
        for permission in self.get_permissions():
            # 咱们写的权限类,要重写has_permission,传了俩参数
            # 参数:request 是新的request
            # 参数:self 是?视图类的对象,就是咱么在视图类中写的self,它里面有
            # request和action等
            if not permission.has_permission(request, self):
                # 视图类的对象,没有权限
                self.permission_denied(
                    request,
                    # 从权限类的对象中反射了message,就是写的给前端看的文字
                    message=getattr(permission, 'message', None),
                    #响应状态码
                    code=getattr(permission, 'code', None)
                )
                
# 4 self.get_permissions() ---> APIView的
    def get_permissions(self):
        # 咱么视图类上配置的 permission_classes = [CommonPermission]
        # 返回值是 [CommonPermission(),]--->咱们配置的一个个权限类的对象,放到列表中
        return [permission() for permission in self.permission_classes]
    
    
# 5 self.permission_denied  --》APIView中
  def permission_denied(self, request, message=None, code=None):
        if request.authenticators and not request.successful_authenticator:
            raise exceptions.NotAuthenticated()
        # 抛了异常,会被全局异常捕获,detai会放在响应体中,code会放在响应状态码中
        raise exceptions.PermissionDenied(detail=message, code=code)
        

认证类源码

-继承APIView后,权限是在执行视图类的方法之前执行的
    -执行视图类的方法---》dispath中
        def dispatch(self, request, *args, **kwargs):

        try:
            # 三大认证:认证,权限,频率
            self.initial(request, *args, **kwargs)
            ####执行视图类的方法开始###
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            response = handler(request, *args, **kwargs)
            ####执行视图类的方法结束###
        except Exception as exc:
            response = self.handle_exception(exc)
        return self.response
    
#2 找到APIView的initial
    def initial(self, request, *args, **kwargs):
        self.perform_authentication(request)#认证
        self.check_permissions(request)#权限 
        self.check_throttles(request)#频率
        
# 3 self.perform_authentication(request)
  def perform_authentication(self, request):
        request.user # 新的request对象
        
        
# 4 Request类中找 user 方法包装成了数据属性
	from rest_framework.request import Request
    @property
    def user(self):
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                # self是 新的request对象
                self._authenticate() # 一开始没有,就走了self._authenticate()
        return self._user
    
    
# 5  Request类中找_authenticate()
    def _authenticate(self):
        # 拿出你配置在视图类上一个个认证类的对象  LoginAuth()
        # authenticator就是LoginAuth()
        for authenticator in self.authenticators:
            try:
                # 为什么咱么要重写 authenticate
                #self是谁?request就是认证类的 def authenticate(self, request):
                # 正常返回两个值:return user, user_token
                # 如果抛了异常:AuthenticationFailed--》捕获了APIException
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                self._not_authenticated()
                raise
            if user_auth_tuple is not None:#正常返回了两个值
                self._authenticator = authenticator
                # 解压赋值:self是request
                #后续在视图类中 request.user 就是当前登录用户
                self.user, self.auth = user_auth_tuple
                return
        self._not_authenticated()
        
        
# 6 咱们视图类中得认证类可以配置多个
	-如果 第一个认证类,就返回了两个值
    -后续的认证类就不走了
    -如果认证类中要返回两个值,视图类中配了多个---》把返回两个值的放在最后
    -返回的两个值,第一个给了request.user,第一个给了request.auth,后续视图类中可以取出来
    
    
# 7 self.authenticators ---》 Request中
# Request类实例化得到对象,传入authenticators 最终给了
def __init__(self, request, parsers=None, authenticators=None,negotiator=None, parser_context=None):
 	self.authenticators = authenticators or ()
    
    
# 8 哪里对Request类实例化了? APIView中
APIView 中dispatch中---》包装了新的request
request = self.initialize_request(request, *args, **kwargs)
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
        )
        
        
# 9 self.get_authenticators()  在APIView中 self是视图类的对象
    def get_authenticators(self):
        return [auth() for auth in self.authentication_classes]
    
    
# 总结:	
1 写一个类,重写authenticate
2 校验失败抛异常--》捕获
3 通过返回两个值  当前登录用户,token
	不返回两个值--》后续的request.user 不是当前登录用户
4 视图类上局部配置和配置文件全局配置跟 权限一模一样