zoukankan      html  css  js  c++  java
  • Django Middleware简介

    1      前言

    Django使用非常熟练了,各种API接口不在话下,全都搞定。为方便定位问题在每个API接口的的开始和返回的地方都加上了log打印,记录入参和返回值。

    但是这样有一个问题,需要每个API接口都要写一遍,非常的不Pythonic,有没有更好的方法呢?

    如果大家对装饰器熟悉的话,会想到这个方法。写一个log_wrapper,在每个API的函数上写上@log_wrapper,这样看起来比较美观了。但是有一个问题,如果那个接口忘记使用这个装饰器了,日志就无法记录了。

    在Django中有没有更好的解决方案呢,答案是肯定的。

     

    2      Django HTTP处理流程

    2.1     HTTP处理流程

    我们先来熟悉下Django的HTTP处理流程,详细的处理流程请看下图。

    每个HTTP请求过来,先经过request middleware处理,如果没有异常,则交由URLConf处理(即urls.py文件)来匹配URL,然后到view middleware处理,最后到具体的view业务函数。

     

    HTTP请求中间处理的每个步骤,如果发生异常,直接返回response,而不是到下一个流程中。可以看到图中每个步骤都可以返回response。

    2.2     Request和Response对象

    在Django中,一个 HTTP 请求,首先被转化成一个 HttpRequest 对象,然后该对象被传递给 Request middleware处理,如果该middleware返回了Response,则生成一个Response对象,里面包含所有的HTTP 响应元素。

    HttpRequest对象的属性

    Attribute

    Description

    path

    请求页面的全路径,不包括域名—例如, "/music/bands/the_beatles/"。

    method

    请求中使用的HTTP方法的字符串表示。全大写表示。例如:

    if request.method == 'GET':
        do_something()
    elif request.method == 'POST':
        do_something_else()

    GET

    包含所有HTTP GET参数的类字典对象。参见QueryDict 文档。

    POST

    包含所有HTTP POST参数的类字典对象。参见QueryDict 文档。

    服务器收到空的POST请求的情况也是有可能发生的。也就是说,表单form通过HTTP POST方法提交请求,但是表单中可以没有数据。因此,不能使用语句if request.POST来判断是否使用HTTP POST方法;应该使用if request.method == "POST" (参见本表的method属性)。

    注意: POST不包括file-upload信息。参见FILES属性。

    REQUEST

    为了方便,该属性是POST和GET属性的集合体,但是有特殊性,先查找POST属性,然后再查找GET属性。借鉴PHP’s $_REQUEST。

    例如,如果GET = {"name": "john"} 和POST = {"age": '34'},则 REQUEST["name"] 的值是"john", REQUEST["age"]的值是"34".

    强烈建议使用GET and POST,因为这两个属性更加显式化,写出的代码也更易理解。

    COOKIES

    包含所有cookies的标准Python字典对象。Keys和values都是字符串。参见第12章,有关于cookies更详细的讲解。

    FILES

    包含所有上传文件的类字典对象。FILES中的每个Key都是<input type="file" name="" />标签中name属性的值. FILES中的每个value 同时也是一个标准Python字典对象,包含下面三个Keys:

    • filename: 上传文件名,用Python字符串表示
    • content-type: 上传文件的Content type
    • content: 上传文件的原始内容

    注意:只有在请求方法是POST,并且请求页面中<form>有enctype="multipart/form-data"属性时FILES才拥有数据。否则,FILES 是一个空字典。

    META

    包含所有可用HTTP头部信息的字典。 例如:

    • CONTENT_LENGTH
    • CONTENT_TYPE
    • QUERY_STRING: 未解析的原始查询字符串
    • REMOTE_ADDR: 客户端IP地址
    • REMOTE_HOST: 客户端主机名
    • SERVER_NAME: 服务器主机名
    • SERVER_PORT: 服务器端口

    META 中这些头加上前缀HTTP_最为Key, 例如:

    • HTTP_ACCEPT_ENCODING
    • HTTP_ACCEPT_LANGUAGE
    • HTTP_HOST: 客户发送的HTTP主机头信息
    • HTTP_REFERER: referring页
    • HTTP_USER_AGENT: 客户端的user-agent字符串
    • HTTP_X_BENDER: X-Bender头信息

    user

    是一个django.contrib.auth.models.User 对象,代表当前登录的用户。如果访问用户当前没有登录,user将被初始化为django.contrib.auth.models.AnonymousUser的实例。你可以通过user的is_authenticated()方法来辨别用户是否登录:

    if request.user.is_authenticated():
        # Do something for logged-in users.
    else:
        # Do something for anonymous users.

    只有激活Django中的AuthenticationMiddleware时该属性才可用

    关于认证和用户的更详细讲解,参见第12章。

    session

    唯一可读写的属性,代表当前会话的字典对象。只有激活Django中的session支持时该属性才可用。 参见第12章。

    raw_post_data

    原始HTTP POST数据,未解析过。 高级处理时会有用处。

    Response对象

    Attribute

    Description

    path

    请求页面的全路径,不包括域名—例如, "/music/bands/the_beatles/"。

    method

    请求中使用的HTTP方法的字符串表示。全大写表示。例如:

    if request.method == 'GET':
        do_something()
    elif request.method == 'POST':
        do_something_else()

    GET

    包含所有HTTP GET参数的类字典对象。

    status_code

    HTTP 状态码

    content

    返回的内容

     

    其他内容略

    Django的Response返回类型比较多,基本都是继承了HTTPResponse这个类。

    3      Django中间件介绍和分析

    根据上面的HTTP处理流程图,有一个想法,把每个请求和返回的日志记录加到middleware中,这样每个HTTP的请求和返回都可以记录到。并且不影响业务函数的编写和美观。

    该如何使用middleware呢?

    我们来看下Django的中间件是如何处理HTTP请求的。如下图。

     

    上面中间件不是每个项目必须的,在Django中是可选的,配置在settings.py文件的MIDDLEWARE配置中。可以根据需要进行增加和删除。

    3.1     Middleware使用

    在Django中,middleware的配置在settings文件中,

    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]

    Middleware调用是有序和分层的,request是从上到下,response则相反。

    我们现在看下django自带的中间件是什么样子的。

    查看django源码: site-packagesdjangomiddlewarecommon.py中的CommonMiddleware的类。

    发现继承了MiddlewareMixin类。

    class CommonMiddleware(MiddlewareMixin):
    
        response_redirect_class = http.HttpResponsePermanentRedirect
    def process_request(self, request): """ Check for denied User-Agents and rewrite the URL based on settings.APPEND_SLASH and settings.PREPEND_WWW """ # Check for denied User-Agents if 'HTTP_USER_AGENT' in request.META: for user_agent_regex in settings.DISALLOWED_USER_AGENTS: if user_agent_regex.search(request.META['HTTP_USER_AGENT']): raise PermissionDenied('Forbidden user agent') # Check for a redirect based on settings.PREPEND_WWW host = request.get_host() must_prepend = settings.PREPEND_WWW and host and not host.startswith('www.') redirect_url = ('%s://www.%s' % (request.scheme, host)) if must_prepend else '' # Check if a slash should be appended if self.should_redirect_with_slash(request): path = self.get_full_path_with_slash(request) else: path = request.get_full_path() # Return a redirect if necessary if redirect_url or path != request.get_full_path(): redirect_url += path return self.response_redirect_class(redirect_url) def should_redirect_with_slash(self, request): """ Return True if settings.APPEND_SLASH is True and appending a slash to the request path turns an invalid path into a valid one. """ if settings.APPEND_SLASH and not request.get_full_path().endswith('/'): urlconf = getattr(request, 'urlconf', None) return ( not is_valid_path(request.path_info, urlconf) and is_valid_path('%s/' % request.path_info, urlconf) ) return False def get_full_path_with_slash(self, request): """ Return the full path of the request with a trailing slash appended. Raise a RuntimeError if settings.DEBUG is True and request.method is POST, PUT, or PATCH. """ new_path = request.get_full_path(force_append_slash=True) if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'): raise RuntimeError( "You called this URL via %(method)s, but the URL doesn't end " "in a slash and you have APPEND_SLASH set. Django can't " "redirect to the slash URL while maintaining %(method)s data. " "Change your form to point to %(url)s (note the trailing " "slash), or set APPEND_SLASH=False in your Django settings." % { 'method': request.method, 'url': request.get_host() + new_path, } ) return new_path def process_response(self, request, response): """ Calculate the ETag, if needed. When the status code of the response is 404, it may redirect to a path with an appended slash if should_redirect_with_slash() returns True. """ # If the given URL is "Not Found", then check if we should redirect to # a path with a slash appended. if response.status_code == 404: if self.should_redirect_with_slash(request): return self.response_redirect_class(self.get_full_path_with_slash(request)) if settings.USE_ETAGS: if not response.has_header('ETag'): set_response_etag(response) if response.has_header('ETag'): return get_conditional_response( request, etag=unquote_etag(response['ETag']), response=response, ) return response

    查看Mixin类,有两个函数:

    self.process_request(request)
    self.porcess_response(request, response)

    这两个函数:

    class MiddlewareMixin(object):
        def __init__(self, get_response=None):
            self.get_response = get_response
            super(MiddlewareMixin, self).__init__()
    
        def __call__(self, request):
            response = None
            if hasattr(self, 'process_request'):
                response = self.process_request(request)
            if not response:
                response = self.get_response(request)
            if hasattr(self, 'process_response'):
                response = self.process_response(request, response)
            return response

    有了这些信息,我们来实现自己的middleware

    3.2     自定义日志middleware

    在apps/common/文件夹下创建middleware.py文件

    from django.utils.deprecation import MiddlewareMixin
    from rest_framework.response import Response
    from django.http import JsonResponse
    import logging
    
    jira_log = logging.getLogger('debug')
    
    
    class Reponse(MiddlewareMixin):
    
        def process_response(self, request, response):
            if isinstance(response, Response) and response.get('content-type') == 'application/json' and 
                    isinstance(response.data, dict):
                response.data['msg'] = '成功'
                response.content = response.rendered_content
                # self._convert_data(response, 200)
            if isinstance(response, JsonResponse):
                jira_log.debug(response.content)
            return response
    
        @staticmethod
        def process_request(request):
            if request.method == 'POST':
                jira_log.debug(request.body)
            if request.method == 'GET':
                jira_log.debug(request.GET)
            setattr(request, '_dont_enforce_csrf_checks', True)

    可以看到,我们自己实现了process_request 、process_response 两个函数,增加了日志打印。这样每个请求和返回都有日志记录。

    2017-12-13 14:12:04,520 [middleware.py: 39][Thread-7    ][debug   ]DEBUG request: b'{"a":1, "b":2}'
    2017-12-13 14:12:04,530 [middleware.py: 31][Thread-7    ][debug   ]DEBUG response {'status': 200, 'msg': '成功'}
    2017-12-13 14:12:21,071 [middleware.py: 41][Thread-8    ][debug   ]DEBUG request: <QueryDict: {}>
    2017-12-13 14:12:21,074 [middleware.py: 31][Thread-8    ][debug   ]DEBUG response {'status': 200, 'msg': '成功'}
    2017-12-13 14:12:44,432 [middleware.py: 41][Thread-9    ][debug   ]DEBUG request: <QueryDict: {}>
    2017-12-13 14:12:44,434 [middleware.py: 33][Thread-9    ][debug   ]DEBUG response b'{"status": 200, "msg": "\u6210\u529f 1"}'

    3.3     使用自定义的middleware

    在middleware的配置中添加自定义middleware类即可。

    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
    
    # 自定义的middleware,记录日志,
        'apps.common.middleware.Reponse',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]

    PS:中间放置的位置是顺序是有要求的,request请求是按照从上到下的顺序处理。放置的位置要根据中间件的功能和实际要求来分析。

    3.4     实测日志记录

    发送几个请求来查看日志的记录情况:

    2017-12-13 14:12:04,520 [middleware.py: 39][Thread-7    ][debug   ]DEBUG request: b'{"a":1, "b":2}'
    
    2017-12-13 14:12:04,530 [middleware.py: 31][Thread-7    ][debug   ]DEBUG response {'status': 200, 'msg': '成功'}
    
    2017-12-13 14:12:21,071 [middleware.py: 41][Thread-8    ][debug   ]DEBUG request: <QueryDict: {}>
    
    2017-12-13 14:12:21,074 [middleware.py: 31][Thread-8    ][debug   ]DEBUG response {'status': 200, 'msg': '成功'}
    
    2017-12-13 14:12:44,432 [middleware.py: 41][Thread-9    ][debug   ]DEBUG request: <QueryDict: {}>
    
    2017-12-13 14:12:44,434 [middleware.py: 33][Thread-9    ][debug   ]DEBUG response b'{"status": 200, "msg": "\u6210\u529f 1"}'

    日志能准确的记录请求的参数和返回的内容。当然也可以记录更详细的情况,这个可以根据需要进行添加。

    4      中间件之process_view

    Process_view处理也是middleware的一个函数,专门用来处理view,访问顺序在request之后,业务view函数之前。

    process_view(self, request, callback, callback_args, callback_kwargs)

    在middleware文件中的的代码进行更改:增加process_view代码,并打印日志

    发现middleware的处理顺序为request-view-response。

    如果有多个middleware,则日志打印顺序为:

    1. Request1
    2. Request2
    3. Request3
    4. middlewareView1
    5. middlewareView2
    6. middlewareView3
    7. API view
    8. Response3
    9. Response2
    10. Response1

    有兴趣的同学可以自己写下看看。

    5      Middleware应用

    根据Django官方定义,middleware是框架的钩子。

    Middleware is a framework of hooks into Django’s request/response processing. It’s a light, low-level “plugin” system for globally altering Django’s input or output.

    了解了middleware的原理,我们可以做许多事情。

    5.1     规范化response

    很多项目使用了rest-framework框架,框架对于异常的处理有两点与我们规范不一致:

    l  status_code是400,与我们定义的规范不符合,

    l  处理成功时,返回json中的msg字段值为:成功。

    l  数据都放到data字段中

    如果我们手动处理,则每个人都要来适配这样的格式。

    感谢成宇为大家实现了response的格式化的middleware。简化了大家的工作。

    例如在众测平台,就使用middleware来规范response输出,把不符合规范的格式整理为:

    {
    
      "msg": "成功",
      "status": 200,
      "data": {
          "data": "our data"
      }
    }

    5.2     用来记录响应时间

    可以根据请求和响应时间,记录接口的性能。

    5.3     中间件其他作用

    • 可以用来整理请求,统一添加某个字段。
    • 用来整理返回,统一添加某个字段。
    • 还可以动态添加访问黑名单,判断IP地址是否可以访问。
    • 收集访问来源类型,是PC还是手机浏览器。

    这些都可以和具体的业务解耦,全局的配置。

    当然有些功能在nginx配置比较好。例如IP黑名单。

    6      参考资料

    编号

    资料名称

    链接

    1

    Django官方文档

    https://docs.djangoproject.com/en/2.0/ref/

    2

    Request和response对象

    http://blog.csdn.net/liu_yanna/article/details/50174851

    3

    Django底层剖析之一次请求到响应的整个流程

    https://www.cnblogs.com/wanghzh/p/5831992.html

  • 相关阅读:
    UVA1292-----Strategic game-----树形DP解决树上的最小点覆盖问题
    【OpenGL游戏开发之三】OpenGl核心函数库汇总
    【Lucene】挖掘相关搜索词
    OpenMP入门教程(三)
    OpenMP入门教程(二)
    OpenMP入门教程(一)
    结构体的排序
    结构体
    循环群、对称群、陪集和拉格朗日定理、正规子群和商群
    概率论与数理统计(一)—— 随机事件与概率
  • 原文地址:https://www.cnblogs.com/StitchSun/p/8552651.html
Copyright © 2011-2022 走看看