(day77)认证组件、权限组件、频率组件、多方式登录(自定义签发token)

一、三大认证流程图

二、认证组件

  1. 在请求的时候,认证组件被触发
  2. 主要用于身份认证,用于校验用户:游客、合法用户、非法用户
  3. 一般使用jwt认证,只需要全局设置的REST_FRAMEWORK中配置好DEFAULT_AUTHENTICATION_CLASSES的JSONWebTokenAuthentication认证类即可

(一)内置认证组件

(1)BasicAuthentication类(源码分析)

BasicAuthentication是drf提供的认证基类

  1. 前端在请求头中使用authorization携带token给后台,后端封装成HTTP_AUTHORIZATION
  2. 前端传的token值格式为:basic token值(可以起到反爬的作用)
  3. 未提交token,返回None,代表游客(匿名用户)
  4. 提交的token结构不对,抛异常,代表非法用户
  5. 反解token失败,抛异常,代表非法用户
  6. 提交token,解析成功,返回元组(user,token),代表合法用户
    • user会被存储到request.user
    • token存储到request.auth中,通常也可以不用保存,所以可以用None填充
# authentication.py
def get_authorization_header(request):
    # 1. 前端在请求头中使用authorization携带token给后台,后端封装成HTTP_AUTHORIZATION
    auth = request.META.get('HTTP_AUTHORIZATION', b'')  # auth为token值
    if isinstance(auth, str):
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth

class BasicAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 2.1 推测token值以空格分隔
        auth = get_authorization_header(request).split()  
        # 2.2 推测token值得结构:开头为basic
        if not auth or auth[0].lower() != b'basic':
            return None  # 3. 没有token,返回None,代表游客(匿名用户)
        # 4. 提交的token结构不对,抛异常,代表非法用户
        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)
        
        # 5. 反解token失败,抛异常,代表非法用户
        try:
            auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
        except (TypeError, UnicodeDecodeError, binascii.Error):
            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
            raise exceptions.AuthenticationFailed(msg)

        userid, password = auth_parts[0], auth_parts[2]
        return self.authenticate_credentials(userid, password, request)
    
    def authenticate_credentials(self, userid, password, request=None):
        # 6. 提交token,解析成功,返回元组(user,token),代表合法用户
        return (user, None) 

(2)SessionAuthentication类

  1. drf提供的用于session认证的认证组件
  2. 继承BasicAuthentication类

(二)第三方认证组件

  1. JSONWebTokenAuthentication认证类:djangorestframework-jwt模块提供的第三方认证类
  2. JSONWebTokenAuthentication继承BaseJSONWebTokenAuthentication类,BaseJSONWebTokenAuthentication类继承了BaseAuthentication类,在其基础上自定义了自己authenticate方法
# rest_framework_jwt/authentication.py
class BaseJSONWebTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 1. 从请求中获取token
        jwt_value = self.get_jwt_value(request)
        # 2. 没有token,返回游客
        if jwt_value is None:
            return None
        
        # 3. 反解token,失败,抛异常,非法用户;成功反解的数据就是载载荷,存放在payload中
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()
        # 4. 载荷校验得到登录用户
        user = self.authenticate_credentials(payload)
        # 5.得到登录用户,返回(user,token)
        return (user, jwt_value)

(三)自定义认证类

(1)自定义认证类场景

  1. 如果是session认证,drf提供了SessionAuthenication认证类
  2. 如果使用jwt认证,djangorestframework_jwt模块提供了JSONWebTokenAuthentication认证类
  3. 如果是需要自定义签发和校验token,才需要把校验token的算法封装到自定义的认证类中

(2)自定义认证类方法

  1. 认证类中定义authenticate方法:
    1. 从请求头中拿到前端提交的token(一般HTTP_AUTHOTIZATION中拿)
    2. 如果设置了反爬等措施,校验反爬(格式:头 token值
    3. 没有token,返回None,代表游客
    4. 有token,进入校验
      • 不通过,抛出AuthenticationFailed异常,代表非法用户
      • 通过,返回(user,token)代表合法用户
    5. 如果自定义签发和校验token,需要将校验token的算法封装到自定义的认证类
from rest_framework.authentication import BaseAuthentication
class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        pass

(四)配置认证组件

(1)局部配置

  1. 在视图类中配置authentication_classes参数(列表形式)
  2. 认证组件只能决定request.user,不是断定权限的地方,所以一般配置全局
# views.py
from rest_framework.viewsets import ModelViewSet
class UserModelViewSet(ModelViewSet):
    # 配置自定义认证类(需求小)
    from api import authentications
    authentication_classes = [authentications.MyAuthentication]
    # 配置drf-jwt框架的认证类(需求大) => 配置还可以在全局
    from rest_framework_jwt.authentication import JSONWebTokenAuthentication
    authentication_classes = [JSONWebTokenAuthentication]
    pass

(2)全局配置

  1. drf中默认配置认证组件为SessionAuthentication和BasicAuthentication,即默认使用session认证
  2. 可以在settings中的REST_FRAMEWORK中通过插拔式设置DEFAULT_AUTHENTICATION_CLASSES的值
# settings.py
REST_FRAMEWORK = {
    # 认证组件,实际使用中,无需注释SessionAuthentication和BasicAuthentication
    'DEFAULT_AUTHENTICATION_CLASSES': [
        # 'rest_framework.authentication.SessionAuthentication',
        # 'rest_framework.authentication.BasicAuthentication'
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication'
    ],
}

二、权限组件

(一)内置权限类

  1. AllowAny:游客和登录用户有全权限,默认
  2. IsAuthenticated:只有登录用户有全权限
  3. IsAdminUser:只有后台用户(is_staff值是否为True)有全权限
  4. IsAuthenticatedOrReadOnly:游客有只读权限,登录用户有全权限

(二)自定义权限组件

  • 当需要一些特殊需求时,可以通过自定义权限类实现,比如:只有超级用户有权限、只有VIP用户有权限,只有某IP网段有权限、

  • 自定义权限类只需自定义权限类,重写has_permission方法

    1. 根据需求,reqeust和view的辅助,制定权限规则判断条件
    2. 条件通过返回True
    3. 条件不通过返回False
    # api/permissions.py
    class VIPUserPermission(BasePermission):
        def has_permission(self, request, view):
            """        
            1. 根据需求,reqeust和view的辅助,制定权限规则判断条件
            2. 条件通过返回True
            3. 条件不通过返回False
            """
            for group in request.user.groups.all():
                if group.name.lower() == 'vip':
                    return True
            return False

(三)配置权限组件

  1. 默认权限配置为不限制(AllowAny),实际开发中,视图类的访问权限不尽相同,因此一般局部配置
  2. 局部配置:在视图类中配置permission_classes参数(列表形式)
# views.py
from rest_framework.viewsets import ModelViewSet
class UserModelViewSet(ModelViewSet):
    # 配置自定义权限类(有需求)
    from api import permissions
    permission_classes = [permissions.MyPermission]
    # 配置drf自带的权限类(有需求)
    from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny, IsAuthenticatedOrReadOnly
    permission_classes = [IsAuthenticated]
    # permission_classes = [IsAdminUser]

三、频率组件

(一)内置频率组件

(1)AnonRateThrottle

  1. 只对游客进行限制
  2. 有名占位:%(scope)s,传值时格式为%{'scope':123},必须为字典
  3. ident:限制对象的唯一标识
# rest_framework/throttle.py
class SimpleRateThrottle(BaseThrottle):
    cache_format = 'throttle_%(scope)s_%(ident)s'  # 有名占位
    
    
class AnonRateThrottle(SimpleRateThrottle):
    scope = 'anon'
    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            return None  # 不限制

        return self.cache_format % {
            'scope': self.scope,
            'ident': self.get_ident(request)
        }

(2)UserRateThrottle

  1. 对所有用户进行频率限制(包括游客和登录用户)
  2. ident:限制对象的唯一标识
class UserRateThrottle(SimpleRateThrottle):
    scope = 'user'

    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            ident = request.user.pk
        else:
            ident = self.get_ident(request)

        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

(二)自定义频率组件

  • 如果需要其他需求,可以自定义频率限制,比如:对IP进行限次、对电话进行限次或对视图某些信息进行限次

  • 新建throttle.py文件,自定义频率类
    1. 设置scope字符串类属性,同时在settings中配置DEFAULT_THROTTLE_RATES:DEFAULT_THROTTLE_RATES={'mobile':1/min}
    2. 重写get_cache_key方法:
      • 返回与限制条件有关的字符串,表示限制
      • 返回None,表示不限制
class MobileRateThrottle(SimpleRateThrottle):
    """
    1)设置scope字符串类属性,同时在settings中进行drf配置DEFAULT_THROTTLE_RATES
        eg: DEFAULT_THROTTLE_RATES = {'mobile': '1/min'}
    2)重写get_catch_key方法:
        返回与限制条件有关的字符串,表示限制
        返回None,表示不限制
    """
    scope = 'mobile'
    def get_cache_key(self, request, view):
        if not request.user.is_authenticated or not request.user.mobile:
            return None  # 匿名用户 或 没有电话号的用户 都不限制

        # 只要有电话号的用户踩进行限制
        return self.cache_format % {
            'scope': self.scope,
            'ident': request.user.mobile
        }

(三)配置频率

  1. 频率类一般在局部做配置,但是频率调节参数在settings中做全局配置
  2. 默认频率调节参数为空
REST_FRAMEWORK = {
    # 频率组件:频率类一般做局部配置,但是频率调节在settings中配置
    'DEFAULT_THROTTLE_RATES': {
        'user': '5/min',
        'anon': '3/min',
        'mobile': '1/min'  # 自定义频率类中频率限制的scope
    },
}

四、自定义签发token(多方式登录)

(一)drf-jwt中签发token源码

  • drf-jwt中签发token通过JSONWebTokenSerializer序列化类处理后签发、
  • JSONWebTokenSerializer序列化类内部源码:
    1. auth组件:根据username、password通过auth组件的authenticate方法得到user对象
    2. jwt_payload_handler:user对象通过drf-jwt模块的jwt_payload_handler函数包装payload(载荷)
    3. jwt_encode_handler:payload(载荷)通过drf-jwt模块的jwt_encode_handler函数签发token字符串

因此,我们借助jwt_payload_handler和jwt_encode_handler方法完成自定义的token签发

from django.contrib.auth import authenticate, get_user_model
class JSONWebTokenSerializer(Serializer):
    def __init__(self, *args, **kwargs):
        super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
        
        # 校验规则只有username和password
        self.fields[self.username_field] = serializers.CharField()  # 账号
        self.fields['password'] = PasswordField(write_only=True)  # 密码密文处理

    @property
    def username_field(self):
        return get_username_field()

    def validate(self, attrs):  # 全局校验
        # 将username和password封装到字典中
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            # 通过auth组件的authenticate方法进行校验,该方法也只能校验username和password
            user = authenticate(**credentials)
            
            # 1. 获得用户
            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)
                # 2. 将用户信息添加到载荷中
                payload = jwt_payload_handler(user)
                
                # 3. 签发token:将token和user存放在serializer对象中,在外界中可以通过request.object.get('token|user')获取token
                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            # 4.失败,抛异常
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

(二)多方式登录

  • drf-jwt提供的签发token只能根据账号密码签发,因此要想实现多方式登录,需要自定义根据多种方式(电话、邮箱等)签发的算法
  • 注意的地方
    1. token只能在登录接口签发
    2. 登录接口也是APIView的子类,会进行认证、权限组件的校验,而登录接口其实是不能进行认证、权限组件的校验,以防止访问被拒之门外。因此,类似登录接口的不需要认证和权限的视图类一定要局部禁用认证和权限组件
      • authentication_classes = []
      • pagination_class = []
    3. 登录接口是通过post请求实现,需要重写post方法,因此不能继承GenericAPIView,只能继承APIView
    4. post请求,序列化时会默认会默认当做create方法进行数据库校验,可能会抛出用户已存在的异常,因此需要自定义系统校验规则
# 多方式登录
from rest_framework.views import APIView
class LoginAPIView(APIView):
    """ 重点
    1)token只能由 登录接口 签发
    2)登录接口也是APIView的子类,使用一定会进行 认证、权限 组件的校验
    结论:不管系统默认、或是全局settings配置的是何认证与权限组件,登录接口不用参与任何认证与权限的校验
    所以,登录接口一定要进行 认证与权限 的局部禁用
    """
    authentication_classes = []
    pagination_class = []

    def post(self, request, *args, **kwargs):
        serializer = serializers.LoginModelSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)  # 内部在全局钩子中完成token的签发
        return APIResponse(results={
            'username': serializer.content.get('user').username,
            'token': serializer.content.get('token')
        })
  1. 将请求数据交给序列化类,执行序列化校验
  2. 在序列化全局校验钩子中,完成user的认证与token的签发,保存在序列化对象的content中
  3. 在视图类中从序列化对象的content中拿user与token相关信息返回
    注:多方式登录体现在 请求的账号类型可能是用户名、邮箱或手机等,采用不同字段校验数据库即可
# serialzier.py
from rest_framework import serializers
class LoginModelSerialzier(serialzier.ModelSerialzier):
    # post请求,序列化默认当做create动作进行校验,需要校验数据库,create动作username会抛用户已存在异常
    # 抛用户已存在异常是多余的,所以自定义系统校验规则即可
    username= serializer.CharField(min_length=3,max_length=16)
    password = serializers.CharField(min_length=3, max_length=16)
    class Meta:
        model = models.User
        fields = ('username', 'password')
       
        # 用全局钩子,完成token的签发
    def validate(self, attrs):
        # 1)通过 username 和 password 完成多方式登录校验,得到user对象
        user = self._validate_user(attrs)
        # 2)user对象包装payload载荷
        payload = jwt_payload_handler(user)
        # 3)payload载荷签发token
        token = jwt_encode_handler(payload)
        # 4)将user与token存储到serializer对象中,方便在视图类中使用
        self.content = {
            'user': user,
            'token': token
        }
        return attrs
  
    def _validate_user(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')

        if re.match(r'.*@.*', username):  # 邮箱
            user = models.User.objects.filter(email=username).first()  # type: models.User
        elif re.match(r'^1[3-9][0-9]{9}$', username):  # 电话
            user = models.User.objects.filter(mobile=username).first()
        else:  # 用户名
            user = models.User.objects.filter(username=username).first()

        if not user or not user.check_password(password):
            raise serializers.ValidationError({'message': '用户信息异常'})

        return user

猜你喜欢

转载自www.cnblogs.com/wick2019/p/12150595.html