Django rest-framework 用户登录认证源码剖析

Django 是一个开放源代码的 Web 应用框架,由Python写成。作为 Python 最热门的后台框架之一,Django 平台上也涌现了很多优秀的基于它开发的框架。而 Django rest-framework,则是其中最为流行的框架。大家都知道它基于 Django 实现的一个 RESTful 风格 API 框架,能够帮助我们快速开发RESTful风格的API。但其实他不仅仅如此描述的这般单一,本篇文章主要从源码的角度剖析 Django rest-framework 的用户登录认证流程以及如何全局以及定制化认证。本篇主要基于 CBV 进行用例展示。

源码版本: 3.1.7

Python 版本:3.9.1

CBV

当来自前端的请求通过网络到达 Django 服务时,通过 Django 的路由系统,在 CBV 下通常会定位到一个 View class:


urlpatterns = [
    url(r'test.html$', TestDemo.as_view())
]

定义一个最简单的视图类 TestDemo:

class TestDemo(APIView):

    def get(self, request):
        return JsonResponse({})

这里有一个问题: CBV 到底是如何做到直接可以不用显式地判断 request.method 就可以自动调用视图类中的对应方法的呢?

其实这里使用了Python 反射的技巧。as_view 会导致 TestDemo 在执行到用户 view 之前调用 view.dispatch 方法。dispatch 方法如下:

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

def dispatch(self, request, *args, **kwargs):
    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)

通过获取到 request.method后在视图类中利用反射,将 TestDemo 中的 get 方法赋值给 handler,然后执行 get函数

getattr(self, …) 中 self 是 TestDemo 的对象。handle 是指向 TestDemo 中 get 函数对象的变量,所以handle也是 callable。

讲到这里总结一下

CBV 通过 dispatch 进行方法的派发,自动通过方法名调用对应方法。

APIView VS View

当然今天的主人公是 Django rest-framework,那我们就得看一下 APIView 类的内容与 View 有什么不同。老规矩先读源码:

class APIView(View):
    
    ...
    
    def dispatch(self, request, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # ============================================================
            # 这一部分与 Django 中 view.dispatch 中的内容相同
            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
    
    ...

我们可以发现,APIView.dispatch 在原来 View.dispatch 的基础上,增加了一些内容。而认证的“奥秘”,也存在于其中。

封装 request

request = self.initialize_request(request, *args, **kwargs)
self.request = request

rest-framework 将 HttpRequest 对象 通过 initialize_request 又封装了一层,产生了一个新的对象赋值给当前类对象,以后我们在 CBV 中得到的是经过了封装的一层 request,内容如下:

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(),
        negotiator=self.get_content_negotiator(),
        parser_context=parser_context
    )

Request 类的片段如下:

class Request:
    def __init__(...)
        self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty
        
   ...

    def __getattr__(self, attr):
        try:
            return getattr(self._request, attr)
        except AttributeError:
            return self.__getattribute__(attr)

这里头包含两个关键信息:

  1. 未来在视图类中,需要通过 request._request 来获取 HttpRequest 对象。
  2. 如果调用了尝试在 Request对象上调用 HttpRequest方法,Request通过魔术方法__getattr__针对找不到方法进行了处理,仍然会再尝试调用 HttpRequest 类的方法。

这里还有一个与今天的主题有直接关系的成员变量 authenticators,通过 get_authenticators 调用进行赋值。

Authenticators

首先来看一下 get_authenticators 方法:

authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

def get_authenticators(self):
    return [auth() for auth in self.authentication_classes]

这里利用一个列表推导,将 authentication_classes 中的 callable 对象进行实例化,authentication_classes 是一个类的列表。在 rest-framework 框架中, authentication_classes 有一个默认值

api_settings.DEFAULT_AUTHENTICATION_CLASSES

api_settings 指代的就是project配置文件 settings.py 中的变量 REST_FRAMEWORK

在 dispatch 中继续往下看,在执行视图 view 方法前还有一个处理:

self.initial(request, *args, **kwargs)

def initial(self, request, *args, **kwargs):
	...
    # 这句话就是确保 request 是符合认证的地方
    self.perform_authentication(request)
	...

Perform_authentication

进入 perform_authentication 观察:

def perform_authentication(self, request):
    request.user

仅仅调用了 Request 中的 user,很明显这是一个property,代码如下:

class Request:
    ...
    
    @property
    def user(self):
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user
    
    ...

判断当前 Request 并没有 _user 属性,即还未确定身份后,调用 _authenticate 方法:

def _authenticate(self):

    for authenticator in self.authenticators:
        try:
            user_auth_tuple = authenticator.authenticate(self)
        except exceptions.APIException:
            self._not_authenticated()
            raise

        if user_auth_tuple is not None:
            self._authenticator = authenticator
            self.user, self.auth = user_auth_tuple
            return

    self._not_authenticated()

刚才通过列表推导生成的 authenticators,此时迭代得到里面的类对象,执行类里面的 authenticate 方法,并传入 Request 对象。这个函数的方法有以下几种形式:

  • None:则表示次 authenticators 中迭代到的当前类并不影响认证,转而判断下一个类

  • user, auth:认证成功,返回用户对象和认证对象的元祖

  • Exception:认证失败,抛出异常。

    只要有其中一个认证成功或者失败,则循环结束,返回 None 则继续下一个类认证

如果抛出异常,则 在 dispatch 中 self.initial(request, *args, **kwargs) 将失败,此时会跳转到 handle_exception 的异常处理中而不会进入到视图类的方法之中:

class APIView(View):
    
    ...
    
    def dispatch(self, request, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers

        try:
            self.initial(request, *args, **kwargs) ## Exception

            # ============================================================
            # 这一部分与 Django 中 view.dispatch 中的内容相同
            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) # Jump here

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

此时即代表认证失败

全局配置

分析到这里,执行视图类中的对应方法前,进行认证的整个流程就通过源码过了一遍。在讲到封装 HTTPRequest 对象到 Request 的时候,有 authentication_classes:

authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

def get_authenticators(self):
    return [auth() for auth in self.authentication_classes]

当我们在 settings.py 中去按照如下方式配置 DEFAULT_AUTHENTICATION_CLASSES 时,全局的继承和修饰的CBV和FBV都会默认启用认证:

REST_FRAMEWORK = {
    DEFAULT_AUTHENTICATION_CLASSES:['xxxx', 'yyyy']
}

通过上面这种方式,我们就可以将全局的 CBV 和 FBV 默认进行 xxxx 类和 yyyy 类的认证。

局部配置

重新审视这段代码:

authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES

def get_authenticators(self):
    return [auth() for auth in self.authentication_classes]

按照继承的调用顺序,如果继承了 APIView 的类也定义了 authentication_classes,则会覆盖父类中的定义。所以自然地,如果某一视图类中定义了 authentication_classes 类,则不会进行全局认证。

class TestDemo(APIView):
    
    authentication_classes = [xxxx]

    def get(self, request):
        return JsonResponse({})

认证类

说到这里,该说一说认证类的编写了,从认证类生效的地方可以得知,此类需要实现 authenticate 方法:

def _authenticate(self):

    for authenticator in self.authenticators:
        try:
            user_auth_tuple = authenticator.authenticate(self)
        except exceptions.APIException:
            self._not_authenticated()
            raise

        if user_auth_tuple is not None:
            self._authenticator = authenticator
            self.user, self.auth = user_auth_tuple
            return

    self._not_authenticated()

rest-framework 框架给我们提供了认证基类 BaseAuthentication 用于在自定义 认证类的时候进行继承,重写方法:

class BaseAuthentication:
    
    def authenticate(self, request):
        raise NotImplementedError(".authenticate() must be overridden.")
        
    def authenticate_header(self, request):
        pass

我们通过继承 BaseAuthentication ,实现自定义的认证类:

class MyAuthentication(BaseAuthentication):

    def authenticate(self, request):
        
        # 在此处进行 token 查表等认证业务逻辑
        ...
        # 根据结果进行返回
        # 1. return None:跳过认证
        # 2. return user, auth :返回用户对象和认证对象
        # 3. raise Exception : AuthenticationFailed 或者 NotAuthenticated 表示认证失败
        ...
  • 需要作用在全局配置,则在 settings 进行关联
  • 需要作用在某一模块内的视图类,则在视图类中定义 authentication_classes 即可

总结

整个认证配置方法分为全局和局部,从源码角度清晰明了,希望能够帮助到大家


版权声明:本文为shijie19930806原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。