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)
这里头包含两个关键信息:
- 未来在视图类中,需要通过 request._request 来获取 HttpRequest 对象。
- 如果调用了尝试在 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 即可
总结
整个认证配置方法分为全局和局部,从源码角度清晰明了,希望能够帮助到大家