Django REST Framework 之认证、权限(超详细)

Django 中认证和权限

在没有使用 drf 之前,如何判断用户是否登录,一般是给前端提供一个获取用户信息的接口,如果未登录返回未授权等信息,权限的话一般是在 model 层通过字段来设置,这样只能完成简单的权限限制。

@require_http_methods(['GET'])
@ensure_csrf_cookie
def api_userinfo(request):
    """
    获取当前用户信息
    """
    if not request.user.is_authenticated:
        return api.not_authorized()

    return JsonResponse()

认证

认证一般分三种:

  1. session认证,内部通过Django 的 auth 系统,cookie中要携带 sessionid、csrftoken,请求头中要携带 x-csrftoken
  2. token 认证,基于令牌的 HTTP 认证方案,请求头中要携带 authorization,值为 jwt空格token,但有弊端,下方会讲
  3. 自定义认证,一般是继承BaseAuthentication(或其子类),重写authenticate

自定义认证类

class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request._request.GET.get("token")
        try:
            token_obj = ReviewUserToken.objects.get(token=token)
        except ReviewUserToken.DoesNotExist:
            raise exceptions.AuthenticationFailed("认证失败")

        return (token_obj.user, token_obj)

    def authenticate_header(self, request):
        pass

Django REST Framework 中认证流程

当用户进行登录的时候,运行了登录类的as_view()方法,进入了APIView类的dispatch方法,在原始的 django 中 dispatch 只做方法的分发与执行,并不做认证、权限、节流等操作。

原始 dispatch :

def dispatch(self, request, *args, **kwargs):
    # Try to dispatch to the right method; if a method doesn't exist,
    # defer to the error handler. Also defer to the error handler if the
    # request method isn't on the approved list.
    if request.method.lower() in self.http_method_names:
        handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
    else:
        handler = self.http_method_not_allowed
    return handler(request, *args, **kwargs)

APIView 类也是继承自 View 并重写了 dispatch 方法,加上了认证等信息。
下面一步一步进行解析:

def dispatch(self, request, *args, **kwargs):
    """
    `.dispatch()` is pretty much the same as Django's regular dispatch,
    but with extra hooks for startup, finalize, and exception handling.
    """
    self.args = args
    self.kwargs = kwargs
    # 重新封装 request 方法以及其他参数,此时这个request不再是django的request 而是 drf 里面的
    request = self.initialize_request(request, *args, **kwargs)
    # 重新复制给request,记住,这是封装之后的
    self.request = request
    self.headers = self.default_response_headers  # deprecate?

    try:
    	# 执行认证、权限、节流相关逻辑,认证也在里面,先看认证。
        self.initial(request, *args, **kwargs)

        # Get the appropriate handler method
        # 以下和 django View 里面内容一样
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(),
                              self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed

        response = handler(request, *args, **kwargs)

    except Exception as exc:
        response = self.handle_exception(exc)

    self.response = self.finalize_response(request, response, *args, **kwargs)
    return self.response

initial 方法:

def initial(self, request, *args, **kwargs):
    """
    Runs anything that needs to occur prior to calling the method handler.
    """
    self.format_kwarg = self.get_format_suffix(**kwargs)

    # Perform content negotiation and store the accepted info on the request
    neg = self.perform_content_negotiation(request)
    request.accepted_renderer, request.accepted_media_type = neg

    # Determine the API version, if versioning is in use.
    version, scheme = self.determine_version(request, *args, **kwargs)
    request.version, request.versioning_scheme = version, scheme

    # 认证、权限、节流,依次,
    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)

perform_authentication 方法:

def perform_authentication(self, request):
    """
    Perform authentication on the incoming request.

    Note that if you override this and simply 'pass', then authentication
    will instead be performed lazily, the first time either
    `request.user` or `request.auth` is accessed.
    """
    # 执行了 user 这个属性,找到这个 user 属性,只不过是方法加了 property 装饰器
    request.user


@property
def user(self):
    """
    Returns the user associated with the current request, as authenticated
    by the authentication classes provided to the request.
    """
    if not hasattr(self, '_user'):
        with wrap_attributeerrors():
        	# 执行了_authenticate() 方法。
            self._authenticate()
    return self._user

_authenticate 方法:

def _authenticate(self):
    """
    Attempt to authenticate the request using each authentication instance
    in turn.
    """
    # MyAuthentication 自定义的认证类
    # APIView 中指定的 authentication_classes = [MyAuthentication, ]
    # authenticators 就是封装 request 的时候已经赋值给 authenticators
    """
    def initialize_request(self, request, *args, **kwargs):
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(), # 将认证类赋值给 authenticators
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )
	
	def get_authenticators(self):
		# 返回一个实例化好的列表
        return [auth() for auth in self.authentication_classes]
	"""
	# 循环每一个认证类,这个时候 self.authenticators 
	# 内容就是 get_authenticators 里面实例化好的对象了,在这循环遍历认证。
    for authenticator in self.authenticators:
        try:
        	# 返回一个二元组,分别是认证成功的 user 实例和 auth 
            user_auth_tuple = authenticator.authenticate(self)
        except exceptions.APIException:
            self._not_authenticated()
            # 如果认证失败,会往上抛出异常,将会被 dispatch 捕获并返回
            raise

        if user_auth_tuple is not None:
            self._authenticator = authenticator
            # 赋值给 user 和 auth 对象
            self.user, self.auth = user_auth_tuple
            return
	
	# 认证失败
    self._not_authenticated()


def _not_authenticated(self):
    """
    Set authenticator, user & authtoken representing an unauthenticated request.

    Defaults are None, AnonymousUser & None.
    """
    self._authenticator = None

	# 默认配置文件中配置的,default_sttings.py
    if api_settings.UNAUTHENTICATED_USER:
    	# 也可以在配置文件中配置成自己想要的
        self.user = api_settings.UNAUTHENTICATED_USER()  # AnonymousUser 
    else:
        self.user = None

    if api_settings.UNAUTHENTICATED_TOKEN:
        self.auth = api_settings.UNAUTHENTICATED_TOKEN()  # None
    else:
        self.auth = None

配置文件中配置认证类:
默认会去配置文件读,如果 APIView 中写了就用 APIView 中的。

key 的名称叫 REST_FRAMEWORK。

配置如下:

REST_FRAMEWORK = {
	# 自定义认证类的路径,可以多个
    "DEFAULT_AUTHENTICATION_CLASSES": ["api.utils.auth.MyAuthentication"],
}

默认的认证类:
为了规范,自定义的认证类都要继承:BaseAuthentication

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

**BasicAuthentication:**基于用户名密码的身份验证。

class BasicAuthentication(BaseAuthentication):
    """
    HTTP Basic authentication against username/password.
    """
    pass

**SessionAuthentication:**使用 Django 的会话框架进行身份验证。

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

**TokenAuthentication: ** 基于 token 令牌的方式
如:Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a

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
    """
    pass
    不建议使用 drf 自带的 token 认证,因为 token 是保存在后端,
    每次都需要查询数据库,增加数据库的压力,不利于分布式系统中使用,
    在实际项目中会使用 JWT 标准的认证方式,python中可以使用 pyjwt

一般配置文件中这样设置:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

Django REST Framework 中权限模块和执行流程

权限模块也是在 initial 中:check_permissions

def check_permissions(self, request):
    """
    Check if the request should be permitted.
    Raises an appropriate exception if the request is not permitted.
    """
    # 这次读取的是 APIView 中的 permission_classes
    # 设置如:permission_classes = [IsAuthenticated]
    # 需要在类中实现一个 has_permission 方法。如下:
    """
	class IsAuthenticated(BasePermission):
    	def has_permission(self, request, view):
        	return bool(request.user and request.user.is_authenticated)
	"""
	# 循环的是 get_permissions 实例化之后的权限类
    for permission in self.get_permissions():
        if not permission.has_permission(request, self):
            self.permission_denied(
            	# message 可以自定义,默认是异常里面抛出的
                request, message=getattr(permission, 'message', None)
            )

同样也有一个获取所有权限类的方法,并且实例化之后返回一个列表:

def get_permissions(self):
    """
    Instantiates and returns the list of permissions that this view requires.
    """
    return [permission() for permission in self.permission_classes]

没有权限直接抛出异常:

def permission_denied(self, request, message=None):
    """
    If request is not permitted, determine what kind of exception to raise.
    """
    if request.authenticators and not request.successful_authenticator:
        raise exceptions.NotAuthenticated()
    raise exceptions.PermissionDenied(detail=message)

同样也可以配置文件中配置:

REST_FRAMEWORK = {
	# 自定义认证类路径
    "DEFAULT_AUTHENTICATION_CLASSES": ["rest_framework.authentication.SessionAuthentication"],
    # 自定义权限类路径,这里是使用的默认的,自定义的可以把路径写在这里
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
}

总结

源码内容:

  1. self.initialize_request这个方法,里面封装了request和认证对象列表等其他参数
  2. 执行self.initial方法中的self.perform_authentication,里面运行了user方法
  3. 再执行了user方法里面的self._authenticate()方法
  4. self._authenticate() 循环遍历每个认证类,将得到的user和auth赋值给user和auth方法
  5. 这两个方法把user和auth的值分别赋值给request.user:是登录用户的对象;request.auth:是认证的信息字典
  6. 注意:自定义的类中没有 authenticate 会报错
  7. 没有携带认证信息,直接返回None => 游客
  8. 有认证信息,校验成功,返回一个元组,第一个参数赋值给request.user,第二个赋值给request.auth
  9. 有认证信息,校验失败,抛异常 => 非法用户

参考文献

drf 中文网:https://q1mi.github.io/Django-REST-framework-documentation/api-guide/authentication_zh/

来源:猫鱼薄荷_她

物联沃分享整理
物联沃-IOTWORD物联网 » Django REST Framework 之认证、权限(超详细)

发表评论