【Django 022】中间件Middleware(一):Django中间件本质和处理流程详解

中间件,是对原有MTV模型的一种补充,可以将其理解为添加额外功能的插件。我们分三节来学习下Django的中间件,首先学习下中间件的本质以及处理流程,然后用添加反爬功能为例做一个简单的自定义中间件。最后我们深入一点,学习下系统自带防跨站攻击的csrf中间件,并针对该中间件的源码看看如何让自己的请求被csrf信任。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

中间件

中间件(Middleware),是Django的一个插件框架,用来给请求和响应,甚至是异常,添加额外的功能。说到额外功能,大家可能最先想到的就是Python的装饰器(decorator),没错,Django中间件的本质其实就是一个类装饰器

配置和激活中间件

Django拥有很多自带的中间件,创建完项目,打开settings.py,就能看到如下配置

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',
]

这些就是Django为我们准备的中间件。如果还要添加新的中间件,往这个list中添加即可。但是要注意,和INSTALLED_APPS这种配置不同,中间件存在一个执行顺序的问题,所以放在list头和尾是有很大区别的。下面的处理流程中我们再详细说。

创建一个自己的中间件也非常容易,只需要创建一个类,继承系统的MiddlewareMixin,然后再将创建的类添加到上面的list中即可。

例如,我在项目的根目录创建了一个文件夹middleware,新建文件testmiddle.py,里面新建类如下

from django.utils.deprecation import MiddlewareMixin


class HelloMiddle(MiddlewareMixin):
    pass

这样就相当于新建了一个自己的中间件HelloMiddle,然后将其完整路径添加到配置中

MIDDLEWARE = [
    'middleware.testmiddle.HelloMiddle',
    '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',
]

我们不妨点进去看看这个MiddlewareMixin到底是个啥

class MiddlewareMixin:
    def __init__(self, get_response=None):
        self.get_response = get_response
        super().__init__()

    def __call__(self, request):
        response = None
        if hasattr(self, 'process_request'):
            response = self.process_request(request)
        response = response or self.get_response(request)
        if hasattr(self, 'process_response'):
            response = self.process_response(request, response)
        return response

先不管具体代码,一看这个结构,__init__加上__call__,这就是一个典型的类装饰器。所以才说Django的本质就是一个类装饰器。我们通过重写类装饰器的特定方法,达到在请求不同阶段添加插件的目的

想对python装饰器了解更多可以参考我的另一篇博客《一篇文章汇总Python装饰器全知识图谱(使用场景,基本用法,参数传递,闭包操作,类装饰器和AOP)》

AOP和中间件处理流程

说到中间件和装饰器,就不得不提一下AOP这个东西,也就是Aspect Oriented Programming,面向切面编程。

Python中AOP的本质也是装饰器,但是根据被装饰函数位置的不同分为不同的切点,同时根据装饰器功能的不一样又分为不同的切面。

所以说来说去,中间件,AOP和装饰器,在Django这里其实说的都是同一回事。

几个切点

中间件这个装饰器,只能在如下5个切点添加插件功能,换句话说,在自己定义的中间件类中,只能重写如下5个方法,来达到在MTV不同阶段添加插件的目的。

而这5个切入点,发生在请求阶段的为如下两个,按照配置中所有中间件从上至下执行

  • process_request(self, request)
  • process_view(self, request, func_view, func_args, func_kwargs)

发生在响应阶段的为如下三个,按照配置中所有中间件从下至上执行

  • process_exception(self, request, exception)
  • process_template_response(self, request, response)
  • process_response(self, request, response)

所以配置文件里面的每个中间件都可能有这5个方法,这么多中间件,每个中间件又有这么多方法,具体的处理流程是啥样的呢?

如果熟悉linux的防火墙iptables的朋友可以将这一个个的中间件类比为iptables中的表,而每个方法可以看作iptables中的链。下面我们来仔细看看。

处理流程

如下图所示
1-AOP.jpg
红色的四个切点分别对应着①②⑦⑧四个阶段,而图中没有出现的process_exception则是在view函数执行阶段触发的。

  • process_request
    用户发起的请求,在还没路由的时候,会按照配置中所有中间件从上至下执行process_request方法。如果有任意一个中间件的process_request返回HttpResponse对象则请求被中断,之后从目前的中间件开始从下至上执行process_response方法。如果所有中间件的process_request都返回None,则进入到下一步urls.py中判断路由

  • process_view
    在确定了view函数但还没执行的时候,和上面类似,从上至下执行process_view方法,如果有任意一个中间件的process_view返回HttpResponse对象则请求被中断,之后从目前的中间件开始从下至上执行process_response方法。如果所有中间件的process_view都返回None,则进入下一步views.py中执行具体view函数

  • process_template_response
    view函数执行阶段,如果一切正常,且返回的对象有render方法时,则从下至上执行process_template_response方法。注意该方法必须返回一个HttpResponse对象。

  • process_exception
    view函数执行阶段,如果出现异常,则从下至上执行process_exception方法。如果某一个中间件返回HttpResponse对象,则跳过上方剩余的中间件,开始从最下面执行process_response

  • process_response
    当前面所有的步骤执行完毕后,开始从下至上执行process_response方法,直到最上层的中间件执行完毕后,返回给用户。注意该方法必须返回一个HttpResponse对象。

验证

下面我们就来创建几个中间件来验证下上面的这个流程。

MIDDLEWARE = [
    'middleware.testmiddle.Test1',
    'middleware.testmiddle.Test2',
    '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.testmiddle.Test3',
    'middleware.testmiddle.Test4',
]

创建了Test1 - Test4一共四个中间件,其中两个在头两个在尾。四个中间件定义如下

class Test1(MiddlewareMixin):
    def process_request(self, request):
        print('process request test1')

    def process_view(self, request, func_view, func_args, func_kwargs):
        print('process view test1')

    def process_exception(self, request, exception):
        print('process exception test1')
        # return HttpResponse('hello')


    def process_template_response(self, request, response):
        print('process template response test1')
        return response

    def process_response(self, request, response):
        print('process response test1')
        return response

class Test2(MiddlewareMixin):
    def process_request(self, request):
        print('process request test2')

    def process_view(self, request, func_view, func_args, func_kwargs):
        print('process view test2')

    def process_exception(self, request, exception):
        print('process exception test2')

    def process_template_response(self, request, response):
        print('process template response test2')
        return response

    def process_response(self, request, response):
        print('process response test2')
        return response


class Test3(MiddlewareMixin):
    def process_request(self, request):
        print('process request test3')

    def process_view(self, request, func_view, func_args, func_kwargs):
        print('process view test3')

    def process_exception(self, request, exception):
        print('process exception test3')
        return HttpResponse('Oh error')

    def process_template_response(self, request, response):
        print('process template response test3')
        return response

    def process_response(self, request, response):
        print('process response test3')
        return response


class Test4(MiddlewareMixin):
    def process_request(self, request):
        print('process request test4')

    def process_view(self, request, func_view, func_args, func_kwargs):
        print('process view test4')

    def process_exception(self, request, exception):
        print('process exception test4')

    def process_template_response(self, request, response):
        print('process template response test4')
        return response

    def process_response(self, request, response):
        print('process response test4')
        return response

需要注意的是,一旦发生异常,会在Test3那里返回HttpResponse('oh error')

之后创建路由和view函数如下

path('test_middleware/',views.test_middleware, name='test_middleware'),
class test():
    def render(self):
        a = 1/0
        return HttpResponse('Life is awesome')

def test_middleware(request):
    return test()

如前所述,为了触发process_template_response方法,这里返回一个带render方法的对象。这里在最终返回前有一个除以0的错误。

打印结果如下

process request test1
process request test2
process request test3
process request test4
process view test1
process view test2
process view test3
process view test4
process template response test4
process template response test3
process template response test2
process template response test1
process exception test4
process exception test3
process response test4
process response test3
process response test2
process response test1

可见在Test3将异常处理后,直接进入了Test4的process_response

几个注意事项

  • 假如view函数中异常抛出在返回一个带render方法的对象之前,那么也是不会触发process_template_response的。例如上面将a = 1/0移到return test()之前。

  • 一旦异常抛出,则该view函数的原本返回值作废,如果所有的process_exceptionprocess_response都没有返回HttpResponse对象,则用户看到5xx错误码。所以通常统一在异常处理中跳转到更友好的页面以防止没有被捕捉的异常被用户看见。

  • 为了触发process_template_response,这里构造了一个带render方法的类,但是实际中一般直接在view函数中返回render对象,所以其实很难有机会触发process_template_response方法

实际应用

理论知识还是挺枯燥的,下面就来几个实际应用来感受下。在配置中将上面的四个中间件只留下Test1来操作。

统计信息

在用户请求处理前可以对所有请求的信息进行统计和分析,例如用户的IP,浏览器类型,操作系统等等。

如果要统计用户的IP

def process_request(self, request):
        print(request.META.get('REMOTE_ADDR'))

当然这里是打印,实际使用可以结合logging或者是数据库来进行存储。

权重和黑白名单

既然可以知道用户的信息,就可以根据这些信息对请求做一些预处理。

例如创建一个抢票网站,普通用户有50%概率抢到,而对于某些IP段可以限制其概率为20%甚至是0。这种预处理功能虽说可以加到view函数中来实现,但是出于低耦合的目的,建议是单独放在中间件中。

创建路由和view函数如下

path('get_ticket/', views.get_ticket, name='get_ticket'),
def get_ticket(request):
    if random.randint(0, 100) > 50:
        return HttpResponse('Congratulations!!')
    else:
        return HttpResponse('Sorry, better luck next time!!')

这个时候用户访问抢到票的概率约为50%。

修改中间件Test1的process_request如下

class Test1(MiddlewareMixin):
    def process_request(self, request):
        ip = request.META.get('REMOTE_ADDR')
        if ip.startswith('127.0.'):
            if random.randint(0,100)>90:
                return HttpResponse('Congratulations!!')
            else:
                return HttpResponse('Sorry!!')

这里为了举例子,将127.0.开头的概率降为了约10%,实际中可以根据用户的cookie等个人信息来做判定。如果要使得页面更为友好,可以在这里返回一个正在排队之类的页面让用户等待。

总结

这一节我们学习了中间件的本质是一个装饰器,并验证了五个切点的执行顺序,最后用两个简单的例子感受了下自定义中间件。下一节我们就针对反爬虫的频率控制来制作一个稍微复杂点的中间件。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值