首页 > 技术文章 > Django高级之中间件、csrf跨站请求伪造

gengfenglog 2021-03-31 18:34 原文

Django中间件

什么是中间件?

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.

  官方的说法:中间件是一个用来处理Django的请求和响应的框架级别的钩子。它是一个轻量、低级别的插件系统,用于在全局范围内改变Django的输入和输出。每个中间件组件都负责做一些特定的功能。

  但是由于其影响的是全局,所以需要谨慎使用,使用不当会影响性能。

  说的直白一点中间件是帮助我们在视图函数执行之前和执行之后都可以做一些额外的操作,它本质上就是一个自定义类,类中定义了几个方法,Django框架会在请求的特定的时间去执行这些方法。

中间件介绍和常用内置中间件

中间件:数据库中间件(mycat,分库分表),

    服务器中间件(tomcat,nginx),

    消息队列中间件(rabbitmq)

    django中间件(Middleware):介于request与response处理之间的一道处理过程,在全局上改变django的输入与输出

django 内置中间件

打开Django项目的Settings.py文件,看到下图的MIDDLEWARE配置项

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware', # 安全中间件
    'django.contrib.sessions.middleware.SessionMiddleware', # 会话中间件:处理session
    'django.middleware.common.CommonMiddleware',  # 站点中间件:处理是否带斜杠的
    'django.middleware.csrf.CsrfViewMiddleware',  # CSRF保护中间件:跨站请求伪造的处理
    'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件:提供用户认证服务
    'django.contrib.messages.middleware.MessageMiddleware',# 消息中间件:基于cookie或者会话的消息功能
    'django.middleware.clickjacking.XFrameOptionsMiddleware', # X-Frame-Options中间件:点击劫持保护
]

# SessionMiddleware源码
django.contrib.sessions.middleware.SessionMiddleware
process_request(self, request) # 请求来了会触发
process_response(self, request, response) # 请求走了会触发

  MIDDLEWARE配置项是一个列表(列表是有序的),列表中是一个个字符串,这些字符串其实是一个个类,也就是一个个中间件。

  我们之前已经接触过一个csrf相关的中间件了?我们一开始让大家把他注释掉,再提交post请求的时候,就不会被forbidden了,后来学会使用csrf_token之后就不再注释这个中间件了。

Django默认有七个中间件,但是django暴露给用户可以自定义中间件并且里面可以写五种方法

中间件可以定义五个方法,分别是:(主要的是process_request和process_response)
1.必须掌握
    process_request(self,request)
    process_response(self, request, response)

2.了解即可
    process_view(self, request, view_func, view_args, view_kwargs)
    process_template_response(self,request,response)
    process_exception(self, request, exception)

以上方法的返回值可以是None或一个HttpResponse对象,如果是None,则继续按照django定义的规则向后继续执行,如果是HttpResponse对象,则直接将该对象返回给用户。

自定义中间件:

自定义中间件需要在Django配置文件中告诉Django去哪找这个自定义的中间件,默认可以放在项目的对应app中,新建一个任意名称的文件夹eg:middleware,在此文件夹中新建任意名称的文件eg:my_middleware.py,然后在此文件中根据需求自定义五个方法中的任意一个或多个   

ps:中间件的本质就是含有五个可以修改的内置方法的类,所以自定义的时候需要做的就是先继承一个Django提供的中间件混合的父类(MiddlewareMixin)。

在settings.py的MIDDLEWARE配置项中注册两个自定义中间件:

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',
    'app01.middleware.MyMiddleWare',  # 自定义中间件
    'app01.middleware.MyMiddleWare1',  # 自定义中间件

]

process_request方法:(重点)

1.请求来的时候需要经过每一个中间件里面的 process_request方法,经过的顺序是按照配置文件中注册的中间件从上往下的顺序依次执行

2.如果中间件里面没有定义该方法,那么直接跳过执行下一个中间件

3.它的返回值可以是None,按正常流程继续走,交给下一个中间件处理,如果该方法返回了Httpresponse对象,那么请求将不再继续往后执行,而是直接原路返回(校验失败不允许访问.)

process_request方法就是用来做全局相关的所有限制功能

1 请求来了触发它,从上往下依次执行(在setting中中间件注册的位置)
    def process_request(self, request):
    如果返回None,就继续往下走
    如果返回四件套之一,直接就回去了
    
2 在这里面写请求来了的一些判断
3 request.META.REMOTE_ADDR  # 客户端地址
4 request.META.HTTP_USER_AGENT  # 客户端类型
#自定义中间件示例
from django.utils.deprecation import MiddlewareMixin

class MyMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('请求来了0000')
 

class MyMiddleWare1(MiddlewareMixin):
    def process_request(self, request):
        print('请求来了1111')

process_response方法:(重点)

1.响应走的时候需要经过每一个中间件里面的 process_response方法,该方法有两个额外的参数 request, response

2,该方法必须返回一个 HttpResponse对象

  1. 默认返回的就是形参 response
  2. 你也可以自己返回自己的

3.顺序是按照配置文件中注册了的中间件从下往上依次经过,如果你没有定义的话直接跳过执行下一个

# 导入MiddlewareMixin模块
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render, HttpResponse,redirect

# 定义中间件的类,它继承MiddlewareMixin
class MyMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('请求来了0000')
        # return HttpResponse('不让你来了')

    def process_response(self, request, response):
        print('请求走了0000')
      
        return response  # 一定要return response

# 定义中间件的类,它继承MiddlewareMixin
class MyMiddleWare1(MiddlewareMixin):
    def process_request(self, request):
        print('请求来了1111')

    def process_response(self, request, response):
        print('请求走了1111')
        return response

总结:

当前端发来请求,Django会执行每个中间件中的process_request,执行顺序按照配置文件由上至下执行。

响应走的时候,Django会执行每个中间件中的 process_response方法,process_response方法是在视图函数之后执行的,执行顺序是按照配置文件中注册了的中间件从下往上依次执行

process_view方法(了解)

process_view方法是在Django路由系统之后,视图系统之前执行的,执行顺序按照配置文件MIDDLEWARE中的注册顺序从前到后顺序执行的

Django会在调用视图函数之前调用process_view方法。

它应该返回None或一个HttpResponse对象。 如果返回None,Django将继续处理这个请求,执行任何其他中间件的process_view方法,然后在执行相应的视图。 如果它返回一个HttpResponse对象,Django不会调用适当的视图函数。 它将执行中间件的process_response方法并将应用到该HttpResponse并返回结果

def process_view(self, request, view_func, view_args, view_kwargs):
    
    """
    该方法有四个参数:
    request 是HttpRequest对象。
    view_func 是Django即将使用的视图函数。 (它是实际的函数对象,而不是函数的名称作为字符串。)
    view_args 是将传递给视图的位置参数的列表.
    view_kwargs 是将传递给视图的关键字参数的字典。 view_args和view_kwargs都不包含第一个视图参数(request)。    
    """      
    print('我是process view')

    # 如果return None,会执行执行视图函数
    # 手动执行了视图函数
    # response=view_func(request,view_args, view_kwargs)
    # 返回response,视图函数就不执行了
    return HttpResponse('ddddd')

process_exception方法(了解)

当视执行视图函数报错时,自动触发该方法(从下往上依次执行)

这个方法只有在视图函数中出现异常了才执行,它返回的值可以是一个None也可以是一个HttpResponse对象。如果是HttpResponse对象,Django将调用模板和中间件中的process_response方法,并返回给浏览器,否则将默认处理异常。如果返回一个None,则交给下一个中间件的process_exception方法来处理异常。它的执行顺序也是按照中间件注册顺序的倒序执行。

def process_exception(self, request, exception):
    """
    该方法有两个参数:
    request 是HttpRequest对象
    exception 是视图函数异常产生的Exception对象。
    """       
    # 记录错误日志
    print(exception)
    print('出错了')

process_template_response方法(了解)

该方法是在视图函数执行完成后立即执行,但是它有一个前提条件,那就是视图函数返回的对象有一个render()方法(或者表明该对象是一个TemplateResponse对象或等价方法)(从下往上依次执行)

# 该方法对视图函数返回值有要求,必须是一个含有render方法类的对象,才会执行此方法

def process_template_response(self,request,response):
    """
    该方法有两个参数:
    request 是HttpRequest对象
    response 是TemplateResponse对象(由视图函数或者中间件产生)
    """
    print('我执行了')
    return response

--------------------------------------------------------------
# views.py
class Test:
    def __init__(self,status,msg):
        self.status=status
        self.msg=msg
    def render(self):
        import json
        dic={'status':self.status,'msg':self.msg}

        return HttpResponse(json.dumps(dic))
def index(response):
    return Test(True,'测试')

自定义中间件总代码

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import render, HttpResponse,redirect
class MyMiddleWare(MiddlewareMixin):
    def process_request(self, request):
        print('请求来了0000')
        # return HttpResponse('不让你来了')

    def process_response(self, request, response):
        print('请求走了0000')
        response.set_cookie('name','lqz')
        response['x-head']='xxx'
        return response

class MyMiddleWare1(MiddlewareMixin):
    def process_request(self, request):
        print('请求来了1111')

    def process_response(self, request, response):
        print('请求走了1111')
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        # view_func 视图函数
        # view_args,  位置参数
        # view_kwargs 关键字参数
        print('我是process view')

        # 如果return None,会执行执行视图函数
        #手动执行了视图函数
        # response=view_func(request,view_args, view_kwargs)
        # 返回response,视图函数就不执行了
        # return HttpResponse('ddddd')

    def process_exception(self, request, exception):
        #记录错误日志
        print(exception)
        print('出错了')

    def process_template_response(self,request,response):
        print('我执行了')
        return response
View Code

中间件的执行流程

上一部分,我们了解了中间件中的5个方法,它们的参数、返回值以及什么时候执行,现在总结一下中间件的执行流程。

请求到达中间件之后,先按照正序执行每个注册中间件的process_request方法,process_request方法返回的值是None,就依次执行,如果返回的值是HttpResponse对象,不再执行后面的process_request方法,而是执行当前对应中间件的process_response方法(注意不是掉头执行所有的process_response方法),将HttpResponse对象返回给浏览器。也就是说:如果MIDDLEWARE中注册了6个中间件,执行过程中,第3个中间件返回了一个HttpResponse对象,那么第4,5,6中间件的process_request和process_response方法都不执行,顺序执行3,2,1中间件的process_response方法。

 

process_request方法都执行完后,匹配路由,找到要执行的视图函数,先不执行视图函数,先执行中间件中的process_view方法,process_view方法返回None,继续按顺序执行,所有process_view方法执行完后执行视图函数。假如中间件3 的process_view方法返回了HttpResponse对象,则4,5,6的process_view以及视图函数都不执行,直接从最后一个中间件,也就是中间件6的process_response方法开始倒序执行。

process_template_response和process_exception两个方法的触发是有条件的,执行顺序也是倒序。总结所有的执行流程如下:

实际应用

1、通过中间件实现访问控制,未登录用户重定向到登陆,登录用户才能访问home页面

2、通过中间件实现用户名,用户ip地址和客户端类型,登录时间记录

from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^login/',views.login),
    url(r'^home/',views.home),
    url(r'^index/',views.index),
]
路由层 urls.py
from django.shortcuts import render,redirect,HttpResponse
from app01 import models

def login(request):
    error_msg=''
    if request.method == 'POST':
        username=request.POST.get('username')
        password=request.POST.get('password')
        user_obj=models.User.objects.filter(username=username,password=password)
        if user_obj:
            request.session['name'] = username # 设置session
            request.session['is_login'] = True  # 保证登录状态(避免超时变为false)
            return redirect('/home/')
        else:
            error_msg='username or password error!' # 返回form错误信息
    return render(request,'login.html',{'error_msg':error_msg})


def home(request):
    return HttpResponse('<h1>这是home页面 只有登录了才能看到</h1>')


def index(request):
    return HttpResponse('<h1>这是index页面 也只有登录了才能看到<h1>')
视图层 views.py
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录认证</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="col-md-6 col-md-offset-3">
    <div class="panel panel-primary">
        <div class="panel-heading">
        </div>
        <div class="panel-body">
            <h1 class="text-center">登录</h1>
            <form action="" method="post">
                {% csrf_token %}
                <p>用户名:<input type="text" name="username" class="form-control"></p>
                <p>密码:<input type="password" name="password" class="form-control"></p>
                <input type="submit" class="btn btn-primary btn-block">
            </form>
            <h2 style="color: red">{{ error_msg }}</h2>
        </div>
    </div>
</div>
</body>
</html>
前端 login.html
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import redirect
# 自定义访问控制中间件
class LoginMiddleware(MiddlewareMixin):
    def process_request(self, request):
        # 如果不是路径不是login就会一直重定向
        if not request.path == '/login/':
            # 接着去获取登录状态
            is_login = request.session.get('is_login')
            # 如果没有登录,重定向到登录页面(如果登录了就会直接往下走)
            if not is_login:
                return redirect('/login')
            
# 自定义登录日志中间件
from app01 import models
class LoginLogMiddlewareMixin(MiddlewareMixin):
    def process_request(self, request):
        name = request.session.get('name',None)
        # name有值或为空都记录日志
        # 客户端的IP 地址
        ip = request.META['REMOTE_ADDR']
        # 客户端的user-agent 字符串
        user_agent = request.environ['HTTP_USER_AGENT']
        models.Log.objects.create(name=name, ip=ip, user_agent=user_agent)            
自定义 Middleware.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',
    'app01.middleware.LoginMiddleware',
    'app01.middleware.LoginLogMiddlewareMixin'
]
配置文件 settings.py
# 创建日志记录表
class Log(models.Model):
    name = models.CharField(max_length=32,null=True,verbose_name='用户名')
    ip = models.CharField(max_length=32,verbose_name='ip地址')
    user_agent = models.CharField(max_length=32,verbose_name='客户端类型')
    visit_time = models.DateTimeField(auto_now_add=True,verbose_name='登录时间')
    
# 不要忘了执行数据库迁移两条命令哦
模板层models.py

CSRF_TOKEN跨站请求伪造

详情参考

https://www.cnblogs.com/liuqingzheng/p/9505044.html
    
1 CSRF或者XSRF:跨站请求伪造
2 攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的但是却完成了攻击者所期望的一个操作,
比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
如下:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户
3 防范:CSRF攻击防范 Referer:上一次访问的地址(图片防盗链) https://www.lagou.com/gongsi/ - 在请求地址中添加 token 并验证 - 在 HTTP 头中自定义属性并验证 - 把随机字符串放在请求体中

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1.登录受信任网站A,并在本地生成Cookie。

  2.在不登出A的情况下,访问危险网站B。

看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:

  1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。

  2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了......)

  3.上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

django中处理csrf

1 中间件不要注释了
2 如果是form表单
<form action="" method="post">
    {% csrf_token %}
    <p>给谁转:<input type="text" name="to_user" id="id_name"></p>
    <p>转多少:<input type="text" name="money" id="id_money"></p>
    <input type="submit" value="转账">

</form>

3 如果是ajax提交:两种方案
       $.ajax({
            url: '/transfer/',
            method: 'post',
            //data: {to_user: $('#id_name').val(), money: $('#id_money').val(),csrfmiddlewaretoken:$('[name="csrfmiddlewaretoken"]').val()},
            data: {to_user: $('#id_name').val(), money: $('#id_money').val(),csrfmiddlewaretoken:'{{csrf_token}}'},
            success: function (data) {
                console.log(data)

            }
        })

cookie的处理

获取cookie:document.cookie

是一个字符串,可以自己用js切割,也可以用jquery的插件

获取cookie:$.cookie(‘csrftoken’)

设置cookie:$.cookie(‘key’,’value’)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="/static/jquery-3.3.1.js"></script>
    <script src="/static/jquery.cookie.js"></script> //用jquery的插件
    <title>Title</title>
</head>
<body>
<form action="" method="post">
    {% csrf_token %}
    <p>用户名:<input type="text" name="name"></p>
    <p>密码:<input type="text" name="password" id="pwd"></p>
    <p><input type="submit"></p>
</form>
<button class="btn">点我</button>
</body>
<script>
    $(".btn").click(function () {
        var token=$.cookie('csrftoken') // 获取
       
        $.ajax({
            url: '',
            headers:{'X-CSRFToken':token},
            type: 'post',
            data: {
                'name': $('[name="name"]').val(),
                'password': $("#pwd").val(),
            },
            success: function (data) {
                console.log(data)
            }

        })
    })
</script>
</html>

csrf放到请求头中

 $.ajax({
            url: '',
            headers:{'X-CSRFToken':token},
            type: 'post',
            data: {
                'name': $('[name="name"]').val(),
                'password': $("#pwd").val(),
            },
            success: function (data) {
                console.log(data)
            }

        })

全局使用csrf,局部视图函数禁用csrf

1 在视图函数上加装饰器
from django.views.decorators.csrf import csrf_exempt,csrf_protect
# 不再检测,局部禁用(前提是全站使用)
# @csrf_exempt
# 检测,局部使用(前提是全站禁用)
@csrf_protect
def csrf_token(request):
  if request.method=='POST':
        print(request.POST)

return HttpResponse('ok')
return render(request,'csrf_token.html')

总结

1 中间件:request对象,response
    -process_request
        -返回None,继续往下走
        -返回response对象,直接返回
    -process_response
        -处理response
    -定义一个类,继承MiddlewareMixin
    -配置文件配置(执行顺序)
2 中间件能干什么
    -全局的登陆认证
    -记录日志(访问日志,登录日志,客户端类型。。。)
    -对ip地址进行限制(一分钟访问十次)
    
3 csrf,xsrf:跨站请求伪造
    
4 django中处理csrf攻击
    1 form表单形式  {% csrf_token %}
    2 ajax提交:手动传,2种方式
    3 ajax提交:放到请求头中
       $.ajax({
            url: '/transfer/',
            headers:{'X-CSRFToken':'sssssss','name':'lqz'},
            method: 'post',
            data: {to_user: $('#id_name').val(), money: $('#id_money').val(),csrfmiddlewaretoken:'{{csrf_token}}'},
            success: function (data) {
                console.log(data)

            }
        })
        
        
4 请求头中的数据,django如何获取
    request.META.get('HTTP_字段名大写')

5 django的csrf中间件
 -全局使用csrf,中间件解开
    -局部禁用:在视图函数上加一个装饰器csrf_exempt
    -全局禁用,中间件加注释
    -局部使用:在视图函数上加一个装饰器csrf_protect

django启动流程

1  配置文件中指定WSGI_APPLICATION = 'day60.wsgi.application'

2  被wsgi服务器管理,一旦有请求进来,会触发application()

3  实际触发WSGIHandler类的__call__传入environ, start_response

4  把environ包装成request对象,执行中间件,执行路由,执行视图函数,返回response

5  最终结束django

 

推荐阅读