drf框架 8 系统权限类使用 用户中心信息自查 token刷新机制 认证组件项目使用:多方式登录 权限组件项目使用:vip用户权限 频率组件 异常组件项目使用
""" 1)前后台权限管理: 后台管理:基于RBAC(auth模块的六表),用admin|xadmin来管理 前台管理:基于三大认证权限管理,认证采用的是jwt认证,jwt一般也是依赖auth模块的六表 2)认证模块: 继承BaseAuthentication,实现authenticate方法,规则: 没认证信息,返回None:游客 有认证信息,认证成功,返回(user, token):合法用户 有认证信息,认证失败,抛异常:非法用户 3)权限模块: 继承BasePermission,实现has_permission方法,规则: 权限条件判断成立,返回True:有权限 权限条件判断失败,返回Fasle:无权限 4)jwt认证规则: json web token 组成:header.payload.sign header:基础信息 payload:核心信息 - 用户主键、客户端设备信息、过期时间(token过期时间,token可刷新过期时间) sign:安全信息 - 前两段加密结果 + 服务器安全码 优势: 没有数据库写:高效 数据库不存:低耗 只是算法:集群 5)三大认证使用: i)是否要自定义三大认证类: 认证类采用第三方jwt,所以自己就不写了 权限类drf通过了一些,如果够用,直接使用,不够用会自定义 频率类系统也提供了一些,一般也会自定义 ii)配置: 局部配置:在视图类中初始化类属性:permission_classes 全局配置:在settings中自定义drf的配置:DEFAULT_AUTHENTICATION_CLASSES """
from rest_framework.permissions import IsAuthenticatedOrReadOnly class BookViewSet(ModelViewSet): # 游客只读,用户可增删改查 permission_classes = [IsAuthenticatedOrReadOnly] queryset = models.Book.objects.all() serializer_class = serializers.BookSerializer
# 用路由组件配置,形成的映射关系是 /user/center/ => list | user/center/(pk)/ => retrieve # router.register('user/center', views.UserCenterViewSet, 'center') urlpatterns = [ # ... # /user/center/ => 单查,不能走路由组件,只能自定义配置映射关系 url('^user/center/$', views.UserCenterViewSet.as_view({'get': 'user_center'})), ]
from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response class UserCenterViewSet(GenericViewSet): # 可以参考下方多方式登录,改为继承ViewSet类,减少代码,减少查询 permission_classes = [IsAuthenticated, ] queryset = models.User.objects.filter(is_active=True).all() # 根据懒查询,会查21条数据,此处不需要得到该对象列表,所以此处最好把继承GenericViewSet改为继承ViewSet类 serializer_class = serializers.UserCenterSerializer def user_center(self, request, *args, **kwargs): # request.user就是前台带token,在经过认证组件解析出来的, # 再经过权限组件IsAuthenticated的校验,所以request.user一定有值,就是当前登录用户,并且是对象 serializer = self.get_serializer(request.user) return Response(serializer.data)
""" 1)运用在像12306这样极少数安全性要求高的网站 2)第一个token由登录签发 3)之后的所有正常逻辑,前端都需要发送两次请求,第一次是刷新token的请求(老的token换成新的token,旧token在过期之前都是有效的),第二次是正常逻辑的请求 """
import datetime JWT_AUTH = { # 配置过期时间 'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=5), # 每条token只要5分钟有效期,一般设置为7天即可 # 是否可刷新 'JWT_ALLOW_REFRESH': True, # 一般情况,是否刷新和刷新过期时间配置成false # 刷新过期时间 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), # 7天内可以刷新,超过7天无法刷新,需要重新登录 }
from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken urlpatterns = [ url('^login/$', ObtainJSONWebToken.as_view()), # 登录签发token接口 类名仅作参考 url('^refresh/$', RefreshJSONWebToken.as_view()), # 刷新toekn接口 类名仅作参考 ]
# 接口:/api/refresh/ # 方法:post # 数据:{"token": "登录签发的token"}
# 自定义登录(重点):post请求 => 查操作(签发token返回给前台) - 自定义路由映射 url('^user/login/$', views.LoginViewSet.as_view({'post': 'login'})),
# 重点:自定义login,完成多方式登录 from rest_framework.viewsets import ViewSet from rest_framework.response import Response class LoginViewSet(ViewSet): # 登录接口,要取消所有的认证与权限规则,也就是要做局部禁用操作(空配置) authentication_classes = [] permission_classes = [] # 需要和mixins结合使用,继承GenericViewSet,不需要则继承ViewSet # 为什么继承视图集,不去继承工具视图或视图基类,因为视图集可以自定义路由映射: # 可以做到get映射get,get映射list,还可以做到自定义(灵活) def login(self, request, *args, **kwargs): serializer = serializers.LoginSerializer(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) # 会根据post方法默认完成create入库检验,校验唯一约束字段 token = serializer.context.get('token') return Response({"token": token})
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler # 重点:自定义login,完成多方式登录 class LoginSerializer(serializers.ModelSerializer): # 登录请求,走的是post方法,默认post方法完成的是create入库校验,所以username唯一约束字段,会进行数据库唯一校验,导致逻辑相悖 # 需要覆盖系统字段,自定义校验规则,就可以避免完成多余的不必要校验,如唯一字段校验 username = serializers.CharField() class Meta: model = models.User # 结合前台登录布局:采用账号密码登录,或手机密码登录,布局一致,所以不管账号还是手机号,都用username字段提交的 fields = ('username', 'password') def validate(self, attrs): # 在全局钩子中,才能提供提供的所需数据,整体校验得到user # 再就可以调用签发token算法(drf-jwt框架提供的),将user信息转换为token # 将token存放到context属性中,传给外键视图类使用 user = self._get_user(attrs) payload = jwt_payload_handler(user) # 参照jwt原生登录ObtainJSONWebToken类源码可获得这两个产生token的方法 获取第二段payload内容 token = jwt_encode_handler(payload) # 加密 self.context['token'] = token return attrs # 多方式登录 def _get_user(self, attrs): username = attrs.get('username') password = attrs.get('password') import re if re.match(r'^1[3-9][0-9]{9}$', username): # 手机登录 user = models.User.objects.filter(mobile=username, is_active=True).first() elif re.match(r'^.+@.+$', username): # 邮箱登录 user = models.User.objects.filter(email=username, is_active=True).first() else: # 账号登录 user = models.User.objects.filter(username=username, is_active=True).first() if user and user.check_password(password): # 有用户,并且检验密码通过 return user raise ValidationError({'user': 'user error'})
""" 1)User表创建两条数据 2)Group表创建一条数据,name叫vip 3)操作User和Group的关系表,让1号用户属于1号vip组 """
from rest_framework.permissions import BasePermission from django.contrib.auth.models import Group class IsVipUser(BasePermission): def has_permission(self, request, view): if request.user and request.user.is_authenticated: # is_authenticated必须是合法用户 try: vip_group = Group.objects.get(name='vip') if vip_group in request.user.groups.all(): # 用户可能不属于任何分组 return True # 必须是vip分组用户 except: pass return False
from .permissions import IsVipUser class CarViewSet(ModelViewSet): permission_classes = [IsVipUser] queryset = models.Car.objects.all() serializer_class = serializers.CarSerializer
class CarSerializer(serializers.ModelSerializer): class Meta: model = models.Car fields = ('name', )
router.register('cars', views.CarViewSet, 'car')
""" 1)如何自定义频率类 2)频率校验的规则 3)自定义频率类是最常见的:短信接口一分钟只能发生一条短信 """
""" 源码参考 Python\Python37\Lib\site-packages\rest_framework\throttling.py 1)自定义类继承SimpleRateThrottle (或者设定全局变量,views中直接调用,下面给案列) 2)设置类实现scope,值就是一个字符串,与settings中的DEFAULT_THROTTLE_RATES进行对应 DEFAULT_THROTTLE_RATES就是设置scope绑定的类的频率规则:1/min 就代表一分钟只能访问一次 3)重写 get_cache_key(self, request, view) 方法,指定限制条件 不满足限制条件,返回None:代表对这类请求不进行频率限制 满足限制条件,返回一个字符串(是动态的):代表对这类请求进行频率限制 短信频率限制类,返回 "throttling_%(mobile)s" % {"mobile": 实际请求来的电话} """
#1)UserRateThrottle: 限制所有用户访问频率 #2)AnonRateThrottle:只限制匿名用户访问频率
from rest_framework.throttling import SimpleRateThrottle # 只限制查接口的频率,不限制增删改的频率 class MethodRateThrottle(SimpleRateThrottle): scope = 'method' # 从setting中寻找对应限制的频率 def get_cache_key(self, request, view): # 只有对get请求进行频率限制 if request.method.lower() not in ('get', 'head', 'option'): return None # 不加以限制 # 区别不同的访问用户,之间的限制是不冲突的 if request.user.is_authenticated: # 有名用户 ident = request.user.pk else: # 匿名用户 # get_ident是BaseThrottle提供的方法,会根据请求头,区别匿名用户, # 保证不同客户端的请求都是代表一个独立的匿名用户 ident = self.get_ident(request) return self.cache_format % {'scope': self.scope, 'ident': ident} # 设定频率 # ident 唯一标识
REST_FRAMEWORK = { # ... # 频率规则配置 'DEFAULT_THROTTLE_RATES': { # 只能设置 s,m,h,d,且只需要第一个字母匹配就ok,m = min = maaa 就代表分钟 参考throttling.py中SimpleTateThrottle中parse_rate方法 'user': '3/min', # 配合drf提供的 UserRateThrottle 使用,限制所有用户访问频率 'anon': '3/min', # 配合drf提供的 AnonRateThrottle 使用,只限制匿名用户访问频率 'method': '3/min', }, }
from .permissions import IsVipUser from .throttles import MethodRateThrottle class CarViewSet(ModelViewSet): permission_classes = [IsVipUser] throttle_classes = [MethodRateThrottle] queryset = models.Car.objects.all() serializer_class = serializers.CarSerializer
使用内置频率组件
# setting.py REST_FRAMEWORK = { # 频率规则配置 'DEFAULT_THROTTLE_RATES': { # 只能设置 s,m,h,d,且只需要第一个字母匹配就ok,m = min = maaa 就代表分钟 'user': '3/min', # 配合drf提供的 UserRateThrottle 使用,限制所有用户访问频率 'anon': '3/min', # 配合drf提供的 AnonRateThrottle 使用,只限制匿名用户访问频率 }, } # views.py from rest_framework.throttling import AnonRateThrottle,UserRateThrottle class BookViewSet(ModelViewSet): queryset = models.Book.objects.all() serializer_class = serializers.BookSerializer permission_classes = [IsAuthenticatedOrReadOnly] # 配置权限校验,所有用户 # throttle_classes = [AnonRateThrottle] # 针对匿名用户 throttle_classes = [UserRateThrottle] # 对所有用户进行限制
from rest_framework.views import exception_handler as drf_exception_handler from rest_framework.response import Response def exception_handler(exc, context): # 重写drf内部异常捕获方法,追加无法处理的异常处理 # 只处理客户端异常,不处理服务器异常, # 如果是客户端异常,response就是可以直接返回给前台的Response对象 response = drf_exception_handler(exc, context) if response is None: # 没有处理的服务器异常,处理一下 # 其实给前台返回 服务器异常 几个字就行了 # 那我们处理异常模块的目的是 不管任何错误,都有必要进行日志记录(线上项目只能通过记录的日志查看出现过的错误) response = Response({'detail': '%s' % exc}) # 需要结合日志模块进行日志记录的:项目中讲 return response
REST_FRAMEWORK = { # ... # 异常模块 # 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', # 原来的,只处理客户端异常 'EXCEPTION_HANDLER': 'api.exception.exception_handler', }
示例:
# meiduo_market/settings/dev.py中logging的配置 LOGGING = { 'version': 1, 'disable_existing_loggers': False, # 是否禁用已存在的日志器(像django自带的输出) 'formatters': { # 日志信息显示的格式 'verbose': { # 输出格式 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' }, 'simple': { # 输出格式 'format': '%(levelname)s %(module)s %(lineno)d %(message)s' }, }, 'filters': { # 日志过滤器 'require_debug_true': { # django在debug情况下才输出日志 '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { # 日志处理器 'console': { # 向终端输出日志 # 实际开发建议使用WARNING 'level': 'DEBUG', 'filters': ['require_debug_true'], # 只有debug为true才输出 'class': 'logging.StreamHandler', 'formatter': 'simple' # 输出格式 }, 'file': { # 向文件输出 # 实际开发建议使用ERROR 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi 'filename': os.path.join(os.path.dirname(BASE_DIR), "logs", "meiduo.log"), # 日志文件的最大值,这里我们设置300M 'maxBytes': 300 * 1024 * 1024, # 日志文件的数量,设置最大日志数量为10 'backupCount': 10, # 超过会删除 # 日志格式:详细格式 'formatter': 'verbose', # 文件内容编码 'encoding': 'utf-8' }, }, # 日志对象 'loggers': { # 日志器 'django': { # 定义一个名为django的日志器 'handlers': ['console', 'file'], 'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统 }, } }
# meiduo_mall/utils/exceptions.py from rest_framework.views import exception_handler as drf_exception_handler import logging from django.db import DatabaseError from redis.exceptions import RedisError from rest_framework.response import Response from rest_framework import status # 获取在配置文件中定义的logger,用来记录日志 logger = logging.getLogger('django') def exception_handler(exc,context): ''' 自定义异常处理 :param exc: 异常示例对象 :param context: 抛出异常的上下文(包含request和view对象) :return: Response响应对象 ''' # 调用drf框架原生的异常处理方法 response = drf_exception_handler(exc,context) if response is None: view = context['view'] if isinstance(exc,DatabaseError) or isinstance(exc,RedisError): # 判断该错误是否是数据库错误,还是redis错误 # 数据库异常 logger.error('[%s] %s'% (view,exc)) response = Response({'message':'服务器内部错误'},status=status.HTTP_507_INSUFFICIENT_STORAGE) return response
# meiduo_mall_settings/dev.py 追加下方配置 REST_FRAMEWORK = { # 异常处理 'EXCEPTION_HANDLER':'meiduo_mall.utils.exceptions.exception_handler', }
""" 1)认证模块:多方式登录 2)权限组件:vip权限认证 3)频率组件:请求方式的频率限制 4)异常组件:目的是记录异常日志 drf整体总结: 1)APIView生命周期:请求解析、响应渲染、异常响应(日志) 2)序列化组件 3)视图家族 4)三大认证 5)群查:搜索、排序、分页、分类、区间 """