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

posted @ 2020-02-25 18:59  GhostAnt  阅读(368)  评论(0编辑  收藏  举报