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.
- 基于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 )
- 基于利用反向解析分发的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
- 基于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.
- 基于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
- 在使用这五种方法时,在配置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
- 认证用户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)
- 认证存放在客户端请求头中的 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
- 还需要自己在进行一步设置;
- 认证- - 看不懂(将身份验证委派给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)
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])