zoukankan      html  css  js  c++  java
  • drf之三大认证

    一、前言

    ​ 我们知道drf的APIView类的as_view直接对原生django的csrf进行了禁用,是什么让drf有如此底气?从之前对drf的源码分析可以看到,三条语句。

    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)
    

    这就是drf的三大认证。

    二、用户认证

    1.drf的用户认证

    ​ 我们的某些接口需要对用户进行辨别,那么我们该如何区分A用户和B用户呢?如果A用户想访问B用户的余额,这种操作是不被允许的。在django中,已经完成过基于cookie和session的身份认证,对登录的用户返回一个cookie,并在服务端也进行保存,用户必须携带cookie才能通过用户认证。

    drf的认证规则:

    • 如果没有认证信息,则认为是游客
    • 如果认证失败,抛出异常
    • 认证成功返回(user,token)

    rest_framework文件下的authentication.py中为我们写好了用户认证的基类,及一些基础的认证类。我们可以通过重写authenticate和相关的方法来定义自己的用户认证类。

    class BaseAuthentication:
        """
        All authentication classes should extend BaseAuthentication.
        """
    
        def authenticate(self, request):
            """
            Authenticate the request and return a two-tuple of (user, token).
            """
            raise NotImplementedError(".authenticate() must be overridden.")
    
        def authenticate_header(self, request):
            """
            Return a string to be used as the value of the `WWW-Authenticate`
            header in a `401 Unauthenticated` response, or `None` if the
            authentication scheme should return `403 Permission Denied` responses.
            """
            pass
    
    2.基于token的drf-jwt认证

    ​ 我们借用第三方djangorestframework-jwt来完成我们的用户认证。jwt是通过签发token、校验token来完成用户认证的。token是有 头、体、签名信息组成的一串字符串,以 . 分割开。

    '''
    token示例eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImN5YW4iLCJleHAiOjE1ODI1NDA0MDcsImVtYWlsIjoiIn0.52E5R00GL0gx-3O3OTosXz0cDWmVzmNU16xEbZpmAkg
    
    1.头和体部一般可以是一个字典序列化成Json格式后转化为二进制然后进行 双向加密如:base64算法得到的。
    2.头部信息一般只是对token的所属进行声明,例如项目名称、公司名称
    3.体部信息则包含用户的标识信息、及关键信息,如用户的主键、用户的设备信息、token的有效时间、token的签发时间。服务端可以通过对其进行反向解密获得相关信息。
    4.尾部信息由 经过加密后的头部和尾部 与 服务器存储的秘钥 进行单向加密如:md5算法生成。
    
    分析:服务端拿到token后会对 头、体及自身存储的秘钥进行md5加密,如果加密的结果与第三段不符,则一定是头、体的信息发生了改变,token便会认为无效。所以服务器端的秘钥是至关重要的,如果泄漏了则攻击者可以伪造任意的token对任意的接口进行访问。
    优势:服务端不用再保存用户的认证信息,也就意味着不需要频繁的读写数据库,降低了数据库的压力,在实现服务器集群时也非常方便。
    '''
    

    jwtauthentication源码分析

    # from rest_framework_jwt.authentication.JSONWebTokenAuthentication类中
    class BaseJSONWebTokenAuthentication(BaseAuthentication):
        """
        Token based authentication using the JSON Web Token standard.
        """
    
        def authenticate(self, request):
            """
            Returns a two-tuple of `User` and token if a valid signature has been
            supplied using JWT-based authentication.  Otherwise returns `None`.
            """
            # 获取request请求中的token
            jwt_value = self.get_jwt_value(request)
            if jwt_value is None:
                return None
    		
            # 对token进碰撞校验:编码格式、过期时间、token是否有效
            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()
    		
            # 获取token中包含的user对象
            user = self.authenticate_credentials(payload)
    
            return (user, jwt_value)
    
        def authenticate_credentials(self, payload):
            """
            Returns an active user that matches the payload's user id and email.
            """
            User = get_user_model()
            username = jwt_get_username_from_payload(payload)
    
            if not username:
                msg = _('Invalid payload.')
                raise exceptions.AuthenticationFailed(msg)
    
            try:
                user = User.objects.get_by_natural_key(username)
            except User.DoesNotExist:
                msg = _('Invalid signature.')
                raise exceptions.AuthenticationFailed(msg)
    
            if not user.is_active:
                msg = _('User account is disabled.')
                raise exceptions.AuthenticationFailed(msg)
    
            return user
    
    
    class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
        """
        Clients should authenticate by passing the token key in the "Authorization"
        HTTP header, prepended with the string specified in the setting
        `JWT_AUTH_HEADER_PREFIX`. For example:
    
            Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj
        """
        www_authenticate_realm = 'api'
    
        def get_jwt_value(self, request):
            
            auth = get_authorization_header(request).split()
            auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
    
            if not auth:
                if api_settings.JWT_AUTH_COOKIE:
                    return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
                return None
    
            if smart_text(auth[0].lower()) != auth_header_prefix:
                return None
    		
            # token如果不由 前缀JWT + 原token组成则抛出异常
            if len(auth) == 1:
                msg = _('Invalid Authorization header. No credentials provided.')
                raise exceptions.AuthenticationFailed(msg)
            elif len(auth) > 2:
                msg = _('Invalid Authorization header. Credentials string '
                        'should not contain spaces.')
                raise exceptions.AuthenticationFailed(msg)
    
            return auth[1]
    
        def authenticate_header(self, request):
            """
            Return a string to be used as the value of the `WWW-Authenticate`
            header in a `401 Unauthenticated` response, or `None` if the
            authentication scheme should return `403 Permission Denied` responses.
            """
            return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
    

    通过了用户认证的类,request.user的对象要么是Anonymous或者是数据库中合法的user对象,至此,用户认证完毕。

    三、权限认证

    ​ 只有通过了用户认证的request请求才会进行权限认证。我们的某些接口通常需要vip用户才能访问,普通用户是无法进行访问的。但vip用户和普通用户都会通过用户认证,我们又该如何区分呢?

    rest_framework文件夹下的permissions.py。

    class BasePermission(metaclass=BasePermissionMetaclass):
        """
        A base class from which all permission classes should inherit.
        """
    
        def has_permission(self, request, view):
            """
            Return `True` if permission is granted, `False` otherwise.
            """
            return True
    
        def has_object_permission(self, request, view, obj):
            """
            Return `True` if permission is granted, `False` otherwise.
            """
            return True
    
    '''
    drf自带的权限认证类:
    	- AllowAny 允许任何人
    	- IsAuthenticated 只允许登录用户
    	- IsAdminUser 只允许后台用户
    	- IsAutenticatedOrReadOnly  只允许未登录用户读,允许登录用户读写
    
    我们可以通过继承BasePermission类重写has_permission方法来实现自定义的权限认证类,认证通过返回True,否则返回False即可。
    '''
    
    校验用户是否是VIP或属于VIP分组的权限类 案例
    class IsVipPermission(BasePermission):
        
        def has_permission(self, request, view):
            if request.user and request.user.is_authenticated and request.user.is_vip:
                return True
            else:
                return False
    
    # 没有is_vip字段,有vip分组控制时
    class IsVipPermission(BasePermission):
        
        def has_permission(self, request, view):
            vip_group = Group.objects.get(name='vip')
            if request.user and request.user.is_authenticated and (vip_group in request.user.groups.all()):
                return True
            else:
                return False
    

    四、频率认证

    ​ 当request请求通过用户认证和权限认证后,还要进行频率的检测,如果我们接口不限制访问频率,那么可能会让攻击者有机可乘,造成服务器的瘫痪。

    rest_framework文件夹下的throttling.py中已经定义了基础的频率校验类。我们只需要继承SimpleRateThrottle类并重写get_cache_key方法。

    案例:自定义频率类,只限制get请求的访问频率,不限制其他访问请求

    class MethodRateThrottle(BaseThrottle):
       scope = 'method'  # scope
       def get_cache_key(self,request,view):
           if request.method.lower() == 'get':
           return self.cache_format % {
               'scope': self.scope,
               'ident': self.get_ident(request)
           }
       	else:
               return None
    '''
    scope需要在settings.py中进行配置
    get_cache_key方法,返回None代表 无限制访问,如果返回字符串,则该字符串会在缓冲中被保存(因为数据库中没有相应的表,所以我们推断内存中应该由一张虚拟的表,用来记录访问的频率)。
    例如:限制了 3/min的访问频率,如果同一用户在1分钟内访问了3次,则会返回3次相同的字符串(因为该字符串是带有用户标识信息的get_indent,不同的用户的表示信息不同,一个用户被限制不会影响其他用户)。当第4次访问时,reqeust请求就会被拒绝。
    
    '''
    

    五、token刷新

    drf-jwt为我们提供了token 的刷新功能,我们可以给token属性设置为可刷新。在token有效并且在刷新过期时间内,可以访问接口来刷新token的过期时间。

    """
    1)运用在像12306这样极少数安全性要求高的网站
    2)第一个token由登录签发
    3)之后的所有正常逻辑,都需要发送两次请求,第一次是刷新token的请求,第二次是正常逻辑的请求
    """
    
    settings.py
    import datetime
    
    JWT_AUTH = {
        # 配置过期时间
        'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=5),
    
        # 是否可刷新
        'JWT_ALLOW_REFRESH': True,
        # 刷新过期时间
        'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
    }
    
    urls.py
    from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken
    urlpatterns = [
        url('^login/$', ObtainJSONWebToken.as_view()),  # 登录签发token接口
        url('^refresh/$', RefreshJSONWebToken.as_view()),  # 刷新toekn接口
    ]
    

    六、多方式登录

    drf-jwt只为我们提供了 用户名-密码登录的签发token,是不支持用户以手机号、邮箱登录进行登录的。如果我们想实现多方式登录,必须自定义签发token。

    serializers.py
    class LoginSerializer(serializers.ModelSerializer):
        # 局部禁用
        authentication_classes = []
        permission_classes = []
        # 需要对字段进行覆盖,以免drf认为这是在做增数据自动校验
        username = serializers.CharField()
        password = serializers.CharField()
        class Meta:
            model = models.User
            fields = ['username', 'password']
    
        def validate(self, attrs):
            from rest_framework_jwt.serializers import jwt_payload_handler,jwt_encode_handler
            user = self._get_user(attrs)
            if user:
                payload = jwt_payload_handler(user)
                token = jwt_encode_handler(payload)
                self.context['token'] = token
                return attrs
            else:
                raise exceptions.ValidationError({"error":"username or password valid!"})
    
        def _get_user(self, attrs):
            import re
            username = attrs.get('username')  # type:str
            password = attrs.get('password')
            if re.match(r'^.+@.+$', username):
                # 邮箱登录
                print('..邮箱登录')
                user = models.User.objects.filter(email=username).first()
            elif re.match(r'^1[3-9][0-9]{9}$', username):
                print('..手机登录')
                user = models.User.objects.filter(mobile=username).first()
            else:
                user = models.User.objects.filter(username=username).first()
            if user and user.check_password(password):
                return user
    
    urls.py
    urlpatterns = [
        url('login/',views.LoginAPIViewSet.as_view({'post':"login"})),
    ]
    
    views.py
    class LoginAPIViewSet(viewsets.GenericViewSet):
    
        def login(self, request, *args, **kwargs):
            serializer = serializers.LoginSerializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            token = serializer.context.get('token')
            return Response({'token': token})
    

    认证类的配置

    '''
    类的优先顺序:局部-->全局-->drf默认
    '''
    settings.py
    
    
    JWT_AUTH = {
        # token存活时间
    	'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
        # token前缀
        'JWT_AUTH_HEADER_PREFIX': 'JWT',
    }
    
    REST_FRAMEWORK = {
        # 用户认证
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        ],
        # 权限认证
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.AllowAny',
        ],
        # 频率认证
        'DEFAULT_THROTTLE_CLASSES': [],
      
        # 频率配置
        'DEFAULT_THROTTLE_RATES': {
            'user': '3/min', # duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
            'anon': None,
        },
    }
    
    
  • 相关阅读:
    day_10作业
    day_10
    day_09
    day_08
    猜年龄游戏
    day_07
    day_06作业
    day05课堂小结
    day05作业
    day04课堂小结
  • 原文地址:https://www.cnblogs.com/Ghostant/p/12363230.html
Copyright © 2011-2022 走看看