Django Rest Framework 版本 、认证、权限以及限流组件

 

DRF版本控制组件

- 版本控制:

“对接口进行版本控制只是一种杀死已部署客户端的‘礼貌’方式。”    - 罗伊菲尔丁。

API版本控制允许 更改不同客户端之间的行为;
版本控制由传入的客户端请求确定,可以基于请求URL,也可以基于请求标头。

 

- 自定义版本控制前的源码分析:

  - 初始化 

    - 在APIViews.dispatch方法中,DRF在Request被封装好后,执行了一个初始化的函数 self.initial() ,在该函数中实例化了四个对象:

    - 源码:

      - APIViews.dispatch():

 

      - APIViews.initial()

         - 注释1,应该是 实例化版本控制 (图片内容错误);

 

  - 在版本控制中,返回了两个对象,一个是版本号,一个是版本控制的实例化对象;

    - determine_version()

 

    - api_settings:  将DEFAULT_VERSIONING_CLASS 对应的值改成自定义的类,即可启用 版本控制;

 

 - 自定义版本控制:

  - 配置settings.py文件:

REST_FRAMEWORK = {
  #
util.versioning.MyVersioning 自定义类的文件路径
  "DEFAULT_VERSIONING_CLASS": "util.versioning.MyVersioning"
}

  - 定义版本管理相关的类:

    - 类中必须有 determine_version() 方法;

"""
util.versioning.py
"""

class MyVersioning:
    def determine_version(self, request, *args, **kwargs):
        print(111)
        return "v1"

 

- DRF自带的五种 权限管理:

  - 基于请求头

class AcceptHeaderVersioning(BaseVersioning):
    """
    GET /something/ HTTP/1.1
    Host: example.com
    Accept: application/json; version=1.0
    """
    invalid_version_message = _('Invalid version in "Accept" header.')

    def determine_version(self, request, *args, **kwargs):
        media_type = _MediaType(request.accepted_media_type)
        version = media_type.params.get(self.version_param, self.default_version)
        version = unicode_http_header(version)
        if not self.is_allowed_version(version):
            raise exceptions.NotAcceptable(self.invalid_version_message)
        return version

    # We don't need to implement `reverse`, as the versioning is based
    # on the `Accept` header, not on the request URL.
AcceptHeaderVersioning

 

  - 基于URL上

class URLPathVersioning(BaseVersioning):
    """
    To the client this is the same style as `NamespaceVersioning`.
    The difference is in the backend - this implementation uses
    Django's URL keyword arguments to determine the version.

    An example URL conf for two views that accept two different versions.

    urlpatterns = [
        url(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'),
        url(r'^(?P<version>[v1|v2]+)/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
    ]

    GET /1.0/something/ HTTP/1.1
    Host: example.com
    Accept: application/json
    """
    invalid_version_message = _('Invalid version in URL path.')

    def determine_version(self, request, *args, **kwargs):
        version = kwargs.get(self.version_param, self.default_version)
        if not self.is_allowed_version(version):
            raise exceptions.NotFound(self.invalid_version_message)
        return version

    def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
        if request.version is not None:
            kwargs = {} if (kwargs is None) else kwargs
            kwargs[self.version_param] = request.version

        return super(URLPathVersioning, self).reverse(
            viewname, args, kwargs, request, format, **extra
        )
URLPathVersioning

 

  - 基于利用反向解析分发的URLname_spance上

class NamespaceVersioning(BaseVersioning):
    """
    To the client this is the same style as `URLPathVersioning`.
    The difference is in the backend - this implementation uses
    Django's URL namespaces to determine the version.

    An example URL conf that is namespaced into two separate versions

    # users/urls.py
    urlpatterns = [
        url(r'^/users/$', users_list, name='users-list'),
        url(r'^/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
    ]

    # urls.py
    urlpatterns = [
        url(r'^v1/', include('users.urls', namespace='v1')),
        url(r'^v2/', include('users.urls', namespace='v2'))
    ]

    GET /1.0/something/ HTTP/1.1
    Host: example.com
    Accept: application/json
    """
    invalid_version_message = _('Invalid version in URL path. Does not match any version namespace.')

    def determine_version(self, request, *args, **kwargs):
        resolver_match = getattr(request, 'resolver_match', None)
        if resolver_match is None or not resolver_match.namespace:
            return self.default_version

        # Allow for possibly nested namespaces.
        possible_versions = resolver_match.namespace.split(':')
        for version in possible_versions:
            if self.is_allowed_version(version):
                return version
        raise exceptions.NotFound(self.invalid_version_message)

    def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
        if request.version is not None:
            viewname = self.get_versioned_viewname(viewname, request)
        return super(NamespaceVersioning, self).reverse(
            viewname, args, kwargs, request, format, **extra
        )

    def get_versioned_viewname(self, viewname, request):
        return request.version + ':' + viewname
NamespaceVersioning

 

  - 基于Host 在域名中加上版本的情况上:

class HostNameVersioning(BaseVersioning):
    """
    GET /something/ HTTP/1.1
    Host: v1.example.com
    Accept: application/json
    """
    hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
    invalid_version_message = _('Invalid version in hostname.')

    def determine_version(self, request, *args, **kwargs):
        hostname, separator, port = request.get_host().partition(':')
        match = self.hostname_regex.match(hostname)
        if not match:
            return self.default_version
        version = match.group(1)
        if not self.is_allowed_version(version):
            raise exceptions.NotFound(self.invalid_version_message)
        return version

    # We don't need to implement `reverse`, as the hostname will already be
    # preserved as part of the REST framework `reverse` implementation.
HostNameVersioning

 

  - 基于GET请求往路由后拼接参数发方式:

class QueryParameterVersioning(BaseVersioning):
    """
    GET /something/?version=0.1 HTTP/1.1
    Host: example.com
    Accept: application/json
    """
    invalid_version_message = _('Invalid version in query parameter.')

    def determine_version(self, request, *args, **kwargs):
        version = request.query_params.get(self.version_param, self.default_version)
        if not self.is_allowed_version(version):
            raise exceptions.NotFound(self.invalid_version_message)
        return version

    def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
        url = super(QueryParameterVersioning, self).reverse(
            viewname, args, kwargs, request, format, **extra
        )
        if request.version is not None:
            return replace_query_param(url, self.version_param, request.version)
        return url
QueryParameterVersioning

  

  - 在使用这五种方法时,在配置settings.py文件时需要注意,添加三个参数:DEFAULT_VERSION,ALLOWED_VERSIONS,VERSION_PARAM;

    - DEFAULT_VERSION:默认使用的版本

    - ALLOWED_VERSIONS:允许的版本

    - VERSION_PARAM: 版本使用的参数名称

 

DRF认证组件

- 认证:

身份验证是将传入请求与一组标识凭据
(例如请求来自的用户或其签名的令牌)相关联的机制。 然后,权限和限制策略可以使用这些凭据来确定是否应该允许该请求

 

- 自定义认证组件前的源码分析:

  - APIViews. initial() 中的self.perform_authentication(request)  

    - 该函数里面只有一个 request.user 因为该request 在 inital() 之前已经被 DRF进行过了封装,所以该request已经不是原本的request对象,

    - 查看新的request中的 uesr() ; 该函数中,执行了一个 self._authenticate() 方法;

 

    - self._authenticate()  利用for循环取出了所有的 认证组件中的类:

    

 

    - self._authenticate() 中的 self.authenticators 指向了在Request对象实例化时传入的参数 authenticators

    

 

    - Request实例化是在 APIViews.dispatch() 中的 self.initialize_request() 函数中进行:

    

 

    - 这里传入的参数 self.get_authenticators() 对应的返回值是一个列表,该列表中,存放的是所有在settings中注册的认证组件,所代表的类的实例化对象;

    

 

    - 也就是说,在 self._authenticate() 中的for循环实际上是执行了所有实例化对象中的 authenticate(self) 方法;

    

 

- 自定义组件:

  - 自定义认证组件的类:

    - 所有的类都必须继承:rest_framework.authentication.BaseAuthentication 此类

    - 必须有authenticate() 方法

      - 该方法必须接收 request,

      - 该方法必须返回一个元组,并且该元组必须有user和token

      - 认证失败的话,只能 raise 一个错误,return 其他的一切都会引起报错;

    - 示例:

# util.auth.py
from
rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from django.http import HttpResponse class MyAuthentication(BaseAuthentication): def authenticate(self, request): user = "alex" token = "dfgsfdgsfdhg4512158" print(user) if user == "alex": raise AuthenticationFailed("认证失败") # 返回Request会报错 # return Request("errors") # 返回django原生的HttpResponse 也会报错 # return HttpResponse("Errors") return (user, token)

  

  - 在settings.py文件中注册 DEFAULT_AUTHENTICATION_CLASSES

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["util.auth.MyAuthentication", ]
}

 

- DRF 自带的四种认证组件:

  - 认证用户名以及密码

class BasicAuthentication(BaseAuthentication):
    """
    HTTP Basic authentication against username/password.
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        """
        Returns a `User` if a correct username and password have been supplied
        using HTTP Basic authentication.  Otherwise returns `None`.
        """
        print("BasicAuthentication")
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != b'basic':
            return None

        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)

        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):
        """
        Authenticate the userid and password against username and password
        with optional request for context.
        """
        credentials = {
            get_user_model().USERNAME_FIELD: userid,
            'password': password
        }
        user = authenticate(request=request, **credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (user, None)

    def authenticate_header(self, request):
        return 'Basic realm="%s"' % self.www_authenticate_realm
BasicAuthentication

 

  - 认证用户session中是否被添加user的字符串

class SessionAuthentication(BaseAuthentication):
    """
    Use Django's session framework for authentication.
    """

    def authenticate(self, request):
        """
        Returns a `User` if the request session currently has a logged in user.
        Otherwise returns `None`.
        """

        # Get the session-based user from the underlying HttpRequest object
        user = getattr(request._request, 'user', None)

        # Unauthenticated, CSRF validation not required
        if not user or not user.is_active:
            return None

        self.enforce_csrf(request)

        # CSRF passed with authenticated user
        return (user, None)

    def enforce_csrf(self, request):
        """
        Enforce CSRF validation for session based authentication.
        """
        check = CSRFCheck()
        # populates request.META['CSRF_COOKIE'], which is used in process_view()
        check.process_request(request)
        reason = check.process_view(request, None, (), {})
        if reason:
            # CSRF failed, bail with explicit error message
            raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
SessionAuthentication

 

  - 认证存放在客户端请求头中的 token

class TokenAuthentication(BaseAuthentication):
    """
    Simple token based authentication.

    Clients should authenticate by passing the token key in the "Authorization"
    HTTP header, prepended with the string "Token ".  For example:

        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
    """

    keyword = 'Token'
    model = None

    def get_model(self):
        if self.model is not None:
            return self.model
        from rest_framework.authtoken.models import Token
        return Token

    """
    A custom token model may be used, but must have the following properties.

    * key -- The string identifying the token
    * user -- The user to which the token belongs
    """

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != self.keyword.lower().encode():
            return None

        if len(auth) == 1:
            msg = _('Invalid token header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid token header. Token string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            token = auth[1].decode()
        except UnicodeError:
            msg = _('Invalid token header. Token string should not contain invalid characters.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(token)

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_('Invalid token.'))

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (token.user, token)

    def authenticate_header(self, request):
        return self.keyword
TokenAuthentication

    - 还需要自己在进行一步设置;

 

  - 认证- - 看不懂(将身份验证委派给Web服务器)???

class RemoteUserAuthentication(BaseAuthentication):
    """
    REMOTE_USER authentication.

    To use this, set up your web server to perform authentication, which will
    set the REMOTE_USER environment variable. You will need to have
    'django.contrib.auth.backends.RemoteUserBackend in your
    AUTHENTICATION_BACKENDS setting
    """

    # Name of request header to grab username from.  This will be the key as
    # used in the request.META dictionary, i.e. the normalization of headers to
    # all uppercase and the addition of "HTTP_" prefix apply.
    header = "REMOTE_USER"

    def authenticate(self, request):
        user = authenticate(remote_user=request.META.get(self.header))
        if user and user.is_active:
            return (user, None)
RemoteUserAuthentication

 

DRF权限组件

- 权限组件:

与身份验证和限制一起,权限确定是应该授予还是拒绝访问请求。

在允许任何其他代码继续之前,权限检查始终在视图的最开始运行。权限检查通常使用request.user和request.auth属性中的身份验证信息来确定是否应允许传入请求。

权限用于授予或拒绝不同类别的用户访问API的不同部分。

最简单的权限类型是允许访问任何经过身份验证的用户,
并拒绝访问任何未经身份验证的用户。

 

- 源码流程:

  - 与权限类同;

 

- 自定义:

  - 在settings.py 文件中注册:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'util.permission.MyPermission',
    ],
}

  

  - 定义 MyPermission 类中的  has_permission(request, self)方法, 以及定义或放弃定义 报错信息 message;

    - 代码:

from rest_framework.permissions import BasePermission


class MyPermission(BasePermission):
    def has_permission(self, request, view):
        self.message = "返回为None"
        print("True")

 

DRF限流组件

- 限流:

限制类似于权限,因为它确定是否应该授权请求。Throttles表示临时状态,用于控制客户端可以对API发出的请求的速率。

与权限一样,可以使用多个限制。您的API可能对未经身份验证的请求具有限制性限制,并且对经过身份验证的请求限制较少。

您可能希望使用多个限制的另一种情况是,如果您需要对API的不同部分施加不同的约束,因为某些服务特别是资源密集型。

如果要同时施加突发限制速率和持续限制速率,也可以使用多个节流阀。例如,您可能希望将用户限制为每分钟最多60个请求,每天1000个请求。

Throttles不一定仅涉及限速请求。例如,存储服务可能还需要限制带宽,并且付费数据服务可能想要限制正在访问的特定数量的记录。

 

- 源码流程: 与权限,认证流程相同;

 

- 实现限流的逻辑:

  - 获取用户ip地址;

  - 建立数据结构:以用户ip为key,访问时间组成列表作为value的一个字典;

  - 当请求进来,判断ip是否存在;若不存在,在数据中添加该数据;若存在,记录当前时间到该用户ip对应的列表中;

  - 对已存的数据进行限制:

    - 列表总量保持一致;

    - 最新时间和最老的时间超多限制则拒绝访问;

    - 若未超过限时时间,则删除最后的时间,添加最新的时间;

  - 代码:

from rest_framework import throttling
import time

VISIT_RECORD = {}
class MyThrottle(object):
    """
    60秒访问3次
    """
    def __init__(self):
        self.history = None

    def allow_request(self, request, view):
        """
        频率限制的逻辑
        通过返回True
        不通过返回False
        :param request:
        :param view:
        :return:
        """
        # 获取用户IP
        ip = request.META.get("REMOTE_ADDR")
        # 判断ip是否在访问记录里
        now = time.time()
        if ip not in VISIT_RECORD:
            VISIT_RECORD[ip] = [now,]
        # 如果ip在访问记录里
        history = VISIT_RECORD[ip]
        # 把当然访问时间添加到列表最前面
        history.insert(0, now)
        self.history = history
        # 确保列表内的时间都是范围内时间
        while history and now - history[-1] > 60:
            history.pop()
        # 看列表长度是否符合限制次数
        if len(history) <= 3:
            return True
        else:
            return False

    def wait(self):
        """
        返回还剩多久可以访问
        :return:
        """
        now = time.time()
        return 60 - (now - self.history[-1])

 

posted @ 2018-10-31 15:24  浮生凉年  阅读(435)  评论(0编辑  收藏  举报